From 6530bea4d0143b84d8b41099d431bc1cf3203635 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:38:45 +1100 Subject: [PATCH 01/47] [8.x] [Security Solution] Update rule link in cases activities (#198836) (#199242) # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Update rule link in cases activities (#198836)](https://github.com/elastic/kibana/pull/198836) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: christineweng <18648970+christineweng@users.noreply.github.com> --- .../cases/public/components/links/index.tsx | 2 +- .../user_actions/comment/alert_event.test.tsx | 63 ++++++++++++++++--- .../user_actions/comment/alert_event.tsx | 10 ++- .../public/cases/pages/index.tsx | 33 ++++------ 4 files changed, 75 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/cases/public/components/links/index.tsx b/x-pack/plugins/cases/public/components/links/index.tsx index f1e8ca5cdb4af..9b610db63ed10 100644 --- a/x-pack/plugins/cases/public/components/links/index.tsx +++ b/x-pack/plugins/cases/public/components/links/index.tsx @@ -12,7 +12,7 @@ import { useCaseViewNavigation, useConfigureCasesNavigation } from '../../common import * as i18n from './translations'; export interface CasesNavigation { - href: K extends 'configurable' ? (arg: T) => string : string; + href?: K extends 'configurable' ? (arg: T) => string : string; onClick: K extends 'configurable' ? (arg: T, arg2: React.MouseEvent | MouseEvent) => Promise | void : (arg: T) => Promise | void; diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.test.tsx index 60d5759de6e21..d060f1d9d71ac 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.test.tsx @@ -47,10 +47,38 @@ describe('Alert events', () => { expect(wrapper.text()).toBe('added an alert from Awesome rule'); }); - it('does NOT render the link when the rule id is null', async () => { + it('renders the link when onClick is provided but href is not valid', async () => { const wrapper = mount( - + + + ); + + expect( + wrapper.find(`[data-test-subj="alert-rule-link-action-id-1"]`).first().exists() + ).toBeTruthy(); + }); + + it('renders the link when href is valid but onClick is not available', async () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="alert-rule-link-action-id-1"]`).first().exists() + ).toBeTruthy(); + }); + + it('does NOT render the link when the href and onclick are invalid but it shows the rule name', async () => { + const wrapper = mount( + + ); @@ -61,10 +89,10 @@ describe('Alert events', () => { expect(wrapper.text()).toBe('added an alert from Awesome rule'); }); - it('does NOT render the link when the href is invalid but it shows the rule name', async () => { + it('does NOT render the link when the rule id is null', async () => { const wrapper = mount( - + ); @@ -131,9 +159,28 @@ describe('Alert events', () => { expect(result.getByTestId('alert-rule-link-action-id-1')).toHaveTextContent('Awesome rule'); }); - it('does NOT render the link when the rule id is null', async () => { + it('renders the link when onClick is provided but href is not valid', async () => { const result = appMock.render( - + + ); + expect(result.getByTestId('alert-rule-link-action-id-1')).toHaveTextContent('Awesome rule'); + }); + + it('renders the link when href is valid but onClick is not available', async () => { + const result = appMock.render( + + ); + expect(result.getByTestId('alert-rule-link-action-id-1')).toHaveTextContent('Awesome rule'); + }); + + it('does NOT render the link when the href and onclick are invalid but it shows the rule name', async () => { + const result = appMock.render( + ); expect(result.getByTestId('multiple-alerts-user-action-action-id-1')).toHaveTextContent( @@ -142,9 +189,9 @@ describe('Alert events', () => { expect(result.queryByTestId('alert-rule-link-action-id-1')).toBeFalsy(); }); - it('does NOT render the link when the href is invalid but it shows the rule name', async () => { + it('does NOT render the link when the rule id is null', async () => { const result = appMock.render( - + ); expect(result.getByTestId('multiple-alerts-user-action-action-id-1')).toHaveTextContent( diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx index 42e5e6b9d4427..a8ffbb987a021 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/alert_event.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { isEmpty } from 'lodash'; import { EuiLoadingSpinner } from '@elastic/eui'; @@ -38,12 +38,18 @@ const RuleLink: React.FC = memo( const ruleDetailsHref = getRuleDetailsHref?.(ruleId); const finalRuleName = ruleName ?? i18n.UNKNOWN_RULE; + const isValidLink = useMemo(() => { + if (!onRuleDetailsClick && !ruleDetailsHref) { + return false; + } + return !isEmpty(ruleId); + }, [onRuleDetailsClick, ruleDetailsHref, ruleId]); if (loadingAlertData) { return ; } - if (!isEmpty(ruleId) && ruleDetailsHref != null) { + if (isValidLink) { return ( { const { getAppUrl, navigateTo } = useNavigation(); const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const dispatch = useDispatch(); - const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl( - SecurityPageName.rules - ); const { openFlyout } = useExpandableFlyoutApi(); - const getDetectionsRuleDetailsHref = useCallback( - (ruleId: string | null | undefined) => - detectionsFormatUrl(getRuleDetailsUrl(ruleId ?? '', detectionsUrlSearch)), - [detectionsFormatUrl, detectionsUrlSearch] - ); - const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions'); const showAlertDetails = useCallback( @@ -71,6 +60,15 @@ const CaseContainerComponent: React.FC = () => { [openFlyout, telemetry] ); + const onRuleDetailsClick = useCallback( + (ruleId: string | null | undefined) => { + if (ruleId) { + openFlyout({ right: { id: RulePanelKey, params: { ruleId } } }); + } + }, + [openFlyout] + ); + const { onLoad: onAlertsTableLoaded } = useFetchNotes(); const endpointDetailsHref = (endpointId: string) => @@ -138,16 +136,7 @@ const CaseContainerComponent: React.FC = () => { }, }, ruleDetailsNavigation: { - href: getDetectionsRuleDetailsHref, - onClick: async (ruleId: string | null | undefined, e) => { - if (e) { - e.preventDefault(); - } - return navigateTo({ - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(ruleId ?? ''), - }); - }, + onClick: onRuleDetailsClick, }, showAlertDetails, timelineIntegration: { From 91da53ae916be3bddf59e56213bddb8c932ebc42 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:36:43 +1100 Subject: [PATCH 02/47] [8.x] [Canvas] Fix bug when trying to move elements (#199211) (#199246) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Canvas] Fix bug when trying to move elements (#199211)](https://github.com/elastic/kibana/pull/199211) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Hannah Mudge --- x-pack/plugins/canvas/public/state/actions/elements.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js index 57b7da1ce1fef..dc21cf1b8f2bc 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.js @@ -65,9 +65,12 @@ function getBareElement(el, includeId = false) { export const elementLayer = createAction('elementLayer'); -export const setMultiplePositions = createAction('setMultiplePosition', (repositionedElements) => ({ - repositionedElements, -})); +export const setMultiplePositions = createAction( + 'setMultiplePositions', + (repositionedElements) => ({ + repositionedElements, + }) +); export const flushContext = createAction('flushContext'); export const flushContextAfterIndex = createAction('flushContextAfterIndex'); From d26f787e957152892e7d56c90279a4e6646a48bb Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:47:14 +1100 Subject: [PATCH 03/47] [8.x] [Fleet] Improve input template API yaml comments (#199168) (#199239) # Backport This will backport the following commits from `main` to `8.x`: - [[Fleet] Improve input template API yaml comments (#199168)](https://github.com/elastic/kibana/pull/199168) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --------- Co-authored-by: Nicolas Chaulet --- .../package_policies_to_agent_inputs.ts | 8 +- .../packages/__fixtures__/docker_2_11_0.ts | 230 ++++++++++++++++++ .../redis_1_18_0_package_info.json | 18 ++ .../redis_1_18_0_streams_template.ts | 9 + .../get_templates_inputs.test.ts.snap | 49 +++- .../epm/packages/get_template_inputs.ts | 138 ++++++++--- .../epm/packages/get_templates_inputs.test.ts | 13 + 7 files changed, 413 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/docker_2_11_0.ts diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.ts index 807312fe5e7cb..f2a39c6f8800b 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.ts @@ -112,7 +112,8 @@ export const mergeInputsOverrides = ( export const getFullInputStreams = ( input: PackagePolicyInput, - allStreamEnabled: boolean = false + allStreamEnabled: boolean = false, + streamsOriginalIdsMap?: Map // Map of stream ids ): FullAgentPolicyInputStream => { return { ...(input.compiled_input || {}), @@ -121,8 +122,9 @@ export const getFullInputStreams = ( streams: input.streams .filter((stream) => stream.enabled || allStreamEnabled) .map((stream) => { + const streamId = stream.id; const fullStream: FullAgentPolicyInputStream = { - id: stream.id, + id: streamId, data_stream: stream.data_stream, ...stream.compiled_stream, ...Object.entries(stream.config || {}).reduce((acc, [key, { value }]) => { @@ -130,6 +132,8 @@ export const getFullInputStreams = ( return acc; }, {} as { [k: string]: any }), }; + streamsOriginalIdsMap?.set(fullStream.id, streamId); + return fullStream; }), } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/docker_2_11_0.ts b/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/docker_2_11_0.ts new file mode 100644 index 0000000000000..67bde1882cfed --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/docker_2_11_0.ts @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DOCKER_2_11_0_PACKAGE_INFO = { + name: 'docker', + title: 'Docker', + version: '2.11.0', + release: 'ga', + description: 'Collect metrics and logs from Docker instances with Elastic Agent.', + type: 'integration', + download: '/epr/docker/docker-2.11.0.zip', + path: '/package/docker/2.11.0', + icons: [ + { + src: '/img/logo_docker.svg', + path: '/package/docker/2.11.0/img/logo_docker.svg', + title: 'logo docker', + size: '32x32', + type: 'image/svg+xml', + }, + ], + conditions: { + kibana: { + version: '^8.8.0', + }, + }, + owner: { + type: 'elastic', + github: 'elastic/obs-cloudnative-monitoring', + }, + categories: ['observability', 'containers'], + signature_path: '/epr/docker/docker-2.11.0.zip.sig', + format_version: '3.2.2', + readme: '/package/docker/2.11.0/docs/README.md', + license: 'basic', + screenshots: [ + { + src: '/img/docker-overview.png', + path: '/package/docker/2.11.0/img/docker-overview.png', + title: 'Docker Overview', + size: '5120x2562', + type: 'image/png', + }, + ], + assets: [ + '/package/docker/2.11.0/LICENSE.txt', + '/package/docker/2.11.0/changelog.yml', + '/package/docker/2.11.0/manifest.yml', + '/package/docker/2.11.0/docs/README.md', + '/package/docker/2.11.0/img/docker-overview.png', + '/package/docker/2.11.0/img/logo_docker.svg', + '/package/docker/2.11.0/data_stream/container/manifest.yml', + '/package/docker/2.11.0/data_stream/container/sample_event.json', + '/package/docker/2.11.0/data_stream/container_logs/manifest.yml', + '/package/docker/2.11.0/data_stream/container_logs/sample_event.json', + '/package/docker/2.11.0/data_stream/cpu/manifest.yml', + '/package/docker/2.11.0/data_stream/cpu/sample_event.json', + '/package/docker/2.11.0/data_stream/diskio/manifest.yml', + '/package/docker/2.11.0/data_stream/diskio/sample_event.json', + '/package/docker/2.11.0/data_stream/event/manifest.yml', + '/package/docker/2.11.0/data_stream/event/sample_event.json', + '/package/docker/2.11.0/data_stream/healthcheck/manifest.yml', + '/package/docker/2.11.0/data_stream/healthcheck/sample_event.json', + '/package/docker/2.11.0/data_stream/image/manifest.yml', + '/package/docker/2.11.0/data_stream/image/sample_event.json', + '/package/docker/2.11.0/data_stream/info/manifest.yml', + '/package/docker/2.11.0/data_stream/info/sample_event.json', + '/package/docker/2.11.0/data_stream/memory/manifest.yml', + '/package/docker/2.11.0/data_stream/memory/sample_event.json', + '/package/docker/2.11.0/data_stream/network/manifest.yml', + '/package/docker/2.11.0/data_stream/network/sample_event.json', + '/package/docker/2.11.0/kibana/dashboard/docker-AV4REOpp5NkDleZmzKkE.json', + '/package/docker/2.11.0/kibana/search/docker-Metrics-Docker.json', + '/package/docker/2.11.0/data_stream/container/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/container/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/container/fields/fields.yml', + '/package/docker/2.11.0/data_stream/container_logs/fields/agent.yml', + '/package/docker/2.11.0/data_stream/container_logs/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/container_logs/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/container_logs/fields/fields.yml', + '/package/docker/2.11.0/data_stream/cpu/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/cpu/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/cpu/fields/fields.yml', + '/package/docker/2.11.0/data_stream/diskio/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/diskio/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/diskio/fields/fields.yml', + '/package/docker/2.11.0/data_stream/event/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/event/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/event/fields/fields.yml', + '/package/docker/2.11.0/data_stream/healthcheck/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/healthcheck/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/healthcheck/fields/fields.yml', + '/package/docker/2.11.0/data_stream/image/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/image/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/image/fields/fields.yml', + '/package/docker/2.11.0/data_stream/info/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/info/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/info/fields/fields.yml', + '/package/docker/2.11.0/data_stream/memory/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/memory/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/memory/fields/fields.yml', + '/package/docker/2.11.0/data_stream/network/fields/base-fields.yml', + '/package/docker/2.11.0/data_stream/network/fields/ecs.yml', + '/package/docker/2.11.0/data_stream/network/fields/fields.yml', + '/package/docker/2.11.0/data_stream/container/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/container_logs/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/cpu/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/diskio/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/event/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/healthcheck/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/image/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/info/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/memory/agent/stream/stream.yml.hbs', + '/package/docker/2.11.0/data_stream/network/agent/stream/stream.yml.hbs', + ], + policy_templates: [ + { + name: 'docker', + title: 'Docker logs and metrics', + description: 'Collect logs and metrics from Docker instances', + inputs: [ + { + type: 'filestream', + title: 'Collect Docker container logs', + description: 'Collecting docker container logs', + }, + ], + multiple: true, + }, + ], + data_streams: [ + { + type: 'logs', + dataset: 'docker.container_logs', + title: 'Docker container logs', + release: 'ga', + streams: [ + { + input: 'filestream', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Docker container log path', + multi: true, + required: true, + show_user: false, + default: ['/var/lib/docker/containers/${docker.container.id}/*-json.log'], + }, + { + name: 'containerParserStream', + type: 'text', + title: "Container parser's stream configuration", + multi: false, + required: true, + show_user: false, + default: 'all', + }, + { + name: 'condition', + type: 'text', + title: 'Condition', + description: + 'Condition to filter when to apply this datastream. Refer to [Docker provider](https://www.elastic.co/guide/en/fleet/current/docker-provider.html) to find the available keys and to [Conditions](https://www.elastic.co/guide/en/fleet/current/dynamic-input-configuration.html#conditions) on how to use the available keys in conditions.', + multi: false, + required: false, + show_user: true, + }, + { + name: 'additionalParsersConfig', + type: 'yaml', + title: 'Additional parsers configuration', + multi: false, + required: true, + show_user: false, + default: + "# - ndjson:\n# target: json\n# ignore_decoding_error: true\n# - multiline:\n# type: pattern\n# pattern: '^\\['\n# negate: true\n# match: after\n", + }, + { + name: 'processors', + type: 'yaml', + title: 'Processors', + description: + 'Processors are used to reduce the number of fields in the exported event or to enhance the event with metadata. This executes in the agent before the events are shipped. See [Processors](https://www.elastic.co/guide/en/beats/filebeat/current/filtering-and-enhancing-data.html) for details.', + multi: false, + required: false, + show_user: false, + }, + ], + template_path: 'stream.yml.hbs', + title: 'Collect Docker container logs', + description: 'Collect Docker container logs', + enabled: true, + }, + ], + package: 'docker', + elasticsearch: {}, + path: 'container_logs', + }, + ], +}; + +export const DOCKER_2_11_0_ASSETS_MAP = new Map([ + [ + 'docker-2.11.0/data_stream/container_logs/agent/stream/stream.yml.hbs', + Buffer.from(`id: docker-container-logs-\${docker.container.name}-\${docker.container.id} +paths: +{{#each paths}} + - {{this}} +{{/each}} +{{#if condition}} +condition: {{ condition }} +{{/if}} +parsers: +- container: + stream: {{ containerParserStream }} + format: docker +{{ additionalParsersConfig }} + +{{#if processors}} +processors: +{{processors}} +{{/if}} +`), + ], +]); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/redis_1_18_0_package_info.json b/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/redis_1_18_0_package_info.json index 57c9b0c68fac9..5aa5c4a878baf 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/redis_1_18_0_package_info.json +++ b/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/redis_1_18_0_package_info.json @@ -139,6 +139,15 @@ "show_user": false, "default": "20s" }, + { + "name": "tags", + "type": "text", + "title": "Tags", + "multi": true, + "required": false, + "show_user": false, + "default": "" + }, { "name": "maxconn", "type": "integer", @@ -203,6 +212,15 @@ { "input": "redis/metrics", "vars": [ + { + "name": "tags_streams", + "type": "text", + "title": "Tags in streams", + "multi": true, + "required": false, + "show_user": false, + "default": "" + }, { "name": "key.patterns", "type": "yaml", diff --git a/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/redis_1_18_0_streams_template.ts b/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/redis_1_18_0_streams_template.ts index 5ff46f358bbe7..207a48d7132a5 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/redis_1_18_0_streams_template.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/__fixtures__/redis_1_18_0_streams_template.ts @@ -76,6 +76,15 @@ period: {{period}} processors: {{processors}} {{/if}} +tags: + - test +{{#each tags as |tag i|}} + - {{tag}} +{{/each}} +tags_streams: +{{#each tags_streams as |tag i|}} + - {{tag}} +{{/each}} `), ], ]); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/__snapshots__/get_templates_inputs.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/packages/__snapshots__/get_templates_inputs.test.ts.snap index 786e8ce9acec4..5f67aea3c4847 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/__snapshots__/get_templates_inputs.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/epm/packages/__snapshots__/get_templates_inputs.test.ts.snap @@ -8,17 +8,16 @@ exports[`Fleet - getTemplateInputs should work for input package 1`] = ` streams: # Custom log file: Custom log file - id: logfile-log.logs - data_stream: - dataset: - # Dataset name: Set the name for your dataset. Changing the dataset will send the data to a different index. You can't use \`-\` in the name of a dataset and only valid characters for [Elasticsearch index names](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html). - - paths: - - # Log file path: Path to log files to be collected - exclude_files: - - # Exclude files: Patterns to be ignored ignore_older: 72h - tags: - - # Tags: Tags to include in the published event + # data_stream: + # dataset: + # # Dataset name: Set the name for your dataset. Changing the dataset will send the data to a different index. You can't use \`-\` in the name of a dataset and only valid characters for [Elasticsearch index names](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html). + # paths: + # - # Log file path: Path to log files to be collected + # exclude_files: + # - # Exclude files: Patterns to be ignored + # tags: + # - # Tags: Tags to include in the published event " `; @@ -49,8 +48,34 @@ exports[`Fleet - getTemplateInputs should work for integration package 1`] = ` pattern: '*' maxconn: 10 network: tcp - username: # Username - password: # Password period: 10s + tags: + - test + # - # Tags + # username: # Username + # password: # Password + # tags_streams: + # - # Tags in streams +" +`; + +exports[`Fleet - getTemplateInputs should work for package with dynamic ids 1`] = ` +"inputs: + # Collect Docker container logs: Collecting docker container logs + - id: docker-filestream + type: filestream + streams: + # Collect Docker container logs: Collect Docker container logs + - id: 'docker-container-logs-\${docker.container.name}-\${docker.container.id}' + data_stream: + dataset: docker.container_logs + type: logs + paths: + - '/var/lib/docker/containers/\${docker.container.id}/*-json.log' + parsers: + - container: + stream: all + format: docker + # condition: # Condition: Condition to filter when to apply this datastream. Refer to [Docker provider](https://www.elastic.co/guide/en/fleet/current/docker-provider.html) to find the available keys and to [Conditions](https://www.elastic.co/guide/en/fleet/current/dynamic-input-configuration.html#conditions) on how to use the available keys in conditions. " `; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_template_inputs.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_template_inputs.ts index d3c3bf7345ce6..ec72a14e06f96 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get_template_inputs.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_template_inputs.ts @@ -51,20 +51,31 @@ type PackageWithInputAndStreamIndexed = Record< // Function based off storedPackagePolicyToAgentInputs, it only creates the `streams` section instead of the FullAgentPolicyInput export const templatePackagePolicyToFullInputStreams = ( - packagePolicyInputs: PackagePolicyInput[] + packagePolicyInputs: PackagePolicyInput[], + inputAndStreamsIdsMap?: Map }> ): TemplateAgentPolicyInput[] => { const fullInputsStreams: TemplateAgentPolicyInput[] = []; if (!packagePolicyInputs || packagePolicyInputs.length === 0) return fullInputsStreams; packagePolicyInputs.forEach((input) => { + const streamsIdsMap = new Map(); + + const inputId = input.policy_template + ? `${input.policy_template}-${input.type}` + : `${input.type}`; const fullInputStream = { // @ts-ignore-next-line the following id is actually one level above the one in fullInputStream, but the linter thinks it gets overwritten - id: input.policy_template ? `${input.policy_template}-${input.type}` : `${input.type}`, + id: inputId, type: input.type, - ...getFullInputStreams(input, true), + ...getFullInputStreams(input, true, streamsIdsMap), }; + inputAndStreamsIdsMap?.set(fullInputStream.id, { + originalId: inputId, + streams: streamsIdsMap, + }); + // deeply merge the input.config values with the full policy input stream merge( fullInputStream, @@ -167,8 +178,13 @@ export async function getTemplateInputs( ...emptyPackagePolicy, inputs: compiledInputs, }; + const inputIdsDestinationMap = new Map< + string, + { originalId: string; streams: Map } + >(); const inputs = templatePackagePolicyToFullInputStreams( - packagePolicyWithInputs.inputs as PackagePolicyInput[] + packagePolicyWithInputs.inputs as PackagePolicyInput[], + inputIdsDestinationMap ); if (format === 'json') { @@ -181,7 +197,7 @@ export async function getTemplateInputs( sortKeys: _sortYamlKeys, } ); - return addCommentsToYaml(yaml, buildIndexedPackage(packageInfo)); + return addCommentsToYaml(yaml, buildIndexedPackage(packageInfo), inputIdsDestinationMap); } return { inputs: [] }; @@ -247,7 +263,8 @@ function buildIndexedPackage(packageInfo: PackageInfo): PackageWithInputAndStrea function addCommentsToYaml( yaml: string, - packageIndexInputAndStreams: PackageWithInputAndStreamIndexed + packageIndexInputAndStreams: PackageWithInputAndStreamIndexed, + inputIdsDestinationMap: Map }> ) { const doc = yamlDoc.parseDocument(yaml); // Add input and streams comments @@ -261,28 +278,16 @@ function addCommentsToYaml( if (!yamlDoc.isScalar(inputIdNode)) { return; } - const inputId = inputIdNode.value as string; + const inputId = + inputIdsDestinationMap.get(inputIdNode.value as string)?.originalId ?? + (inputIdNode.value as string); const pkgInput = packageIndexInputAndStreams[inputId]; if (pkgInput) { inputItem.commentBefore = ` ${pkgInput.title}${ pkgInput.description ? `: ${pkgInput.description}` : '' }`; - yamlDoc.visit(inputItem, { - Scalar(key, node) { - if (node.value) { - const val = node.value.toString(); - for (const varDef of pkgInput.vars ?? []) { - const placeholder = getPlaceholder(varDef); - if (val.includes(placeholder)) { - node.comment = ` ${varDef.title}${ - varDef.description ? `: ${varDef.description}` : '' - }`; - } - } - } - }, - }); + commentVariablesInYaml(inputItem, pkgInput.vars ?? []); const yamlStreams = inputItem.get('streams'); if (!yamlDoc.isCollection(yamlStreams)) { @@ -294,27 +299,16 @@ function addCommentsToYaml( } const streamIdNode = streamItem.get('id', true); if (yamlDoc.isScalar(streamIdNode)) { - const streamId = streamIdNode.value as string; + const streamId = + inputIdsDestinationMap + .get(inputIdNode.value as string) + ?.streams?.get(streamIdNode.value as string) ?? (streamIdNode.value as string); const pkgStream = pkgInput.streams[streamId]; if (pkgStream) { streamItem.commentBefore = ` ${pkgStream.title}${ pkgStream.description ? `: ${pkgStream.description}` : '' }`; - yamlDoc.visit(streamItem, { - Scalar(key, node) { - if (node.value) { - const val = node.value.toString(); - for (const varDef of pkgStream.vars ?? []) { - const placeholder = getPlaceholder(varDef); - if (val.includes(placeholder)) { - node.comment = ` ${varDef.title}${ - varDef.description ? `: ${varDef.description}` : '' - }`; - } - } - } - }, - }); + commentVariablesInYaml(streamItem, pkgStream.vars ?? []); } } }); @@ -324,3 +318,71 @@ function addCommentsToYaml( return doc.toString(); } + +function commentVariablesInYaml(rootNode: yamlDoc.Node, vars: RegistryVarsEntry[] = []) { + // Node need to be deleted after the end of the visit to be able to visit every node + const toDeleteFn: Array<() => void> = []; + yamlDoc.visit(rootNode, { + Scalar(key, node, path) { + if (node.value) { + const val = node.value.toString(); + for (const varDef of vars) { + const placeholder = getPlaceholder(varDef); + if (val.includes(placeholder)) { + node.comment = ` ${varDef.title}${varDef.description ? `: ${varDef.description}` : ''}`; + + const paths = [...path].reverse(); + + let prevPart: yamlDoc.Node | yamlDoc.Document | yamlDoc.Pair = node; + + for (const pathPart of paths) { + if (yamlDoc.isCollection(pathPart)) { + // If only one items in the collection comment the whole collection + if (pathPart.items.length === 1) { + continue; + } + } + if (yamlDoc.isSeq(pathPart)) { + const commentDoc = new yamlDoc.Document(new yamlDoc.YAMLSeq()); + commentDoc.add(prevPart); + const commentStr = commentDoc.toString().trimEnd(); + pathPart.comment = pathPart.comment + ? `${pathPart.comment} ${commentStr}` + : ` ${commentStr}`; + const keyToDelete = prevPart; + + toDeleteFn.push(() => { + pathPart.items.forEach((item, index) => { + if (item === keyToDelete) { + pathPart.delete(new yamlDoc.Scalar(index)); + } + }); + }); + return; + } + + if (yamlDoc.isMap(pathPart)) { + if (yamlDoc.isPair(prevPart)) { + const commentDoc = new yamlDoc.Document(new yamlDoc.YAMLMap()); + commentDoc.add(prevPart); + const commentStr = commentDoc.toString().trimEnd(); + + pathPart.comment = pathPart.comment + ? `${pathPart.comment}\n ${commentStr.toString()}` + : ` ${commentStr.toString()}`; + const keyToDelete = prevPart.key; + toDeleteFn.push(() => pathPart.delete(keyToDelete)); + } + return; + } + + prevPart = pathPart; + } + } + } + } + }, + }); + + toDeleteFn.forEach((deleteFn) => deleteFn()); +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_templates_inputs.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_templates_inputs.test.ts index 087002f212852..1a3738d8eaa82 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get_templates_inputs.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_templates_inputs.test.ts @@ -16,6 +16,7 @@ import REDIS_1_18_0_PACKAGE_INFO from './__fixtures__/redis_1_18_0_package_info. import { getPackageAssetsMap, getPackageInfo } from './get'; import { REDIS_ASSETS_MAP } from './__fixtures__/redis_1_18_0_streams_template'; import { LOGS_2_3_0_ASSETS_MAP, LOGS_2_3_0_PACKAGE_INFO } from './__fixtures__/logs_2_3_0'; +import { DOCKER_2_11_0_PACKAGE_INFO, DOCKER_2_11_0_ASSETS_MAP } from './__fixtures__/docker_2_11_0'; jest.mock('./get'); @@ -41,6 +42,7 @@ packageInfoCache.set('limited_package-0.0.0', { packageInfoCache.set('redis-1.18.0', REDIS_1_18_0_PACKAGE_INFO); packageInfoCache.set('log-2.3.0', LOGS_2_3_0_PACKAGE_INFO); +packageInfoCache.set('docker-2.11.0', DOCKER_2_11_0_PACKAGE_INFO); describe('Fleet - templatePackagePolicyToFullInputStreams', () => { const mockInput: PackagePolicyInput = { @@ -330,6 +332,9 @@ describe('Fleet - getTemplateInputs', () => { if (packageInfo.name === 'log') { return LOGS_2_3_0_ASSETS_MAP; } + if (packageInfo.name === 'docker') { + return DOCKER_2_11_0_ASSETS_MAP; + } return new Map(); }); @@ -350,6 +355,14 @@ describe('Fleet - getTemplateInputs', () => { expect(template).toMatchSnapshot(); }); + it('should work for package with dynamic ids', async () => { + const soMock = savedObjectsClientMock.create(); + soMock.get.mockResolvedValue({ attributes: {} } as any); + const template = await getTemplateInputs(soMock, 'docker', '2.11.0', 'yml'); + + expect(template).toMatchSnapshot(); + }); + it('should work for input package', async () => { const soMock = savedObjectsClientMock.create(); soMock.get.mockResolvedValue({ attributes: {} } as any); From 0b40d99e217b13dcb9c700d363ee82497c2afc10 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:49:21 +1100 Subject: [PATCH 04/47] [8.x] Enable summarisation spec (#199134) (#199244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [Enable summarisation spec (#199134)](https://github.com/elastic/kibana/pull/199134) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Søren Louv-Jansen --- .../server/plugin.ts | 5 ---- .../complete/functions/summarize.spec.ts | 30 +++++++++++++++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index 81f4e24d4d21f..3bdff9eb17606 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -32,7 +32,6 @@ import { ObservabilityAIAssistantPluginSetupDependencies, ObservabilityAIAssistantPluginStartDependencies, } from './types'; -import { addLensDocsToKb } from './service/knowledge_base_service/kb_docs/lens'; import { registerFunctions } from './functions'; import { recallRankingEvent } from './analytics/recall_ranking'; import { initLangtrace } from './service/client/instrumentation/init_langtrace'; @@ -164,10 +163,6 @@ export class ObservabilityAIAssistantPlugin service.register(registerFunctions); - if (this.config.enableKnowledgeBase) { - addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') }); - } - registerServerRoutes({ core, logger: this.logger, diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/summarize.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/summarize.spec.ts index 238be31220aa9..923e8b0206070 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/summarize.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/summarize.spec.ts @@ -14,18 +14,32 @@ import { createProxyActionConnector, deleteActionConnector, } from '../../../common/action_connectors'; +import { + clearKnowledgeBase, + createKnowledgeBaseModel, + deleteKnowledgeBaseModel, +} from '../../knowledge_base/helpers'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const log = getService('log'); + const ml = getService('ml'); + const es = getService('es'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); - // Skipped until Elser is available in tests - describe.skip('when calling summarize function', () => { + describe('when calling summarize function', () => { let proxy: LlmProxy; let connectorId: string; before(async () => { + await clearKnowledgeBase(es); + await createKnowledgeBaseModel(ml); + await observabilityAIAssistantAPIClient + .editorUser({ + endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + }) + .expect(200); + proxy = await createLlmProxy(log); connectorId = await createProxyActionConnector({ supertest, log, port: proxy.getPort() }); @@ -44,7 +58,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { id: 'my-id', text: 'Hello world', is_correction: false, - confidence: 1, + confidence: 'high', public: false, }), }, @@ -55,7 +69,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { proxy.close(); + await deleteActionConnector({ supertest, connectorId, log }); + await deleteKnowledgeBaseModel(ml); }); it('persists entry in knowledge base', async () => { @@ -70,6 +86,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); + const { role, public: isPublic, text, type, user, id } = res.body.entries[0]; + + expect(role).to.eql('assistant_summarization'); + expect(isPublic).to.eql(false); + expect(text).to.eql('Hello world'); + expect(type).to.eql('contextual'); + expect(user?.name).to.eql('editor'); + expect(id).to.eql('my-id'); expect(res.body.entries).to.have.length(1); }); }); From 4e8b6ec576f30570c78c4b51100e4b2576f51985 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:00:26 +1100 Subject: [PATCH 05/47] [8.x] Skip claiming tasks that were modified during the task claiming phase (#198711) (#199251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [Skip claiming tasks that were modified during the task claiming phase (#198711)](https://github.com/elastic/kibana/pull/198711) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Mike Côté --- .../task_claimers/strategy_mget.test.ts | 24 ++++++------ .../server/task_claimers/strategy_mget.ts | 38 ++++++++----------- .../task_manager/server/task_store.test.ts | 17 ++++++--- .../plugins/task_manager/server/task_store.ts | 3 +- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts index e089d3b2d8785..92f2dba988575 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts @@ -1344,15 +1344,15 @@ describe('TaskClaiming', () => { expect(result.docs.length).toEqual(3); }); - test('should assign startedAt value if bulkGet returns task with null startedAt', async () => { + test('should skip tasks where bulkGet returns a newer task document than the bulkPartialUpdate', async () => { const store = taskStoreMock.create({ taskManagerId: 'test-test' }); store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); const fetchedTasks = [ - mockInstance({ id: `id-1`, taskType: 'report' }), - mockInstance({ id: `id-2`, taskType: 'report' }), - mockInstance({ id: `id-3`, taskType: 'yawn' }), - mockInstance({ id: `id-4`, taskType: 'report' }), + mockInstance({ id: `id-1`, taskType: 'report', version: '123' }), + mockInstance({ id: `id-2`, taskType: 'report', version: '123' }), + mockInstance({ id: `id-3`, taskType: 'yawn', version: '123' }), + mockInstance({ id: `id-4`, taskType: 'report', version: '123' }), ]; const { versionMap, docLatestVersions } = getVersionMapsFromTasks(fetchedTasks); @@ -1365,7 +1365,7 @@ describe('TaskClaiming', () => { ); store.bulkGet.mockResolvedValueOnce([ asOk({ ...fetchedTasks[0], startedAt: new Date() }), - asOk(fetchedTasks[1]), + asOk({ ...fetchedTasks[1], startedAt: new Date(), version: 'abc' }), asOk({ ...fetchedTasks[2], startedAt: new Date() }), asOk({ ...fetchedTasks[3], startedAt: new Date() }), ]); @@ -1399,11 +1399,11 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 4; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.warn).toHaveBeenCalledWith( - 'Task id-2 has a null startedAt value, setting to current time - ownerId null, status idle', + 'Task id-2 was modified during the claiming phase, skipping until the next claiming cycle.', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -1463,14 +1463,14 @@ describe('TaskClaiming', () => { expect(store.bulkGet).toHaveBeenCalledWith(['id-1', 'id-2', 'id-3', 'id-4']); expect(result.stats).toEqual({ - tasksClaimed: 4, - tasksConflicted: 0, + tasksClaimed: 3, + tasksConflicted: 1, tasksErrors: 0, - tasksUpdated: 4, + tasksUpdated: 3, tasksLeftUnclaimed: 0, staleTasks: 0, }); - expect(result.docs.length).toEqual(4); + expect(result.docs.length).toEqual(3); for (const r of result.docs) { expect(r.startedAt).not.toBeNull(); } diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts index 4e74454e8c982..cd3efdc783008 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts @@ -195,7 +195,7 @@ async function claimAvailableTasks(opts: TaskClaimerOpts): Promise = {}; let conflicts = 0; let bulkUpdateErrors = 0; let bulkGetErrors = 0; @@ -203,7 +203,7 @@ async function claimAvailableTasks(opts: TaskClaimerOpts): Promise( - (acc, task) => { - if (isOk(task)) { - acc.push(task.value); - } else { - const { id, type, error } = task.error; - logger.error(`Error getting full task ${id}:${type} during claim: ${error.message}`); - bulkGetErrors++; - } - return acc; - }, - [] - ); - - // Look for tasks that have a null startedAt value, log them and manually set a startedAt field - for (const task of fullTasksToRun) { - if (task.startedAt == null) { + const fullTasksToRun = (await taskStore.bulkGet(Object.keys(updatedTasks))).reduce< + ConcreteTaskInstance[] + >((acc, task) => { + if (isOk(task) && task.value.version !== updatedTasks[task.value.id].version) { logger.warn( - `Task ${task.id} has a null startedAt value, setting to current time - ownerId ${task.ownerId}, status ${task.status}` + `Task ${task.value.id} was modified during the claiming phase, skipping until the next claiming cycle.` ); - task.startedAt = now; + conflicts++; + } else if (isOk(task)) { + acc.push(task.value); + } else { + const { id, type, error } = task.error; + logger.error(`Error getting full task ${id}:${type} during claim: ${error.message}`); + bulkGetErrors++; } - } + return acc; + }, []); // separate update for removed tasks; shouldn't happen often, so unlikely // a performance concern, and keeps the rest of the logic simpler diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 2238381552861..cbb1c44dde3fc 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -1020,7 +1020,10 @@ describe('TaskStore', () => { refresh: false, }); - expect(result).toEqual([asOk(task)]); + expect(result).toEqual([ + // New version returned after update + asOk({ ...task, version: 'Wzg0LDFd' }), + ]); }); test(`should perform partial update with minimal fields`, async () => { @@ -1062,7 +1065,8 @@ describe('TaskStore', () => { refresh: false, }); - expect(result).toEqual([asOk(task)]); + // New version returned after update + expect(result).toEqual([asOk({ ...task, version: 'Wzg0LDFd' })]); }); test(`should perform partial update with no version`, async () => { @@ -1100,7 +1104,8 @@ describe('TaskStore', () => { refresh: false, }); - expect(result).toEqual([asOk(task)]); + // New version returned after update + expect(result).toEqual([asOk({ ...task, version: 'Wzg0LDFd' })]); }); test(`should gracefully handle errors within the response`, async () => { @@ -1183,7 +1188,8 @@ describe('TaskStore', () => { }); expect(result).toEqual([ - asOk(task1), + // New version returned after update + asOk({ ...task1, version: 'Wzg0LDFd' }), asErr({ type: 'task', id: '45343254', @@ -1267,7 +1273,8 @@ describe('TaskStore', () => { }); expect(result).toEqual([ - asOk(task1), + // New version returned after update + asOk({ ...task1, version: 'Wzg0LDFd' }), asErr({ type: 'task', id: 'unknown', diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index b7f1cec3f5567..0946c5c18d328 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -26,7 +26,7 @@ import { ElasticsearchClient, } from '@kbn/core/server'; -import { decodeRequestVersion } from '@kbn/core-saved-objects-base-server-internal'; +import { decodeRequestVersion, encodeVersion } from '@kbn/core-saved-objects-base-server-internal'; import { RequestTimeoutsConfig } from './config'; import { asOk, asErr, Result } from './lib/result_type'; @@ -427,6 +427,7 @@ export class TaskStore { return asOk({ ...doc, id: docId, + version: encodeVersion(item.update._seq_no, item.update._primary_term), }); }); } From 0e2e24cc0e5927cb34586172ec8c1743b38555c4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:52:56 +1100 Subject: [PATCH 06/47] [8.x] [Security Solution] Adds UI support for filtering by rule source customization (#197340) (#199217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Adds UI support for filtering by rule source customization (#197340)](https://github.com/elastic/kibana/pull/197340) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --------- Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Co-authored-by: Davis Plumlee --- .../customized_prebuilt_rule_badge.tsx | 8 +- .../components/rule_details/translations.ts | 4 +- .../hooks/use_default_index_pattern.tsx | 6 +- ...s_prebuilt_rules_customization_enabled.tsx | 12 ++ .../rule_management/logic/types.ts | 6 + .../upgrade_prebuilt_rules_table_context.tsx | 14 ++- .../upgrade_prebuilt_rules_table_filters.tsx | 48 ++++++-- ...rade_rule_customization_filter_popover.tsx | 92 +++++++++++++++ .../use_filter_prebuilt_rules_to_upgrade.ts | 28 ++++- .../use_prebuilt_rules_upgrade_state.ts | 6 +- ...e_upgrade_prebuilt_rules_table_columns.tsx | 76 +++++++++++- .../components/rules_table/use_columns.tsx | 54 ++++++++- .../integrations_popover/index.tsx | 7 +- .../detection_engine/rules/translations.ts | 35 ++++++ ...low_with_prebuilt_rule_customization.cy.ts | 110 ++++++++++++++++++ .../cypress/screens/alerts_detection_rules.ts | 2 + .../cypress/screens/rule_updates.ts | 12 ++ .../cypress/tasks/api_calls/rules.ts | 17 +++ .../cypress/tasks/prebuilt_rules.ts | 10 ++ 19 files changed, 503 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_with_prebuilt_rule_customization.cy.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx index 56a559a91794a..e4b00196f4768 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx @@ -10,7 +10,7 @@ import { EuiBadge } from '@elastic/eui'; import * as i18n from './translations'; import { isCustomizedPrebuiltRule } from '../../../../../common/api/detection_engine'; import type { RuleResponse } from '../../../../../common/api/detection_engine'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useIsPrebuiltRulesCustomizationEnabled } from '../../hooks/use_is_prebuilt_rules_customization_enabled'; interface CustomizedPrebuiltRuleBadgeProps { rule: RuleResponse | null; @@ -19,9 +19,7 @@ interface CustomizedPrebuiltRuleBadgeProps { export const CustomizedPrebuiltRuleBadge: React.FC = ({ rule, }) => { - const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled( - 'prebuiltRulesCustomizationEnabled' - ); + const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled(); if (!isPrebuiltRulesCustomizationEnabled) { return null; @@ -31,5 +29,5 @@ export const CustomizedPrebuiltRuleBadge: React.FC{i18n.CUSTOMIZED_PREBUILT_RULE_LABEL}; + return {i18n.MODIFIED_PREBUILT_RULE_LABEL}; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts index 89c22a285e327..e7f36e2011f3c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts @@ -350,10 +350,10 @@ export const MAX_SIGNALS_FIELD_LABEL = i18n.translate( } ); -export const CUSTOMIZED_PREBUILT_RULE_LABEL = i18n.translate( +export const MODIFIED_PREBUILT_RULE_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.customizedPrebuiltRuleLabel', { - defaultMessage: 'Customized Elastic rule', + defaultMessage: 'Modified Elastic rule', } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_default_index_pattern.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_default_index_pattern.tsx index b5ca86c6f1f57..5450cf9950d59 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_default_index_pattern.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_default_index_pattern.tsx @@ -7,7 +7,7 @@ import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { useIsPrebuiltRulesCustomizationEnabled } from './use_is_prebuilt_rules_customization_enabled'; /** * Gets the default index pattern for cases when rule has neither index patterns or data view. @@ -15,9 +15,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_exper */ export function useDefaultIndexPattern(): string[] { const { services } = useKibana(); - const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled( - 'prebuiltRulesCustomizationEnabled' - ); + const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled(); return isPrebuiltRulesCustomizationEnabled ? services.settings.client.get(DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled.tsx new file mode 100644 index 0000000000000..d25925860c175 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; + +export const useIsPrebuiltRulesCustomizationEnabled = () => { + return useIsExperimentalFeatureEnabled('prebuiltRulesCustomizationEnabled'); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index e12442c97aa4c..59ac52d592bcd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -99,6 +99,7 @@ export interface FilterOptions { excludeRuleTypes?: Type[]; enabled?: boolean; // undefined is to display all the rules ruleExecutionStatus?: RuleExecutionStatus; // undefined means "all" + ruleSource?: RuleCustomizationEnum[]; // undefined is to display all the rules } export interface FetchRulesResponse { @@ -202,3 +203,8 @@ export interface FindRulesReferencedByExceptionsProps { lists: FindRulesReferencedByExceptionsListProp[]; signal?: AbortSignal; } + +export enum RuleCustomizationEnum { + customized = 'CUSTOMIZED', + not_customized = 'NOT_CUSTOMIZED', +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 39e9d3db2f2c1..6ec9ffdd02e67 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -8,8 +8,8 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; +import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled'; import type { RulesUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { RuleUpgradeConflictsResolverTab } from '../../../../rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab'; import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; @@ -75,11 +75,14 @@ export interface UpgradePrebuiltRulesTableState { * List of rule IDs that are currently being upgraded */ loadingRules: RuleSignatureId[]; - /** /** * The timestamp for when the rules were successfully fetched */ lastUpdated: number; + /** + * Feature Flag to enable prebuilt rules customization + */ + isPrebuiltRulesCustomizationEnabled: boolean; } export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; @@ -108,13 +111,12 @@ interface UpgradePrebuiltRulesTableContextProviderProps { export const UpgradePrebuiltRulesTableContextProvider = ({ children, }: UpgradePrebuiltRulesTableContextProviderProps) => { - const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled( - 'prebuiltRulesCustomizationEnabled' - ); + const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled(); const [loadingRules, setLoadingRules] = useState([]); const [filterOptions, setFilterOptions] = useState({ filter: '', tags: [], + ruleSource: [], }); const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); @@ -318,6 +320,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ isUpgradingSecurityPackages, loadingRules, lastUpdated: dataUpdatedAt, + isPrebuiltRulesCustomizationEnabled, }, actions, }; @@ -334,6 +337,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ loadingRules, dataUpdatedAt, actions, + isPrebuiltRulesCustomizationEnabled, ]); return ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx index 215a810bf3aa2..900d81d0b0037 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx @@ -9,10 +9,13 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; import React, { useCallback } from 'react'; import styled from 'styled-components'; +import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled'; +import type { RuleCustomizationEnum } from '../../../../rule_management/logic'; import * as i18n from './translations'; import { TagsFilterPopover } from '../rules_table_filters/tags_filter_popover'; import { RuleSearchField } from '../rules_table_filters/rule_search_field'; import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; +import { RuleCustomizationFilterPopover } from './upgrade_rule_customization_filter_popover'; const FilterWrapper = styled(EuiFlexGroup)` margin-bottom: ${({ theme }) => theme.eui.euiSizeM}; @@ -28,7 +31,9 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { actions: { setFilterOptions }, } = useUpgradePrebuiltRulesTableContext(); - const { tags: selectedTags } = filterOptions; + const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled(); + + const { tags: selectedTags, ruleSource: selectedRuleSource = [] } = filterOptions; const handleOnSearch = useCallback( (filterString: string) => { @@ -52,22 +57,45 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { [selectedTags, setFilterOptions] ); + const handleSelectedRuleSource = useCallback( + (newRuleSource: RuleCustomizationEnum[]) => { + if (!isEqual(newRuleSource, selectedRuleSource)) { + setFilterOptions((filters) => ({ + ...filters, + ruleSource: newRuleSource, + })); + } + }, + [selectedRuleSource, setFilterOptions] + ); + return ( - + - - - + + {isPrebuiltRulesCustomizationEnabled && ( + + + + )} + + + + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx new file mode 100644 index 0000000000000..234943e333272 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useMemo } from 'react'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { EuiFilterButton, EuiPopover, EuiSelectable } from '@elastic/eui'; +import { RuleCustomizationEnum } from '../../../../rule_management/logic'; +import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; +import { toggleSelectedGroup } from '../../../../../common/components/ml_popover/jobs_table/filters/toggle_selected_group'; + +interface RuleCustomizationFilterPopoverProps { + selectedRuleSource: RuleCustomizationEnum[]; + onSelectedRuleSourceChanged: (newRuleSource: RuleCustomizationEnum[]) => void; +} + +const RULE_CUSTOMIZATION_POPOVER_WIDTH = 200; + +const RuleCustomizationFilterPopoverComponent = ({ + selectedRuleSource, + onSelectedRuleSourceChanged, +}: RuleCustomizationFilterPopoverProps) => { + const [isRuleCustomizationPopoverOpen, setIsRuleCustomizationPopoverOpen] = useState(false); + + const selectableOptions: EuiSelectableOption[] = useMemo( + () => [ + { + label: i18n.MODIFIED_LABEL, + key: RuleCustomizationEnum.customized, + checked: selectedRuleSource.includes(RuleCustomizationEnum.customized) ? 'on' : undefined, + }, + { + label: i18n.UNMODIFIED_LABEL, + key: RuleCustomizationEnum.not_customized, + checked: selectedRuleSource.includes(RuleCustomizationEnum.not_customized) + ? 'on' + : undefined, + }, + ], + [selectedRuleSource] + ); + + const handleSelectableOptionsChange = ( + newOptions: EuiSelectableOption[], + _: unknown, + changedOption: EuiSelectableOption + ) => { + toggleSelectedGroup( + changedOption.key ?? '', + selectedRuleSource, + onSelectedRuleSourceChanged as (args: string[]) => void + ); + }; + + const triggerButton = ( + setIsRuleCustomizationPopoverOpen(!isRuleCustomizationPopoverOpen)} + numFilters={selectableOptions.length} + isSelected={isRuleCustomizationPopoverOpen} + hasActiveFilters={selectedRuleSource.length > 0} + numActiveFilters={selectedRuleSource.length} + data-test-subj="rule-customization-filter-popover-button" + > + {i18n.RULE_SOURCE} + + ); + + return ( + setIsRuleCustomizationPopoverOpen(!isRuleCustomizationPopoverOpen)} + panelPaddingSize="none" + repositionOnScroll + panelProps={{ + 'data-test-subj': 'rule-customization-filter-popover', + }} + > + + {(list) =>
{list}
} +
+
+ ); +}; + +export const RuleCustomizationFilterPopover = React.memo(RuleCustomizationFilterPopoverComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts index 342a1e6e8768e..b5a0e123d7510 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts @@ -7,9 +7,12 @@ import { useMemo } from 'react'; import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { FilterOptions } from '../../../../rule_management/logic/types'; +import { RuleCustomizationEnum, type FilterOptions } from '../../../../rule_management/logic/types'; -export type UpgradePrebuiltRulesTableFilterOptions = Pick; +export type UpgradePrebuiltRulesTableFilterOptions = Pick< + FilterOptions, + 'filter' | 'tags' | 'ruleSource' +>; export const useFilterPrebuiltRulesToUpgrade = ({ rules, @@ -19,7 +22,7 @@ export const useFilterPrebuiltRulesToUpgrade = ({ filterOptions: UpgradePrebuiltRulesTableFilterOptions; }) => { const filteredRules = useMemo(() => { - const { filter, tags } = filterOptions; + const { filter, tags, ruleSource } = filterOptions; return rules.filter((ruleInfo) => { if (filter && !ruleInfo.current_rule.name.toLowerCase().includes(filter.toLowerCase())) { return false; @@ -29,6 +32,25 @@ export const useFilterPrebuiltRulesToUpgrade = ({ return tags.every((tag) => ruleInfo.current_rule.tags.includes(tag)); } + if (ruleSource && ruleSource.length > 0) { + if ( + ruleSource.includes(RuleCustomizationEnum.customized) && + ruleSource.includes(RuleCustomizationEnum.not_customized) + ) { + return true; + } else if ( + ruleSource.includes(RuleCustomizationEnum.customized) && + ruleInfo.current_rule.rule_source.type === 'external' + ) { + return ruleInfo.current_rule.rule_source.is_customized; + } else if ( + ruleSource.includes(RuleCustomizationEnum.not_customized) && + ruleInfo.current_rule.rule_source.type === 'external' + ) { + return ruleInfo.current_rule.rule_source.is_customized === false; + } + } + return true; }); }, [filterOptions, rules]); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts index 29c5b2b201fe6..8c97a4ef52e2b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts @@ -6,7 +6,7 @@ */ import { useCallback, useMemo, useState } from 'react'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled'; import type { RulesUpgradeState, FieldsUpgradeState, @@ -33,9 +33,7 @@ interface UseRulesUpgradeStateResult { export function usePrebuiltRulesUpgradeState( ruleUpgradeInfos: RuleUpgradeInfoForReview[] ): UseRulesUpgradeStateResult { - const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled( - 'prebuiltRulesCustomizationEnabled' - ); + const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled(); const [rulesResolvedConflicts, setRulesResolvedConflicts] = useState({}); const setRuleFieldResolvedValue = useCallback( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx index cf93925375082..dbfba0b927041 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx @@ -6,7 +6,14 @@ */ import type { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiBadge, EuiButtonEmpty, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import { + EuiBadge, + EuiButtonEmpty, + EuiLink, + EuiLoadingSpinner, + EuiText, + EuiToolTip, +} from '@elastic/eui'; import React, { useMemo } from 'react'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state'; import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name'; @@ -104,16 +111,48 @@ const INTEGRATIONS_COLUMN: TableColumn = { truncateText: true, }; +const MODIFIED_COLUMN: TableColumn = { + field: 'current_rule.rule_source', + name: , + align: 'center', + render: (ruleSource: Rule['rule_source']) => { + if ( + ruleSource == null || + ruleSource.type === 'internal' || + (ruleSource.type === 'external' && ruleSource.is_customized === false) + ) { + return null; + } + + return ( + + + {i18n.MODIFIED_LABEL} + + + ); + }, + width: '90px', + truncateText: true, +}; + const createUpgradeButtonColumn = ( upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'], loadingRules: RuleSignatureId[], - isDisabled: boolean + isDisabled: boolean, + isPrebuiltRulesCustomizationEnabled: boolean ): TableColumn => ({ field: 'rule_id', name: , render: (ruleId: RuleSignatureId, record) => { const isRuleUpgrading = loadingRules.includes(ruleId); - const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled || record.hasUnresolvedConflicts; + const isDisabledByConflicts = + isPrebuiltRulesCustomizationEnabled && record.hasUnresolvedConflicts; + const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled || isDisabledByConflicts; const spinner = ( { const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD); const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING); const { - state: { loadingRules, isRefetching, isUpgradingSecurityPackages }, + state: { + loadingRules, + isRefetching, + isUpgradingSecurityPackages, + isPrebuiltRulesCustomizationEnabled, + }, actions: { upgradeRules }, } = useUpgradePrebuiltRulesTableContext(); const isDisabled = isRefetching || isUpgradingSecurityPackages; + // TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed + if (isPrebuiltRulesCustomizationEnabled) { + INTEGRATIONS_COLUMN.width = '70px'; + } + return useMemo( () => [ RULE_NAME_COLUMN, + ...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []), ...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []), TAGS_COLUMN, { @@ -173,9 +223,23 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { width: '12%', }, ...(hasCRUDPermissions - ? [createUpgradeButtonColumn(upgradeRules, loadingRules, isDisabled)] + ? [ + createUpgradeButtonColumn( + upgradeRules, + loadingRules, + isDisabled, + isPrebuiltRulesCustomizationEnabled + ), + ] : []), ], - [hasCRUDPermissions, loadingRules, isDisabled, showRelatedIntegrations, upgradeRules] + [ + isPrebuiltRulesCustomizationEnabled, + showRelatedIntegrations, + hasCRUDPermissions, + upgradeRules, + loadingRules, + isDisabled, + ] ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx index c38fec638e478..ae24b2bb482d3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx @@ -46,6 +46,7 @@ import { useRulesTableActions } from './use_rules_table_actions'; import { MlRuleWarningPopover } from '../ml_rule_warning_popover/ml_rule_warning_popover'; import { getMachineLearningJobId } from '../../../../detections/pages/detection_engine/rules/helpers'; import type { TimeRange } from '../../../rule_gaps/types'; +import { useIsPrebuiltRulesCustomizationEnabled } from '../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled'; export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; @@ -233,6 +234,35 @@ const INTEGRATIONS_COLUMN: TableColumn = { truncateText: true, }; +const MODIFIED_COLUMN: TableColumn = { + field: 'rule_source', + name: , + align: 'center', + render: (ruleSource: Rule['rule_source']) => { + if ( + ruleSource == null || + ruleSource.type === 'internal' || + (ruleSource.type === 'external' && ruleSource.is_customized === false) + ) { + return null; + } + + return ( + + + {i18n.MODIFIED_LABEL} + + + ); + }, + width: '90px', + truncateText: true, +}; + const useActionsColumn = ({ showExceptionsDuplicateConfirmation, showManualRuleRunConfirmation, @@ -265,6 +295,7 @@ export const useRulesColumns = ({ }); const ruleNameColumn = useRuleNameColumn(); const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING); + const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled(); const enabledColumn = useEnabledColumn({ hasCRUDPermissions, isLoadingJobs, @@ -279,9 +310,15 @@ export const useRulesColumns = ({ }); const snoozeColumn = useRuleSnoozeColumn(); + // TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed + if (isPrebuiltRulesCustomizationEnabled) { + INTEGRATIONS_COLUMN.width = '70px'; + } + return useMemo( () => [ ruleNameColumn, + ...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []), ...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []), TAGS_COLUMN, { @@ -352,13 +389,14 @@ export const useRulesColumns = ({ ...(hasCRUDPermissions ? [actionsColumn] : []), ], [ - actionsColumn, - enabledColumn, + ruleNameColumn, + isPrebuiltRulesCustomizationEnabled, + showRelatedIntegrations, executionStatusColumn, snoozeColumn, + enabledColumn, hasCRUDPermissions, - ruleNameColumn, - showRelatedIntegrations, + actionsColumn, ] ); }; @@ -380,6 +418,7 @@ export const useMonitoringColumns = ({ }); const ruleNameColumn = useRuleNameColumn(); const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING); + const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled(); const enabledColumn = useEnabledColumn({ hasCRUDPermissions, isLoadingJobs, @@ -393,12 +432,18 @@ export const useMonitoringColumns = ({ mlJobs, }); + // TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed + if (isPrebuiltRulesCustomizationEnabled) { + INTEGRATIONS_COLUMN.width = '70px'; + } + return useMemo( () => [ { ...ruleNameColumn, width: '28%', }, + ...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []), ...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []), TAGS_COLUMN, { @@ -503,6 +548,7 @@ export const useMonitoringColumns = ({ enabledColumn, executionStatusColumn, hasCRUDPermissions, + isPrebuiltRulesCustomizationEnabled, ruleNameColumn, showRelatedIntegrations, ] diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_popover/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_popover/index.tsx index 2d2875d5a8734..49b6c1d1e4e99 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_popover/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_popover/index.tsx @@ -16,6 +16,7 @@ import { EuiSpacer, } from '@elastic/eui'; +import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../../detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled'; import type { RelatedIntegrationArray } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { IntegrationDescription } from '../integrations_description'; import { useRelatedIntegrations } from '../use_related_integrations'; @@ -54,6 +55,7 @@ const IntegrationListItem = styled('li')` const IntegrationsPopoverComponent = ({ relatedIntegrations }: IntegrationsPopoverProps) => { const [isPopoverOpen, setPopoverOpen] = useState(false); const { integrations, isLoaded } = useRelatedIntegrations(relatedIntegrations); + const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled(); const enabledIntegrations = useMemo(() => { return integrations.filter( @@ -65,10 +67,13 @@ const IntegrationsPopoverComponent = ({ relatedIntegrations }: IntegrationsPopov const numIntegrationsEnabled = enabledIntegrations.length; const badgeTitle = useMemo(() => { + if (isPrebuiltRulesCustomizationEnabled) { + return isLoaded ? `${numIntegrationsEnabled}/${numIntegrations}` : `${numIntegrations}`; + } return isLoaded ? `${numIntegrationsEnabled}/${numIntegrations} ${i18n.INTEGRATIONS_BADGE}` : `${numIntegrations} ${i18n.INTEGRATIONS_BADGE}`; - }, [isLoaded, numIntegrations, numIntegrationsEnabled]); + }, [isLoaded, isPrebuiltRulesCustomizationEnabled, numIntegrations, numIntegrationsEnabled]); return ( { + describe('Upgrade of prebuilt rules with conflicts', () => { + const RULE_1_ID = 'rule_1'; + const RULE_2_ID = 'rule_2'; + const OUTDATED_RULE_1 = createRuleAssetSavedObject({ + name: 'Outdated rule 1', + rule_id: RULE_1_ID, + version: 1, + }); + const UPDATED_RULE_1 = createRuleAssetSavedObject({ + name: 'Updated rule 1', + rule_id: RULE_1_ID, + version: 2, + }); + const OUTDATED_RULE_2 = createRuleAssetSavedObject({ + name: 'Outdated rule 2', + rule_id: RULE_2_ID, + version: 1, + }); + const UPDATED_RULE_2 = createRuleAssetSavedObject({ + name: 'Updated rule 2', + rule_id: RULE_2_ID, + version: 2, + }); + const patchedName = 'A new name that creates a conflict'; + beforeEach(() => { + login(); + resetRulesTableState(); + deleteAlertsAndRules(); + cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/upgrade/_perform').as( + 'updatePrebuiltRules' + ); + /* Create a new rule and install it */ + createAndInstallMockedPrebuiltRules([OUTDATED_RULE_1, OUTDATED_RULE_2]); + /* Modify one of the rule's name to cause a conflict */ + patchRule(OUTDATED_RULE_1['security-rule'].rule_id, { + name: patchedName, + }); + /* Create a second version of the rule, making it available for update */ + installPrebuiltRuleAssets([UPDATED_RULE_1, UPDATED_RULE_2]); + + visitRulesManagementTable(); + clickRuleUpdatesTab(); + }); + + it('should filter by customized prebuilt rules', () => { + // Filter table to show modified rules only + filterPrebuiltRulesUpdateTableByRuleCustomization('Modified'); + cy.get(MODIFIED_RULE_BADGE).should('exist'); + + // Verify only rules with customized rule sources are displayed + cy.get(RULES_UPDATES_TABLE).contains(patchedName); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_2]); + }); + + it('should filter by customized prebuilt rules', () => { + // Filter table to show unmodified rules only + filterPrebuiltRulesUpdateTableByRuleCustomization('Unmodified'); + cy.get(MODIFIED_RULE_BADGE).should('not.exist'); + + // Verify only rules with non-customized rule sources are displayed + assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_2]); + cy.get(patchedName).should('not.exist'); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts index bbc7a346b252f..e2ce41dee2847 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts @@ -349,3 +349,5 @@ export const ESQL_QUERY_VALUE = '[data-test-subj="esqlQueryPropertyValue"]'; export const PER_FIELD_DIFF_WRAPPER = '[data-test-subj="ruleUpgradePerFieldDiffWrapper"]'; export const PER_FIELD_DIFF_DEFINITION_SECTION = '[data-test-subj="perFieldDiffDefinitionSection"]'; + +export const MODIFIED_RULE_BADGE = '[data-test-subj="upgradeRulesTableModifiedColumnBadge"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts new file mode 100644 index 0000000000000..4b11a4624c3e2 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.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 const RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON = + '[data-test-subj="rule-customization-filter-popover-button"]'; + +export const RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL = + '[data-test-subj="rule-customization-filter-popover"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts index 008fc92bd0224..c0ef625f52e35 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts @@ -39,6 +39,23 @@ export const createRule = ( ); }; +export const patchRule = ( + ruleId: string, + updateData: Partial +): Cypress.Chainable> => { + return cy.currentSpace().then((spaceId) => + rootRequest({ + method: 'PATCH', + url: spaceId ? getSpaceUrl(spaceId, DETECTION_ENGINE_RULES_URL) : DETECTION_ENGINE_RULES_URL, + body: { + rule_id: ruleId, + ...updateData, + }, + failOnStatusCode: false, + }) + ); +}; + /** * Snoozes a rule via API * diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts index b4f68dba976c3..d4148d5e632a1 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts @@ -17,6 +17,10 @@ import { TOASTER, } from '../screens/alerts_detection_rules'; import type { SAMPLE_PREBUILT_RULE } from './api_calls/prebuilt_rules'; +import { + RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON, + RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL, +} from '../screens/rule_updates'; export const clickAddElasticRulesButton = () => { cy.get(ADD_ELASTIC_RULES_BTN).click(); @@ -150,3 +154,9 @@ export const assertRulesNotPresentInRuleUpdatesTable = ( cy.get(rule['security-rule'].name).should('not.exist'); } }; + +export const filterPrebuiltRulesUpdateTableByRuleCustomization = (text: string) => { + cy.get(RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON).click(); + cy.get(RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL).contains(text).click(); + cy.get(RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON).click(); +}; From 65d84cf27678af1c1edd693a6c6a77bafc7cb0b2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:50:35 +1100 Subject: [PATCH 07/47] [8.x] [ResponseOps][Rules] Allow users to create rules with predefined non random IDs (#199119) (#199259) # Backport This will backport the following commits from `main` to `8.x`: - [[ResponseOps][Rules] Allow users to create rules with predefined non random IDs (#199119)](https://github.com/elastic/kibana/pull/199119) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Christos Nasikas --- .../alerting/server/saved_objects/index.ts | 5 +++++ .../tests/alerting/group1/create.ts | 20 +++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index 8e76f28ff7fb8..8f11020ee6285 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -217,6 +217,11 @@ export function setupSavedObjects( // Encrypted attributes encryptedSavedObjects.registerType({ type: RULE_SAVED_OBJECT_TYPE, + /** + * We disable enforcing random SO IDs for the rule SO + * to allow users creating rules with a predefined ID. + */ + enforceRandomId: false, attributesToEncrypt: new Set(RuleAttributesToEncrypt), attributesToIncludeInAAD: new Set(RuleAttributesIncludedInAAD), }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts index 57d41424186b3..5a6385a3895d2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts @@ -395,20 +395,18 @@ export default function createAlertTests({ getService }: FtrProviderContext) { }); }); - it('should not allow providing simple custom ids (non uuid)', async () => { - const customId = '1'; + it('should create a rule with a predefined non random ID', async () => { + const ruleId = 'my_id'; + const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${ruleId}`) .set('kbn-xsrf', 'foo') - .send(getTestRuleData()); + .send(getTestRuleData()) + .expect(200); - expect(response.status).to.eql(400); - expect(response.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'Predefined IDs are not allowed for saved objects with encrypted attributes unless the ID is a UUID.: Bad Request', - }); + objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); + + expect(response.body.id).to.eql(ruleId); }); it('should return 409 when document with id already exists', async () => { From feab4ef51b8f2ce7daf46ea5064427dbbf59fa23 Mon Sep 17 00:00:00 2001 From: Ash <1849116+ashokaditya@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:16:34 +0100 Subject: [PATCH 08/47] [8.x] [Security Solution] Removing cypress folder (#197273) (#199260) > [!Warning] > `.github/CODEOWNERS` and `.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml` were updated as part of merge conflicts so would need a thorough review. # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Removing cypress folder (#197273)](https://github.com/elastic/kibana/pull/197273) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --------- Co-authored-by: Gloria Hornero Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../ftr_security_serverless_configs.yml | 6 +- .../security_solution/defend_workflows.yml | 17 + .../security_serverless_defend_workflows.sh | 12 - .github/CODEOWNERS | 2022 +++++++++++++++++ .../osquery/cypress/cypress_base.config.ts | 2 +- x-pack/plugins/osquery/cypress/support/e2e.ts | 11 + .../support/setup_data_loader_tasks.ts | 17 +- .../serverless_config.base.ts} | 2 +- .../serverless_config.ts | 4 +- .../osquery_cypress/serverless_cli_config.ts | 4 +- .../osquery_cypress/serverless_config.base.ts | 35 + .../security/cypress/.eslintrc.json | 13 - .../test_suites/security/cypress/.gitignore | 3 - .../test_suites/security/cypress/README.md | 65 - .../security/cypress/cypress.config.ts | 40 - .../test_suites/security/cypress/cypress.d.ts | 207 -- .../security/cypress/e2e/serverless.cy.ts | 22 - .../test_suites/security/cypress/package.json | 13 - .../security/cypress/reporter_config.json | 10 - .../test_suites/security/cypress/runner.ts | 24 - .../security/cypress/screens/index.ts | 8 - .../security/cypress/screens/landing_page.ts | 8 - .../security/cypress/security_config.ts | 31 - .../security/cypress/support/commands.js | 32 - .../security/cypress/support/e2e.js | 29 - .../security/cypress/support/index.d.ts | 52 - .../index_endpoint_hosts.ts | 35 - .../security/cypress/tasks/login.ts | 87 - .../security/cypress/tasks/navigation.ts | 10 - x-pack/test_serverless/tsconfig.json | 1 - 30 files changed, 2099 insertions(+), 723 deletions(-) delete mode 100644 .buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh create mode 100644 .github/CODEOWNERS rename x-pack/{test_serverless/functional/test_suites/security => plugins/osquery}/cypress/support/setup_data_loader_tasks.ts (77%) rename x-pack/{test_serverless/functional/test_suites/security/cypress/security_config.base.ts => test/defend_workflows_cypress/serverless_config.base.ts} (93%) create mode 100644 x-pack/test/osquery_cypress/serverless_config.base.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/.eslintrc.json delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/.gitignore delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/README.md delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/cypress.d.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/e2e/serverless.cy.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/package.json delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/reporter_config.json delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/runner.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/screens/index.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/screens/landing_page.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/support/commands.js delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/support/e2e.js delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/support/index.d.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/tasks/endpoint_management/index_endpoint_hosts.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts delete mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/tasks/navigation.ts diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index b642b2c680bb1..834db3ce9849e 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -1,17 +1,17 @@ disabled: # Base config files, only necessary to inform config finding script - - x-pack/test_serverless/functional/test_suites/security/cypress/security_config.base.ts - - x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts - x-pack/test/security_solution_api_integration/config/serverless/config.base.ts - x-pack/test/security_solution_api_integration/config/serverless/config.base.essentials.ts - x-pack/test/security_solution_api_integration/config/serverless/config.base.edr_workflows.ts + - x-pack/test/defend_workflows_cypress/serverless_config.base.ts + - x-pack/test/osquery_cypress/serverless_config.base.ts # Cypress configs, for now these are still run manually - x-pack/test/defend_workflows_cypress/serverless_config.ts - x-pack/test/osquery_cypress/serverless_cli_config.ts - - x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts - x-pack/test/security_solution_cypress/serverless_config.ts + # Playwright - x-pack/test/security_solution_playwright/serverless_config.ts diff --git a/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml b/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml index 28cc4f2812b5a..104853c27b112 100644 --- a/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml +++ b/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml @@ -18,3 +18,20 @@ steps: automatic: - exit_status: '-1' limit: 1 + + - command: .buildkite/scripts/steps/functional/defend_workflows_serverless.sh + label: 'Defend Workflows Cypress Tests on Serverless' + agents: + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + depends_on: + - build + - quick_checks + timeout_in_minutes: 60 + parallelism: 14 + retry: + automatic: + - exit_status: '-1' + limit: 1 diff --git a/.buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh b/.buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh deleted file mode 100644 index 7b16afa214fed..0000000000000 --- a/.buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -export JOB=kibana-serverless-security-cypress -export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} - -echo "--- Security Defend Workflows Serverless Cypress" - -yarn --cwd x-pack/test_serverless/functional/test_suites/security/cypress cypress:run diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000000..dda83bdf40615 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2022 @@ +#### +## Everything at the top of the codeowners file is auto generated based on the +## "owner" fields in the kibana.jsonc files at the root of each package. This +## file is automatically updated by CI or can be updated locally by running +## `node scripts/generate codeowners`. +#### + +x-pack/test/alerting_api_integration/common/plugins/aad @elastic/response-ops +x-pack/plugins/actions @elastic/response-ops +x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops +packages/kbn-actions-types @elastic/response-ops +src/plugins/advanced_settings @elastic/appex-sharedux @elastic/kibana-management +x-pack/packages/kbn-ai-assistant @elastic/search-kibana +x-pack/packages/kbn-ai-assistant-common @elastic/search-kibana +src/plugins/ai_assistant_management/selection @elastic/obs-knowledge-team +x-pack/packages/ml/aiops_change_point_detection @elastic/ml-ui +x-pack/packages/ml/aiops_common @elastic/ml-ui +x-pack/packages/ml/aiops_components @elastic/ml-ui +x-pack/packages/ml/aiops_log_pattern_analysis @elastic/ml-ui +x-pack/packages/ml/aiops_log_rate_analysis @elastic/ml-ui +x-pack/plugins/aiops @elastic/ml-ui +x-pack/packages/ml/aiops_test_utils @elastic/ml-ui +x-pack/test/alerting_api_integration/packages/helpers @elastic/response-ops +x-pack/test/alerting_api_integration/common/plugins/alerts @elastic/response-ops +x-pack/packages/kbn-alerting-comparators @elastic/response-ops +x-pack/examples/alerting_example @elastic/response-ops +x-pack/test/functional_with_es_ssl/plugins/alerts @elastic/response-ops +x-pack/plugins/alerting @elastic/response-ops +x-pack/packages/kbn-alerting-state-types @elastic/response-ops +packages/kbn-alerting-types @elastic/response-ops +packages/kbn-alerts-as-data-utils @elastic/response-ops +packages/kbn-alerts-grouping @elastic/response-ops +x-pack/test/alerting_api_integration/common/plugins/alerts_restricted @elastic/response-ops +packages/kbn-alerts-ui-shared @elastic/response-ops +packages/kbn-ambient-common-types @elastic/kibana-operations +packages/kbn-ambient-ftr-types @elastic/kibana-operations @elastic/appex-qa +packages/kbn-ambient-storybook-types @elastic/kibana-operations +packages/kbn-ambient-ui-types @elastic/kibana-operations +packages/kbn-analytics @elastic/kibana-core +packages/analytics/utils/analytics_collection_utils @elastic/kibana-core +test/analytics/plugins/analytics_ftr_helpers @elastic/kibana-core +test/analytics/plugins/analytics_plugin_a @elastic/kibana-core +packages/kbn-apm-config-loader @elastic/kibana-core @vigneshshanmugam +x-pack/plugins/observability_solution/apm_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team +packages/kbn-apm-data-view @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/apm/ftr_e2e @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/apm @elastic/obs-ux-infra_services-team +packages/kbn-apm-synthtrace @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team +packages/kbn-apm-synthtrace-client @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team +packages/kbn-apm-types @elastic/obs-ux-infra_services-team +packages/kbn-apm-utils @elastic/obs-ux-infra_services-team +test/plugin_functional/plugins/app_link_test @elastic/kibana-core +x-pack/test/usage_collection/plugins/application_usage_test @elastic/kibana-core +x-pack/test/security_api_integration/plugins/audit_log @elastic/kibana-security +packages/kbn-avc-banner @elastic/security-defend-workflows +packages/kbn-axe-config @elastic/kibana-qa +packages/kbn-babel-preset @elastic/kibana-operations +packages/kbn-babel-register @elastic/kibana-operations +packages/kbn-babel-transform @elastic/kibana-operations +x-pack/plugins/banners @elastic/appex-sharedux +packages/kbn-bazel-runner @elastic/kibana-operations +packages/kbn-bfetch-error @elastic/appex-sharedux +examples/bfetch_explorer @elastic/appex-sharedux +src/plugins/bfetch @elastic/appex-sharedux +packages/kbn-calculate-auto @elastic/obs-ux-management-team +packages/kbn-calculate-width-from-char-count @elastic/kibana-visualizations +x-pack/plugins/canvas @elastic/kibana-presentation +packages/kbn-capture-oas-snapshot-cli @elastic/kibana-core +x-pack/test/cases_api_integration/common/plugins/cases @elastic/response-ops +packages/kbn-cases-components @elastic/response-ops +x-pack/plugins/cases @elastic/response-ops +packages/kbn-cbor @elastic/kibana-operations +packages/kbn-cell-actions @elastic/security-threat-hunting-explore +src/plugins/chart_expressions/common @elastic/kibana-visualizations +packages/kbn-chart-icons @elastic/kibana-visualizations +src/plugins/charts @elastic/kibana-visualizations +packages/kbn-check-mappings-update-cli @elastic/kibana-core +packages/kbn-check-prod-native-modules-cli @elastic/kibana-operations +packages/kbn-ci-stats-core @elastic/kibana-operations +packages/kbn-ci-stats-performance-metrics @elastic/kibana-operations +packages/kbn-ci-stats-reporter @elastic/kibana-operations +packages/kbn-ci-stats-shipper-cli @elastic/kibana-operations +packages/kbn-cli-dev-mode @elastic/kibana-operations +packages/cloud @elastic/kibana-core +x-pack/plugins/cloud_integrations/cloud_chat @elastic/kibana-core +x-pack/plugins/cloud_integrations/cloud_data_migration @elastic/kibana-management +x-pack/plugins/cloud_defend @elastic/kibana-cloud-security-posture +x-pack/plugins/cloud_integrations/cloud_experiments @elastic/kibana-core +x-pack/plugins/cloud_integrations/cloud_full_story @elastic/kibana-core +x-pack/test/cloud_integration/plugins/saml_provider @elastic/kibana-core +x-pack/plugins/cloud_integrations/cloud_links @elastic/kibana-core +x-pack/plugins/cloud @elastic/kibana-core +x-pack/packages/kbn-cloud-security-posture/public @elastic/kibana-cloud-security-posture +x-pack/packages/kbn-cloud-security-posture/common @elastic/kibana-cloud-security-posture +x-pack/packages/kbn-cloud-security-posture/graph @elastic/kibana-cloud-security-posture +x-pack/plugins/cloud_security_posture @elastic/kibana-cloud-security-posture +packages/shared-ux/code_editor/impl @elastic/appex-sharedux +packages/shared-ux/code_editor/mocks @elastic/appex-sharedux +packages/kbn-code-owners @elastic/appex-qa +packages/kbn-coloring @elastic/kibana-visualizations +packages/kbn-config @elastic/kibana-core +packages/kbn-config-mocks @elastic/kibana-core +packages/kbn-config-schema @elastic/kibana-core +src/plugins/console @elastic/kibana-management +packages/content-management/content_editor @elastic/appex-sharedux +packages/content-management/content_insights/content_insights_public @elastic/appex-sharedux +packages/content-management/content_insights/content_insights_server @elastic/appex-sharedux +examples/content_management_examples @elastic/appex-sharedux +packages/content-management/favorites/favorites_public @elastic/appex-sharedux +packages/content-management/favorites/favorites_server @elastic/appex-sharedux +src/plugins/content_management @elastic/appex-sharedux +packages/content-management/tabbed_table_list_view @elastic/appex-sharedux +packages/content-management/table_list_view @elastic/appex-sharedux +packages/content-management/table_list_view_common @elastic/appex-sharedux +packages/content-management/table_list_view_table @elastic/appex-sharedux +packages/content-management/user_profiles @elastic/appex-sharedux +packages/kbn-content-management-utils @elastic/kibana-data-discovery +examples/controls_example @elastic/kibana-presentation +src/plugins/controls @elastic/kibana-presentation +src/core @elastic/kibana-core +packages/core/analytics/core-analytics-browser @elastic/kibana-core +packages/core/analytics/core-analytics-browser-internal @elastic/kibana-core +packages/core/analytics/core-analytics-browser-mocks @elastic/kibana-core +packages/core/analytics/core-analytics-server @elastic/kibana-core +packages/core/analytics/core-analytics-server-internal @elastic/kibana-core +packages/core/analytics/core-analytics-server-mocks @elastic/kibana-core +test/plugin_functional/plugins/core_app_status @elastic/kibana-core +packages/core/application/core-application-browser @elastic/kibana-core +packages/core/application/core-application-browser-internal @elastic/kibana-core +packages/core/application/core-application-browser-mocks @elastic/kibana-core +packages/core/application/core-application-common @elastic/kibana-core +packages/core/apps/core-apps-browser-internal @elastic/kibana-core +packages/core/apps/core-apps-browser-mocks @elastic/kibana-core +packages/core/apps/core-apps-server-internal @elastic/kibana-core +packages/core/base/core-base-browser-internal @elastic/kibana-core +packages/core/base/core-base-browser-mocks @elastic/kibana-core +packages/core/base/core-base-common @elastic/kibana-core +packages/core/base/core-base-common-internal @elastic/kibana-core +packages/core/base/core-base-server-internal @elastic/kibana-core +packages/core/base/core-base-server-mocks @elastic/kibana-core +packages/core/capabilities/core-capabilities-browser-internal @elastic/kibana-core +packages/core/capabilities/core-capabilities-browser-mocks @elastic/kibana-core +packages/core/capabilities/core-capabilities-common @elastic/kibana-core +packages/core/capabilities/core-capabilities-server @elastic/kibana-core +packages/core/capabilities/core-capabilities-server-internal @elastic/kibana-core +packages/core/capabilities/core-capabilities-server-mocks @elastic/kibana-core +packages/core/chrome/core-chrome-browser @elastic/appex-sharedux +packages/core/chrome/core-chrome-browser-internal @elastic/appex-sharedux +packages/core/chrome/core-chrome-browser-mocks @elastic/appex-sharedux +packages/core/config/core-config-server-internal @elastic/kibana-core +packages/core/custom-branding/core-custom-branding-browser @elastic/appex-sharedux +packages/core/custom-branding/core-custom-branding-browser-internal @elastic/appex-sharedux +packages/core/custom-branding/core-custom-branding-browser-mocks @elastic/appex-sharedux +packages/core/custom-branding/core-custom-branding-common @elastic/appex-sharedux +packages/core/custom-branding/core-custom-branding-server @elastic/appex-sharedux +packages/core/custom-branding/core-custom-branding-server-internal @elastic/appex-sharedux +packages/core/custom-branding/core-custom-branding-server-mocks @elastic/appex-sharedux +packages/core/deprecations/core-deprecations-browser @elastic/kibana-core +packages/core/deprecations/core-deprecations-browser-internal @elastic/kibana-core +packages/core/deprecations/core-deprecations-browser-mocks @elastic/kibana-core +packages/core/deprecations/core-deprecations-common @elastic/kibana-core +packages/core/deprecations/core-deprecations-server @elastic/kibana-core +packages/core/deprecations/core-deprecations-server-internal @elastic/kibana-core +packages/core/deprecations/core-deprecations-server-mocks @elastic/kibana-core +packages/core/doc-links/core-doc-links-browser @elastic/kibana-core +packages/core/doc-links/core-doc-links-browser-internal @elastic/kibana-core +packages/core/doc-links/core-doc-links-browser-mocks @elastic/kibana-core +packages/core/doc-links/core-doc-links-server @elastic/kibana-core +packages/core/doc-links/core-doc-links-server-internal @elastic/kibana-core +packages/core/doc-links/core-doc-links-server-mocks @elastic/kibana-core +packages/core/elasticsearch/core-elasticsearch-client-server-internal @elastic/kibana-core +packages/core/elasticsearch/core-elasticsearch-client-server-mocks @elastic/kibana-core +packages/core/elasticsearch/core-elasticsearch-server @elastic/kibana-core +packages/core/elasticsearch/core-elasticsearch-server-internal @elastic/kibana-core +packages/core/elasticsearch/core-elasticsearch-server-mocks @elastic/kibana-core +packages/core/environment/core-environment-server-internal @elastic/kibana-core +packages/core/environment/core-environment-server-mocks @elastic/kibana-core +packages/core/execution-context/core-execution-context-browser @elastic/kibana-core +packages/core/execution-context/core-execution-context-browser-internal @elastic/kibana-core +packages/core/execution-context/core-execution-context-browser-mocks @elastic/kibana-core +packages/core/execution-context/core-execution-context-common @elastic/kibana-core +packages/core/execution-context/core-execution-context-server @elastic/kibana-core +packages/core/execution-context/core-execution-context-server-internal @elastic/kibana-core +packages/core/execution-context/core-execution-context-server-mocks @elastic/kibana-core +packages/core/fatal-errors/core-fatal-errors-browser @elastic/kibana-core +packages/core/fatal-errors/core-fatal-errors-browser-internal @elastic/kibana-core +packages/core/fatal-errors/core-fatal-errors-browser-mocks @elastic/kibana-core +packages/core/feature-flags/core-feature-flags-browser @elastic/kibana-core +packages/core/feature-flags/core-feature-flags-browser-internal @elastic/kibana-core +packages/core/feature-flags/core-feature-flags-browser-mocks @elastic/kibana-core +packages/core/feature-flags/core-feature-flags-server @elastic/kibana-core +packages/core/feature-flags/core-feature-flags-server-internal @elastic/kibana-core +packages/core/feature-flags/core-feature-flags-server-mocks @elastic/kibana-core +test/plugin_functional/plugins/core_history_block @elastic/kibana-core +packages/core/http/core-http-browser @elastic/kibana-core +packages/core/http/core-http-browser-internal @elastic/kibana-core +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 +test/plugin_functional/plugins/core_http @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-resources-server @elastic/kibana-core +packages/core/http/core-http-resources-server-internal @elastic/kibana-core +packages/core/http/core-http-resources-server-mocks @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 +packages/core/http/core-http-server-internal @elastic/kibana-core +packages/core/http/core-http-server-mocks @elastic/kibana-core +packages/core/i18n/core-i18n-browser @elastic/kibana-core +packages/core/i18n/core-i18n-browser-internal @elastic/kibana-core +packages/core/i18n/core-i18n-browser-mocks @elastic/kibana-core +packages/core/i18n/core-i18n-server @elastic/kibana-core +packages/core/i18n/core-i18n-server-internal @elastic/kibana-core +packages/core/i18n/core-i18n-server-mocks @elastic/kibana-core +packages/core/injected-metadata/core-injected-metadata-browser-internal @elastic/kibana-core +packages/core/injected-metadata/core-injected-metadata-browser-mocks @elastic/kibana-core +packages/core/injected-metadata/core-injected-metadata-common-internal @elastic/kibana-core +packages/core/integrations/core-integrations-browser-internal @elastic/kibana-core +packages/core/integrations/core-integrations-browser-mocks @elastic/kibana-core +packages/core/lifecycle/core-lifecycle-browser @elastic/kibana-core +packages/core/lifecycle/core-lifecycle-browser-internal @elastic/kibana-core +packages/core/lifecycle/core-lifecycle-browser-mocks @elastic/kibana-core +packages/core/lifecycle/core-lifecycle-server @elastic/kibana-core +packages/core/lifecycle/core-lifecycle-server-internal @elastic/kibana-core +packages/core/lifecycle/core-lifecycle-server-mocks @elastic/kibana-core +packages/core/logging/core-logging-browser-internal @elastic/kibana-core +packages/core/logging/core-logging-browser-mocks @elastic/kibana-core +packages/core/logging/core-logging-common-internal @elastic/kibana-core +packages/core/logging/core-logging-server @elastic/kibana-core +packages/core/logging/core-logging-server-internal @elastic/kibana-core +packages/core/logging/core-logging-server-mocks @elastic/kibana-core +packages/core/metrics/core-metrics-collectors-server-internal @elastic/kibana-core +packages/core/metrics/core-metrics-collectors-server-mocks @elastic/kibana-core +packages/core/metrics/core-metrics-server @elastic/kibana-core +packages/core/metrics/core-metrics-server-internal @elastic/kibana-core +packages/core/metrics/core-metrics-server-mocks @elastic/kibana-core +packages/core/mount-utils/core-mount-utils-browser @elastic/kibana-core +packages/core/mount-utils/core-mount-utils-browser-internal @elastic/kibana-core +packages/core/node/core-node-server @elastic/kibana-core +packages/core/node/core-node-server-internal @elastic/kibana-core +packages/core/node/core-node-server-mocks @elastic/kibana-core +packages/core/notifications/core-notifications-browser @elastic/kibana-core +packages/core/notifications/core-notifications-browser-internal @elastic/kibana-core +packages/core/notifications/core-notifications-browser-mocks @elastic/kibana-core +packages/core/overlays/core-overlays-browser @elastic/kibana-core +packages/core/overlays/core-overlays-browser-internal @elastic/kibana-core +packages/core/overlays/core-overlays-browser-mocks @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_a @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_appleave @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_b @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_chromeless @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_deep_links @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_deprecations @elastic/kibana-core +test/plugin_functional/plugins/core_dynamic_resolving_a @elastic/kibana-core +test/plugin_functional/plugins/core_dynamic_resolving_b @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_execution_context @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_helpmenu @elastic/kibana-core +test/node_roles_functional/plugins/core_plugin_initializer_context @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_route_timeouts @elastic/kibana-core +test/plugin_functional/plugins/core_plugin_static_assets @elastic/kibana-core +packages/core/plugins/core-plugins-base-server-internal @elastic/kibana-core +packages/core/plugins/core-plugins-browser @elastic/kibana-core +packages/core/plugins/core-plugins-browser-internal @elastic/kibana-core +packages/core/plugins/core-plugins-browser-mocks @elastic/kibana-core +packages/core/plugins/core-plugins-contracts-browser @elastic/kibana-core +packages/core/plugins/core-plugins-contracts-server @elastic/kibana-core +packages/core/plugins/core-plugins-server @elastic/kibana-core +packages/core/plugins/core-plugins-server-internal @elastic/kibana-core +packages/core/plugins/core-plugins-server-mocks @elastic/kibana-core +packages/core/preboot/core-preboot-server @elastic/kibana-core +packages/core/preboot/core-preboot-server-internal @elastic/kibana-core +packages/core/preboot/core-preboot-server-mocks @elastic/kibana-core +test/plugin_functional/plugins/core_provider_plugin @elastic/kibana-core +packages/core/rendering/core-rendering-browser-internal @elastic/kibana-core +packages/core/rendering/core-rendering-browser-mocks @elastic/kibana-core +packages/core/rendering/core-rendering-server-internal @elastic/kibana-core +packages/core/rendering/core-rendering-server-mocks @elastic/kibana-core +packages/core/root/core-root-browser-internal @elastic/kibana-core +packages/core/root/core-root-server-internal @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-api-browser @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-api-server @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-api-server-internal @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-api-server-mocks @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-base-server-internal @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-base-server-mocks @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-browser @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-browser-internal @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-browser-mocks @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-common @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-import-export-server-internal @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-import-export-server-mocks @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-migration-server-internal @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-migration-server-mocks @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-server @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-server-internal @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-server-mocks @elastic/kibana-core +packages/core/saved-objects/core-saved-objects-utils-server @elastic/kibana-core +packages/core/security/core-security-browser @elastic/kibana-core +packages/core/security/core-security-browser-internal @elastic/kibana-core +packages/core/security/core-security-browser-mocks @elastic/kibana-core +packages/core/security/core-security-common @elastic/kibana-core @elastic/kibana-security +packages/core/security/core-security-server @elastic/kibana-core +packages/core/security/core-security-server-internal @elastic/kibana-core +packages/core/security/core-security-server-mocks @elastic/kibana-core +packages/core/status/core-status-common @elastic/kibana-core +packages/core/status/core-status-common-internal @elastic/kibana-core +packages/core/status/core-status-server @elastic/kibana-core +packages/core/status/core-status-server-internal @elastic/kibana-core +packages/core/status/core-status-server-mocks @elastic/kibana-core +packages/core/test-helpers/core-test-helpers-deprecations-getters @elastic/kibana-core +packages/core/test-helpers/core-test-helpers-http-setup-browser @elastic/kibana-core +packages/core/test-helpers/core-test-helpers-kbn-server @elastic/kibana-core +packages/core/test-helpers/core-test-helpers-model-versions @elastic/kibana-core +packages/core/test-helpers/core-test-helpers-so-type-serializer @elastic/kibana-core +packages/core/test-helpers/core-test-helpers-test-utils @elastic/kibana-core +packages/core/theme/core-theme-browser @elastic/kibana-core +packages/core/theme/core-theme-browser-internal @elastic/kibana-core +packages/core/theme/core-theme-browser-mocks @elastic/kibana-core +packages/core/ui-settings/core-ui-settings-browser @elastic/appex-sharedux +packages/core/ui-settings/core-ui-settings-browser-internal @elastic/appex-sharedux +packages/core/ui-settings/core-ui-settings-browser-mocks @elastic/appex-sharedux +packages/core/ui-settings/core-ui-settings-common @elastic/appex-sharedux +packages/core/ui-settings/core-ui-settings-server @elastic/appex-sharedux +packages/core/ui-settings/core-ui-settings-server-internal @elastic/appex-sharedux +packages/core/ui-settings/core-ui-settings-server-mocks @elastic/appex-sharedux +packages/core/usage-data/core-usage-data-base-server-internal @elastic/kibana-core +packages/core/usage-data/core-usage-data-server @elastic/kibana-core +packages/core/usage-data/core-usage-data-server-internal @elastic/kibana-core +packages/core/usage-data/core-usage-data-server-mocks @elastic/kibana-core +packages/core/user-profile/core-user-profile-browser @elastic/kibana-core +packages/core/user-profile/core-user-profile-browser-internal @elastic/kibana-core +packages/core/user-profile/core-user-profile-browser-mocks @elastic/kibana-core +packages/core/user-profile/core-user-profile-common @elastic/kibana-core +packages/core/user-profile/core-user-profile-server @elastic/kibana-core +packages/core/user-profile/core-user-profile-server-internal @elastic/kibana-core +packages/core/user-profile/core-user-profile-server-mocks @elastic/kibana-core +packages/core/user-settings/core-user-settings-server @elastic/kibana-security +packages/core/user-settings/core-user-settings-server-internal @elastic/kibana-security +packages/core/user-settings/core-user-settings-server-mocks @elastic/kibana-security +x-pack/plugins/cross_cluster_replication @elastic/kibana-management +packages/kbn-crypto @elastic/kibana-security +packages/kbn-crypto-browser @elastic/kibana-core +x-pack/plugins/custom_branding @elastic/appex-sharedux +packages/kbn-custom-icons @elastic/obs-ux-logs-team +packages/kbn-custom-integrations @elastic/obs-ux-logs-team +src/plugins/custom_integrations @elastic/fleet +packages/kbn-cypress-config @elastic/kibana-operations +x-pack/plugins/dashboard_enhanced @elastic/kibana-presentation +src/plugins/dashboard @elastic/kibana-presentation +x-pack/packages/kbn-data-forge @elastic/obs-ux-management-team +src/plugins/data @elastic/kibana-visualizations @elastic/kibana-data-discovery +x-pack/plugins/data_quality @elastic/obs-ux-logs-team +test/plugin_functional/plugins/data_search @elastic/kibana-data-discovery +packages/kbn-data-service @elastic/kibana-visualizations @elastic/kibana-data-discovery +packages/kbn-data-stream-adapter @elastic/security-threat-hunting-explore +x-pack/plugins/data_usage @elastic/obs-ai-assistant @elastic/security-solution +src/plugins/data_view_editor @elastic/kibana-data-discovery +examples/data_view_field_editor_example @elastic/kibana-data-discovery +src/plugins/data_view_field_editor @elastic/kibana-data-discovery +src/plugins/data_view_management @elastic/kibana-data-discovery +packages/kbn-data-view-utils @elastic/kibana-data-discovery +src/plugins/data_views @elastic/kibana-data-discovery +x-pack/plugins/data_visualizer @elastic/ml-ui +x-pack/plugins/observability_solution/dataset_quality @elastic/obs-ux-logs-team +packages/kbn-datemath @elastic/kibana-data-discovery +packages/deeplinks/analytics @elastic/kibana-data-discovery @elastic/kibana-presentation @elastic/kibana-visualizations +packages/deeplinks/devtools @elastic/kibana-management +packages/deeplinks/fleet @elastic/fleet +packages/deeplinks/management @elastic/kibana-management +packages/deeplinks/ml @elastic/ml-ui +packages/deeplinks/observability @elastic/obs-ux-management-team +packages/deeplinks/search @elastic/search-kibana +packages/deeplinks/security @elastic/security-solution +packages/deeplinks/shared @elastic/appex-sharedux +packages/default-nav/analytics @elastic/kibana-data-discovery @elastic/kibana-presentation @elastic/kibana-visualizations +packages/default-nav/devtools @elastic/kibana-management +packages/default-nav/management @elastic/kibana-management +packages/default-nav/ml @elastic/ml-ui +packages/kbn-dev-cli-errors @elastic/kibana-operations +packages/kbn-dev-cli-runner @elastic/kibana-operations +packages/kbn-dev-proc-runner @elastic/kibana-operations +src/plugins/dev_tools @elastic/kibana-management +packages/kbn-dev-utils @elastic/kibana-operations +examples/developer_examples @elastic/appex-sharedux +packages/kbn-discover-contextual-components @elastic/obs-ux-logs-team @elastic/kibana-data-discovery +examples/discover_customization_examples @elastic/kibana-data-discovery +x-pack/plugins/discover_enhanced @elastic/kibana-data-discovery +src/plugins/discover @elastic/kibana-data-discovery +src/plugins/discover_shared @elastic/kibana-data-discovery @elastic/obs-ux-logs-team +packages/kbn-discover-utils @elastic/kibana-data-discovery +packages/kbn-doc-links @elastic/docs +packages/kbn-docs-utils @elastic/kibana-operations +packages/kbn-dom-drag-drop @elastic/kibana-visualizations @elastic/kibana-data-discovery +packages/kbn-ebt-tools @elastic/kibana-core +x-pack/packages/security-solution/ecs_data_quality_dashboard @elastic/security-threat-hunting-explore +x-pack/plugins/ecs_data_quality_dashboard @elastic/security-threat-hunting-explore +packages/kbn-elastic-agent-utils @elastic/obs-ux-logs-team +x-pack/packages/kbn-elastic-assistant @elastic/security-generative-ai +x-pack/packages/kbn-elastic-assistant-common @elastic/security-generative-ai +x-pack/plugins/elastic_assistant @elastic/security-generative-ai +test/plugin_functional/plugins/elasticsearch_client_plugin @elastic/kibana-core +x-pack/test/plugin_api_integration/plugins/elasticsearch_client @elastic/kibana-core +x-pack/plugins/embeddable_enhanced @elastic/kibana-presentation +examples/embeddable_examples @elastic/kibana-presentation +src/plugins/embeddable @elastic/kibana-presentation +x-pack/examples/embedded_lens_example @elastic/kibana-visualizations +x-pack/plugins/encrypted_saved_objects @elastic/kibana-security +x-pack/plugins/enterprise_search @elastic/search-kibana +x-pack/plugins/observability_solution/entities_data_access @elastic/obs-entities +x-pack/packages/kbn-entities-schema @elastic/obs-entities +x-pack/test/api_integration/apis/entity_manager/fixture_plugin @elastic/obs-entities +x-pack/plugins/entity_manager @elastic/obs-entities +examples/error_boundary @elastic/appex-sharedux +packages/kbn-es @elastic/kibana-operations +packages/kbn-es-archiver @elastic/kibana-operations @elastic/appex-qa +packages/kbn-es-errors @elastic/kibana-core +packages/kbn-es-query @elastic/kibana-data-discovery +packages/kbn-es-types @elastic/kibana-core @elastic/obs-knowledge-team +src/plugins/es_ui_shared @elastic/kibana-management +packages/kbn-eslint-config @elastic/kibana-operations +packages/kbn-eslint-plugin-disable @elastic/kibana-operations +packages/kbn-eslint-plugin-eslint @elastic/kibana-operations +packages/kbn-eslint-plugin-i18n @elastic/obs-knowledge-team @elastic/kibana-operations +packages/kbn-eslint-plugin-imports @elastic/kibana-operations +packages/kbn-eslint-plugin-telemetry @elastic/obs-knowledge-team +examples/eso_model_version_example @elastic/kibana-security +x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security +src/plugins/esql @elastic/kibana-esql +packages/kbn-esql-ast @elastic/kibana-esql +examples/esql_ast_inspector @elastic/kibana-esql +src/plugins/esql_datagrid @elastic/kibana-esql +packages/kbn-esql-editor @elastic/kibana-esql +packages/kbn-esql-utils @elastic/kibana-esql +packages/kbn-esql-validation-autocomplete @elastic/kibana-esql +examples/esql_validation_example @elastic/kibana-esql +test/plugin_functional/plugins/eui_provider_dev_warning @elastic/appex-sharedux +packages/kbn-event-annotation-common @elastic/kibana-visualizations +packages/kbn-event-annotation-components @elastic/kibana-visualizations +src/plugins/event_annotation_listing @elastic/kibana-visualizations +src/plugins/event_annotation @elastic/kibana-visualizations +x-pack/test/plugin_api_integration/plugins/event_log @elastic/response-ops +x-pack/plugins/event_log @elastic/response-ops +packages/kbn-expandable-flyout @elastic/security-threat-hunting-investigations +packages/kbn-expect @elastic/kibana-operations @elastic/appex-qa +x-pack/examples/exploratory_view_example @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/exploratory_view @elastic/obs-ux-management-team +src/plugins/expression_error @elastic/kibana-presentation +src/plugins/chart_expressions/expression_gauge @elastic/kibana-visualizations +src/plugins/chart_expressions/expression_heatmap @elastic/kibana-visualizations +src/plugins/expression_image @elastic/kibana-presentation +src/plugins/chart_expressions/expression_legacy_metric @elastic/kibana-visualizations +src/plugins/expression_metric @elastic/kibana-presentation +src/plugins/chart_expressions/expression_metric @elastic/kibana-visualizations +src/plugins/chart_expressions/expression_partition_vis @elastic/kibana-visualizations +src/plugins/expression_repeat_image @elastic/kibana-presentation +src/plugins/expression_reveal_image @elastic/kibana-presentation +src/plugins/expression_shape @elastic/kibana-presentation +src/plugins/chart_expressions/expression_tagcloud @elastic/kibana-visualizations +src/plugins/chart_expressions/expression_xy @elastic/kibana-visualizations +examples/expressions_explorer @elastic/kibana-visualizations +src/plugins/expressions @elastic/kibana-visualizations +packages/kbn-failed-test-reporter-cli @elastic/kibana-operations @elastic/appex-qa +examples/feature_control_examples @elastic/kibana-security +examples/feature_flags_example @elastic/kibana-core +x-pack/test/plugin_api_integration/plugins/feature_usage_test @elastic/kibana-security +x-pack/plugins/features @elastic/kibana-core +x-pack/test/security_api_integration/plugins/features_provider @elastic/kibana-security +x-pack/test/functional_execution_context/plugins/alerts @elastic/kibana-core +examples/field_formats_example @elastic/kibana-data-discovery +src/plugins/field_formats @elastic/kibana-data-discovery +packages/kbn-field-types @elastic/kibana-data-discovery +packages/kbn-field-utils @elastic/kibana-data-discovery +x-pack/plugins/fields_metadata @elastic/obs-ux-logs-team +x-pack/plugins/file_upload @elastic/kibana-gis @elastic/ml-ui +examples/files_example @elastic/appex-sharedux +src/plugins/files_management @elastic/appex-sharedux +src/plugins/files @elastic/appex-sharedux +packages/kbn-find-used-node-modules @elastic/kibana-operations +x-pack/plugins/fleet @elastic/fleet +packages/kbn-flot-charts @elastic/kibana-operations +x-pack/test/ui_capabilities/common/plugins/foo_plugin @elastic/kibana-security +packages/kbn-formatters @elastic/obs-ux-logs-team +src/plugins/ftr_apis @elastic/kibana-core +packages/kbn-ftr-common-functional-services @elastic/kibana-operations @elastic/appex-qa +packages/kbn-ftr-common-functional-ui-services @elastic/appex-qa +packages/kbn-ftr-screenshot-filename @elastic/kibana-operations @elastic/appex-qa +x-pack/test/functional_with_es_ssl/plugins/cases @elastic/response-ops +x-pack/examples/gen_ai_streaming_response_example @elastic/response-ops +packages/kbn-generate @elastic/kibana-operations +packages/kbn-generate-console-definitions @elastic/kibana-management +packages/kbn-generate-csv @elastic/appex-sharedux +packages/kbn-get-repo-files @elastic/kibana-operations +x-pack/plugins/global_search_bar @elastic/appex-sharedux +x-pack/plugins/global_search @elastic/appex-sharedux +x-pack/plugins/global_search_providers @elastic/appex-sharedux +x-pack/test/plugin_functional/plugins/global_search_test @elastic/kibana-core +x-pack/plugins/graph @elastic/kibana-visualizations +examples/grid_example @elastic/kibana-presentation +packages/kbn-grid-layout @elastic/kibana-presentation +x-pack/plugins/grokdebugger @elastic/kibana-management +packages/kbn-grouping @elastic/response-ops +packages/kbn-guided-onboarding @elastic/appex-sharedux +examples/guided_onboarding_example @elastic/appex-sharedux +src/plugins/guided_onboarding @elastic/appex-sharedux +packages/kbn-handlebars @elastic/kibana-security +packages/kbn-hapi-mocks @elastic/kibana-core +test/plugin_functional/plugins/hardening @elastic/kibana-security +packages/kbn-health-gateway-server @elastic/kibana-core +examples/hello_world @elastic/kibana-core +src/plugins/home @elastic/kibana-core +packages/home/sample_data_card @elastic/appex-sharedux +packages/home/sample_data_tab @elastic/appex-sharedux +packages/home/sample_data_types @elastic/appex-sharedux +packages/kbn-i18n @elastic/kibana-core +packages/kbn-i18n-react @elastic/kibana-core +x-pack/test/functional_embedded/plugins/iframe_embedded @elastic/kibana-core +src/plugins/image_embeddable @elastic/appex-sharedux +packages/kbn-import-locator @elastic/kibana-operations +packages/kbn-import-resolver @elastic/kibana-operations +x-pack/plugins/index_lifecycle_management @elastic/kibana-management +x-pack/plugins/index_management @elastic/kibana-management +x-pack/packages/index-management/index_management_shared_types @elastic/kibana-management +test/plugin_functional/plugins/index_patterns @elastic/kibana-data-discovery +x-pack/packages/ml/inference_integration_flyout @elastic/ml-ui +x-pack/packages/ai-infra/inference-common @elastic/appex-ai-infra +x-pack/plugins/inference @elastic/appex-ai-infra +x-pack/packages/kbn-infra-forge @elastic/obs-ux-management-team +x-pack/plugins/observability_solution/infra @elastic/obs-ux-logs-team @elastic/obs-ux-infra_services-team +x-pack/plugins/ingest_pipelines @elastic/kibana-management +src/plugins/input_control_vis @elastic/kibana-presentation +src/plugins/inspector @elastic/kibana-presentation +x-pack/plugins/integration_assistant @elastic/security-scalability +src/plugins/interactive_setup @elastic/kibana-security +test/interactive_setup_api_integration/plugins/test_endpoints @elastic/kibana-security +packages/kbn-interpreter @elastic/kibana-visualizations +x-pack/plugins/observability_solution/inventory/e2e @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/inventory @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/investigate_app @elastic/obs-ux-management-team +x-pack/plugins/observability_solution/investigate @elastic/obs-ux-management-team +packages/kbn-investigation-shared @elastic/obs-ux-management-team +packages/kbn-io-ts-utils @elastic/obs-knowledge-team +packages/kbn-ipynb @elastic/search-kibana +packages/kbn-jest-serializers @elastic/kibana-operations +packages/kbn-journeys @elastic/kibana-operations @elastic/appex-qa +packages/kbn-json-ast @elastic/kibana-operations +x-pack/packages/ml/json_schemas @elastic/ml-ui +test/health_gateway/plugins/status @elastic/kibana-core +test/plugin_functional/plugins/kbn_sample_panel_action @elastic/appex-sharedux +test/plugin_functional/plugins/kbn_top_nav @elastic/kibana-core +test/plugin_functional/plugins/kbn_tp_custom_visualizations @elastic/kibana-visualizations +test/interpreter_functional/plugins/kbn_tp_run_pipeline @elastic/kibana-core +x-pack/test/functional_cors/plugins/kibana_cors_test @elastic/kibana-security +packages/kbn-kibana-manifest-schema @elastic/kibana-operations +src/plugins/kibana_overview @elastic/appex-sharedux +src/plugins/kibana_react @elastic/appex-sharedux +src/plugins/kibana_usage_collection @elastic/kibana-core +src/plugins/kibana_utils @elastic/appex-sharedux +x-pack/plugins/kubernetes_security @elastic/kibana-cloud-security-posture +x-pack/packages/kbn-langchain @elastic/security-generative-ai +packages/kbn-language-documentation @elastic/kibana-esql +x-pack/examples/lens_config_builder_example @elastic/kibana-visualizations +packages/kbn-lens-embeddable-utils @elastic/obs-ux-infra_services-team @elastic/kibana-visualizations +packages/kbn-lens-formula-docs @elastic/kibana-visualizations +x-pack/examples/lens_embeddable_inline_editing_example @elastic/kibana-visualizations +x-pack/plugins/lens @elastic/kibana-visualizations +x-pack/plugins/license_api_guard @elastic/kibana-management +x-pack/plugins/license_management @elastic/kibana-management +x-pack/plugins/licensing @elastic/kibana-core +src/plugins/links @elastic/kibana-presentation +packages/kbn-lint-packages-cli @elastic/kibana-operations +packages/kbn-lint-ts-projects-cli @elastic/kibana-operations +x-pack/plugins/lists @elastic/security-detection-engine +examples/locator_examples @elastic/appex-sharedux +examples/locator_explorer @elastic/appex-sharedux +packages/kbn-logging @elastic/kibana-core +packages/kbn-logging-mocks @elastic/kibana-core +x-pack/plugins/observability_solution/logs_data_access @elastic/obs-knowledge-team @elastic/obs-ux-logs-team +x-pack/plugins/observability_solution/logs_explorer @elastic/obs-ux-logs-team +x-pack/plugins/observability_solution/logs_shared @elastic/obs-ux-logs-team +x-pack/plugins/logstash @elastic/logstash +packages/kbn-managed-content-badge @elastic/kibana-visualizations +packages/kbn-managed-vscode-config @elastic/kibana-operations +packages/kbn-managed-vscode-config-cli @elastic/kibana-operations +packages/kbn-management/cards_navigation @elastic/kibana-management +src/plugins/management @elastic/kibana-management +packages/kbn-management/settings/application @elastic/kibana-management +packages/kbn-management/settings/components/field_category @elastic/kibana-management +packages/kbn-management/settings/components/field_input @elastic/kibana-management +packages/kbn-management/settings/components/field_row @elastic/kibana-management +packages/kbn-management/settings/components/form @elastic/kibana-management +packages/kbn-management/settings/field_definition @elastic/kibana-management +packages/kbn-management/settings/setting_ids @elastic/appex-sharedux @elastic/kibana-management +packages/kbn-management/settings/section_registry @elastic/appex-sharedux @elastic/kibana-management +packages/kbn-management/settings/types @elastic/kibana-management +packages/kbn-management/settings/utilities @elastic/kibana-management +packages/kbn-management/storybook/config @elastic/kibana-management +test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management +packages/kbn-manifest @elastic/kibana-core +packages/kbn-mapbox-gl @elastic/kibana-gis +x-pack/examples/third_party_maps_source_example @elastic/kibana-gis +src/plugins/maps_ems @elastic/kibana-gis +x-pack/plugins/maps @elastic/kibana-gis +x-pack/packages/maps/vector_tile_utils @elastic/kibana-gis +x-pack/plugins/observability_solution/metrics_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team +x-pack/packages/ml/agg_utils @elastic/ml-ui +x-pack/packages/ml/anomaly_utils @elastic/ml-ui +x-pack/packages/ml/cancellable_search @elastic/ml-ui +x-pack/packages/ml/category_validator @elastic/ml-ui +x-pack/packages/ml/chi2test @elastic/ml-ui +x-pack/packages/ml/creation_wizard_utils @elastic/ml-ui +x-pack/packages/ml/data_frame_analytics_utils @elastic/ml-ui +x-pack/packages/ml/data_grid @elastic/ml-ui +x-pack/packages/ml/data_view_utils @elastic/ml-ui +x-pack/packages/ml/date_picker @elastic/ml-ui +x-pack/packages/ml/date_utils @elastic/ml-ui +x-pack/packages/ml/error_utils @elastic/ml-ui +x-pack/packages/ml/field_stats_flyout @elastic/ml-ui +x-pack/packages/ml/in_memory_table @elastic/ml-ui +x-pack/packages/ml/is_defined @elastic/ml-ui +x-pack/packages/ml/is_populated_object @elastic/ml-ui +x-pack/packages/ml/kibana_theme @elastic/ml-ui +x-pack/packages/ml/local_storage @elastic/ml-ui +x-pack/packages/ml/nested_property @elastic/ml-ui +x-pack/packages/ml/number_utils @elastic/ml-ui +x-pack/packages/ml/parse_interval @elastic/ml-ui +x-pack/plugins/ml @elastic/ml-ui +x-pack/packages/ml/query_utils @elastic/ml-ui +x-pack/packages/ml/random_sampler_utils @elastic/ml-ui +x-pack/packages/ml/response_stream @elastic/ml-ui +x-pack/packages/ml/route_utils @elastic/ml-ui +x-pack/packages/ml/runtime_field_utils @elastic/ml-ui +x-pack/packages/ml/string_hash @elastic/ml-ui +x-pack/packages/ml/time_buckets @elastic/ml-ui +x-pack/packages/ml/trained_models_utils @elastic/ml-ui +x-pack/packages/ml/ui_actions @elastic/ml-ui +x-pack/packages/ml/url_state @elastic/ml-ui +x-pack/packages/ml/validators @elastic/ml-ui +packages/kbn-mock-idp-plugin @elastic/kibana-security +packages/kbn-mock-idp-utils @elastic/kibana-security +packages/kbn-monaco @elastic/appex-sharedux +x-pack/plugins/monitoring_collection @elastic/stack-monitoring +x-pack/plugins/monitoring @elastic/stack-monitoring +src/plugins/navigation @elastic/appex-sharedux +src/plugins/newsfeed @elastic/kibana-core +test/common/plugins/newsfeed @elastic/kibana-core +src/plugins/no_data_page @elastic/appex-sharedux +x-pack/plugins/notifications @elastic/appex-sharedux +packages/kbn-object-versioning @elastic/appex-sharedux +packages/kbn-object-versioning-utils @elastic/appex-sharedux +x-pack/plugins/observability_solution/observability_ai_assistant_app @elastic/obs-ai-assistant +x-pack/plugins/observability_solution/observability_ai_assistant_management @elastic/obs-ai-assistant +x-pack/plugins/observability_solution/observability_ai_assistant @elastic/obs-ai-assistant +x-pack/packages/observability/alert_details @elastic/obs-ux-management-team +x-pack/packages/observability/alerting_rule_utils @elastic/obs-ux-management-team +x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team +x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops +x-pack/packages/observability/get_padded_alert_time_range_util @elastic/obs-ux-management-team +x-pack/plugins/observability_solution/observability_logs_explorer @elastic/obs-ux-logs-team +x-pack/packages/observability/logs_overview @elastic/obs-ux-logs-team +x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team +x-pack/plugins/observability_solution/observability_onboarding @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team +x-pack/plugins/observability_solution/observability @elastic/obs-ux-management-team +x-pack/plugins/observability_solution/observability_shared @elastic/observability-ui +x-pack/packages/observability/synthetics_test_data @elastic/obs-ux-management-team +x-pack/packages/observability/observability_utils @elastic/observability-ui +x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security +test/common/plugins/otel_metrics @elastic/obs-ux-infra_services-team +packages/kbn-openapi-bundler @elastic/security-detection-rule-management +packages/kbn-openapi-common @elastic/security-detection-rule-management +packages/kbn-openapi-generator @elastic/security-detection-rule-management +packages/kbn-optimizer @elastic/kibana-operations +packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations +packages/kbn-osquery-io-ts-types @elastic/security-asset-management +x-pack/plugins/osquery @elastic/security-defend-workflows +examples/partial_results_example @elastic/kibana-data-discovery +x-pack/plugins/painless_lab @elastic/kibana-management +packages/kbn-panel-loader @elastic/kibana-presentation +packages/kbn-peggy @elastic/kibana-operations +packages/kbn-peggy-loader @elastic/kibana-operations +packages/kbn-performance-testing-dataset-extractor @elastic/kibana-performance-testing +packages/kbn-picomatcher @elastic/kibana-operations +packages/kbn-plugin-check @elastic/appex-sharedux +packages/kbn-plugin-generator @elastic/kibana-operations +packages/kbn-plugin-helpers @elastic/kibana-operations +examples/portable_dashboards_example @elastic/kibana-presentation +examples/preboot_example @elastic/kibana-security @elastic/kibana-core +packages/presentation/presentation_containers @elastic/kibana-presentation +src/plugins/presentation_panel @elastic/kibana-presentation +packages/presentation/presentation_publishing @elastic/kibana-presentation +src/plugins/presentation_util @elastic/kibana-presentation +x-pack/packages/ai-infra/product-doc-artifact-builder @elastic/appex-ai-infra +x-pack/plugins/observability_solution/profiling_data_access @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/profiling @elastic/obs-ux-infra_services-team +packages/kbn-profiling-utils @elastic/obs-ux-infra_services-team +x-pack/packages/kbn-random-sampling @elastic/kibana-visualizations +packages/kbn-react-field @elastic/kibana-data-discovery +packages/kbn-react-hooks @elastic/obs-ux-logs-team +packages/react/kibana_context/common @elastic/appex-sharedux +packages/react/kibana_context/render @elastic/appex-sharedux +packages/react/kibana_context/root @elastic/appex-sharedux +packages/react/kibana_context/styled @elastic/appex-sharedux +packages/react/kibana_context/theme @elastic/appex-sharedux +packages/react/kibana_mount @elastic/appex-sharedux +packages/kbn-recently-accessed @elastic/appex-sharedux +x-pack/plugins/remote_clusters @elastic/kibana-management +test/plugin_functional/plugins/rendering_plugin @elastic/kibana-core +packages/kbn-repo-file-maps @elastic/kibana-operations +packages/kbn-repo-info @elastic/kibana-operations +packages/kbn-repo-linter @elastic/kibana-operations +packages/kbn-repo-packages @elastic/kibana-operations +packages/kbn-repo-path @elastic/kibana-operations +packages/kbn-repo-source-classifier @elastic/kibana-operations +packages/kbn-repo-source-classifier-cli @elastic/kibana-operations +packages/kbn-reporting/common @elastic/appex-sharedux +packages/kbn-reporting/get_csv_panel_actions @elastic/appex-sharedux +packages/kbn-reporting/export_types/csv @elastic/appex-sharedux +packages/kbn-reporting/export_types/csv_common @elastic/appex-sharedux +packages/kbn-reporting/export_types/pdf @elastic/appex-sharedux +packages/kbn-reporting/export_types/pdf_common @elastic/appex-sharedux +packages/kbn-reporting/export_types/png @elastic/appex-sharedux +packages/kbn-reporting/export_types/png_common @elastic/appex-sharedux +packages/kbn-reporting/mocks_server @elastic/appex-sharedux +x-pack/plugins/reporting @elastic/appex-sharedux +packages/kbn-reporting/public @elastic/appex-sharedux +packages/kbn-reporting/server @elastic/appex-sharedux +packages/kbn-resizable-layout @elastic/kibana-data-discovery +examples/resizable_layout_examples @elastic/kibana-data-discovery +x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution +packages/response-ops/feature_flag_service @elastic/response-ops +packages/response-ops/rule_params @elastic/response-ops +examples/response_stream @elastic/ml-ui +packages/kbn-rison @elastic/kibana-operations +x-pack/packages/rollup @elastic/kibana-management +x-pack/plugins/rollup @elastic/kibana-management +packages/kbn-router-to-openapispec @elastic/kibana-core +packages/kbn-router-utils @elastic/obs-ux-logs-team +examples/routing_example @elastic/kibana-core +packages/kbn-rrule @elastic/response-ops +packages/kbn-rule-data-utils @elastic/security-detections-response @elastic/response-ops @elastic/obs-ux-management-team +x-pack/plugins/rule_registry @elastic/response-ops @elastic/obs-ux-management-team +x-pack/plugins/runtime_fields @elastic/kibana-management +packages/kbn-safer-lodash-set @elastic/kibana-security +x-pack/test/security_api_integration/plugins/saml_provider @elastic/kibana-security +x-pack/test/plugin_api_integration/plugins/sample_task_plugin @elastic/response-ops +x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget @elastic/response-ops +test/plugin_functional/plugins/saved_object_export_transforms @elastic/kibana-core +test/plugin_functional/plugins/saved_object_import_warnings @elastic/kibana-core +x-pack/test/saved_object_api_integration/common/plugins/saved_object_test_plugin @elastic/kibana-security +src/plugins/saved_objects_finder @elastic/kibana-data-discovery +test/plugin_functional/plugins/saved_objects_hidden_from_http_apis_type @elastic/kibana-core +test/plugin_functional/plugins/saved_objects_hidden_type @elastic/kibana-core +src/plugins/saved_objects_management @elastic/kibana-core +src/plugins/saved_objects @elastic/kibana-core +packages/kbn-saved-objects-settings @elastic/appex-sharedux +src/plugins/saved_objects_tagging_oss @elastic/appex-sharedux +x-pack/plugins/saved_objects_tagging @elastic/appex-sharedux +src/plugins/saved_search @elastic/kibana-data-discovery +examples/screenshot_mode_example @elastic/appex-sharedux +src/plugins/screenshot_mode @elastic/appex-sharedux +x-pack/examples/screenshotting_example @elastic/appex-sharedux +x-pack/plugins/screenshotting @elastic/kibana-reporting-services +packages/kbn-screenshotting-server @elastic/appex-sharedux +packages/kbn-search-api-keys-components @elastic/search-kibana +packages/kbn-search-api-keys-server @elastic/search-kibana +packages/kbn-search-api-panels @elastic/search-kibana +x-pack/plugins/search_assistant @elastic/search-kibana +packages/kbn-search-connectors @elastic/search-kibana +x-pack/plugins/search_connectors @elastic/search-kibana +packages/kbn-search-errors @elastic/kibana-data-discovery +examples/search_examples @elastic/kibana-data-discovery +x-pack/plugins/search_homepage @elastic/search-kibana +packages/kbn-search-index-documents @elastic/search-kibana +x-pack/plugins/search_indices @elastic/search-kibana +x-pack/plugins/search_inference_endpoints @elastic/search-kibana +x-pack/plugins/search_notebooks @elastic/search-kibana +x-pack/plugins/search_playground @elastic/search-kibana +packages/kbn-search-response-warnings @elastic/kibana-data-discovery +x-pack/packages/search/shared_ui @elastic/search-kibana +packages/kbn-search-types @elastic/kibana-data-discovery +x-pack/plugins/searchprofiler @elastic/kibana-management +x-pack/test/security_api_integration/packages/helpers @elastic/kibana-security +x-pack/packages/security/api_key_management @elastic/kibana-security +x-pack/packages/security/authorization_core @elastic/kibana-security +x-pack/packages/security/authorization_core_common @elastic/kibana-security +x-pack/packages/security/form_components @elastic/kibana-security +packages/kbn-security-hardening @elastic/kibana-security +x-pack/plugins/security @elastic/kibana-security +x-pack/packages/security/plugin_types_common @elastic/kibana-security +x-pack/packages/security/plugin_types_public @elastic/kibana-security +x-pack/packages/security/plugin_types_server @elastic/kibana-security +x-pack/packages/security/role_management_model @elastic/kibana-security +x-pack/packages/security-solution/distribution_bar @elastic/kibana-cloud-security-posture +x-pack/plugins/security_solution_ess @elastic/security-solution +x-pack/packages/security-solution/features @elastic/security-threat-hunting-explore +x-pack/test/cases_api_integration/common/plugins/security_solution @elastic/response-ops +x-pack/packages/security-solution/navigation @elastic/security-threat-hunting-explore +x-pack/plugins/security_solution @elastic/security-solution +x-pack/plugins/security_solution_serverless @elastic/security-solution +x-pack/packages/security-solution/side_nav @elastic/security-threat-hunting-explore +x-pack/packages/security-solution/storybook/config @elastic/security-threat-hunting-explore +x-pack/packages/security-solution/upselling @elastic/security-threat-hunting-explore +x-pack/test/security_functional/plugins/test_endpoints @elastic/kibana-security +x-pack/packages/security/ui_components @elastic/kibana-security +packages/kbn-securitysolution-autocomplete @elastic/security-detection-engine +x-pack/packages/security-solution/data_table @elastic/security-threat-hunting-investigations +packages/kbn-securitysolution-ecs @elastic/security-threat-hunting-explore +packages/kbn-securitysolution-endpoint-exceptions-common @elastic/security-detection-engine +packages/kbn-securitysolution-es-utils @elastic/security-detection-engine +packages/kbn-securitysolution-exception-list-components @elastic/security-detection-engine +packages/kbn-securitysolution-exceptions-common @elastic/security-detection-engine +packages/kbn-securitysolution-hook-utils @elastic/security-detection-engine +packages/kbn-securitysolution-io-ts-alerting-types @elastic/security-detection-engine +packages/kbn-securitysolution-io-ts-list-types @elastic/security-detection-engine +packages/kbn-securitysolution-io-ts-types @elastic/security-detection-engine +packages/kbn-securitysolution-io-ts-utils @elastic/security-detection-engine +packages/kbn-securitysolution-list-api @elastic/security-detection-engine +packages/kbn-securitysolution-list-constants @elastic/security-detection-engine +packages/kbn-securitysolution-list-hooks @elastic/security-detection-engine +packages/kbn-securitysolution-list-utils @elastic/security-detection-engine +packages/kbn-securitysolution-lists-common @elastic/security-detection-engine +packages/kbn-securitysolution-rules @elastic/security-detection-engine +packages/kbn-securitysolution-t-grid @elastic/security-detection-engine +packages/kbn-securitysolution-utils @elastic/security-detection-engine +packages/kbn-server-http-tools @elastic/kibana-core +packages/kbn-server-route-repository @elastic/obs-knowledge-team +packages/kbn-server-route-repository-client @elastic/obs-knowledge-team +packages/kbn-server-route-repository-utils @elastic/obs-knowledge-team +x-pack/plugins/serverless @elastic/appex-sharedux +packages/serverless/settings/common @elastic/appex-sharedux @elastic/kibana-management +x-pack/plugins/serverless_observability @elastic/obs-ux-management-team +packages/serverless/settings/observability_project @elastic/appex-sharedux @elastic/kibana-management @elastic/obs-ux-management-team +packages/serverless/project_switcher @elastic/appex-sharedux +x-pack/plugins/serverless_search @elastic/search-kibana +packages/serverless/settings/search_project @elastic/search-kibana @elastic/kibana-management +packages/serverless/settings/security_project @elastic/security-solution @elastic/kibana-management +packages/serverless/storybook/config @elastic/appex-sharedux +packages/serverless/types @elastic/appex-sharedux +test/plugin_functional/plugins/session_notifications @elastic/kibana-core +x-pack/plugins/session_view @elastic/kibana-cloud-security-posture +packages/kbn-set-map @elastic/kibana-operations +examples/share_examples @elastic/appex-sharedux +src/plugins/share @elastic/appex-sharedux +packages/kbn-shared-svg @elastic/obs-ux-infra_services-team +packages/shared-ux/avatar/solution @elastic/appex-sharedux +packages/shared-ux/button/exit_full_screen @elastic/appex-sharedux +packages/shared-ux/button_toolbar @elastic/appex-sharedux +packages/shared-ux/card/no_data/impl @elastic/appex-sharedux +packages/shared-ux/card/no_data/mocks @elastic/appex-sharedux +packages/shared-ux/card/no_data/types @elastic/appex-sharedux +packages/shared-ux/chrome/navigation @elastic/appex-sharedux +packages/shared-ux/error_boundary @elastic/appex-sharedux +packages/shared-ux/file/context @elastic/appex-sharedux +packages/shared-ux/file/image/impl @elastic/appex-sharedux +packages/shared-ux/file/image/mocks @elastic/appex-sharedux +packages/shared-ux/file/mocks @elastic/appex-sharedux +packages/shared-ux/file/file_picker/impl @elastic/appex-sharedux +packages/shared-ux/file/types @elastic/appex-sharedux +packages/shared-ux/file/file_upload/impl @elastic/appex-sharedux +packages/shared-ux/file/util @elastic/appex-sharedux +packages/shared-ux/link/redirect_app/impl @elastic/appex-sharedux +packages/shared-ux/link/redirect_app/mocks @elastic/appex-sharedux +packages/shared-ux/link/redirect_app/types @elastic/appex-sharedux +packages/shared-ux/markdown/impl @elastic/appex-sharedux +packages/shared-ux/markdown/mocks @elastic/appex-sharedux +packages/shared-ux/markdown/types @elastic/appex-sharedux +packages/shared-ux/page/analytics_no_data/impl @elastic/appex-sharedux +packages/shared-ux/page/analytics_no_data/mocks @elastic/appex-sharedux +packages/shared-ux/page/analytics_no_data/types @elastic/appex-sharedux +packages/shared-ux/page/kibana_no_data/impl @elastic/appex-sharedux +packages/shared-ux/page/kibana_no_data/mocks @elastic/appex-sharedux +packages/shared-ux/page/kibana_no_data/types @elastic/appex-sharedux +packages/shared-ux/page/kibana_template/impl @elastic/appex-sharedux +packages/shared-ux/page/kibana_template/mocks @elastic/appex-sharedux +packages/shared-ux/page/kibana_template/types @elastic/appex-sharedux +packages/shared-ux/page/no_data/impl @elastic/appex-sharedux +packages/shared-ux/page/no_data_config/impl @elastic/appex-sharedux +packages/shared-ux/page/no_data_config/mocks @elastic/appex-sharedux +packages/shared-ux/page/no_data_config/types @elastic/appex-sharedux +packages/shared-ux/page/no_data/mocks @elastic/appex-sharedux +packages/shared-ux/page/no_data/types @elastic/appex-sharedux +packages/shared-ux/page/solution_nav @elastic/appex-sharedux +packages/shared-ux/prompt/no_data_views/impl @elastic/appex-sharedux +packages/shared-ux/prompt/no_data_views/mocks @elastic/appex-sharedux +packages/shared-ux/prompt/no_data_views/types @elastic/appex-sharedux +packages/shared-ux/prompt/not_found @elastic/appex-sharedux +packages/shared-ux/router/impl @elastic/appex-sharedux +packages/shared-ux/router/mocks @elastic/appex-sharedux +packages/shared-ux/router/types @elastic/appex-sharedux +packages/shared-ux/storybook/config @elastic/appex-sharedux +packages/shared-ux/storybook/mock @elastic/appex-sharedux +packages/shared-ux/modal/tabbed @elastic/appex-sharedux +packages/shared-ux/table_persist @elastic/appex-sharedux +packages/kbn-shared-ux-utility @elastic/appex-sharedux +x-pack/plugins/observability_solution/slo @elastic/obs-ux-management-team +x-pack/packages/kbn-slo-schema @elastic/obs-ux-management-team +x-pack/plugins/snapshot_restore @elastic/kibana-management +packages/kbn-some-dev-log @elastic/kibana-operations +packages/kbn-sort-package-json @elastic/kibana-operations +packages/kbn-sort-predicates @elastic/kibana-visualizations +x-pack/plugins/spaces @elastic/kibana-security +x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin @elastic/kibana-security +packages/kbn-spec-to-console @elastic/kibana-management +packages/kbn-sse-utils @elastic/obs-knowledge-team +packages/kbn-sse-utils-client @elastic/obs-knowledge-team +packages/kbn-sse-utils-server @elastic/obs-knowledge-team +x-pack/plugins/stack_alerts @elastic/response-ops +x-pack/plugins/stack_connectors @elastic/response-ops +x-pack/test/usage_collection/plugins/stack_management_usage_test @elastic/kibana-management +examples/state_containers_examples @elastic/appex-sharedux +test/server_integration/plugins/status_plugin_a @elastic/kibana-core +test/server_integration/plugins/status_plugin_b @elastic/kibana-core +packages/kbn-std @elastic/kibana-core +packages/kbn-stdio-dev-helpers @elastic/kibana-operations +packages/kbn-storybook @elastic/kibana-operations +x-pack/plugins/observability_solution/synthetics/e2e @elastic/obs-ux-management-team +x-pack/plugins/observability_solution/synthetics @elastic/obs-ux-management-team +x-pack/packages/kbn-synthetics-private-location @elastic/obs-ux-management-team +x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture @elastic/response-ops +x-pack/test/plugin_api_perf/plugins/task_manager_performance @elastic/response-ops +x-pack/plugins/task_manager @elastic/response-ops +src/plugins/telemetry_collection_manager @elastic/kibana-core +x-pack/plugins/telemetry_collection_xpack @elastic/kibana-core +src/plugins/telemetry_management_section @elastic/kibana-core +src/plugins/telemetry @elastic/kibana-core +test/plugin_functional/plugins/telemetry @elastic/kibana-core +packages/kbn-telemetry-tools @elastic/kibana-core +packages/kbn-test @elastic/kibana-operations @elastic/appex-qa +packages/kbn-test-eui-helpers @elastic/kibana-visualizations +x-pack/test/licensing_plugin/plugins/test_feature_usage @elastic/kibana-security +packages/kbn-test-jest-helpers @elastic/kibana-operations @elastic/appex-qa +packages/kbn-test-subj-selector @elastic/kibana-operations @elastic/appex-qa +x-pack/test_serverless +test +x-pack/test +x-pack/performance @elastic/appex-qa +x-pack/examples/testing_embedded_lens @elastic/kibana-visualizations +x-pack/examples/third_party_lens_navigation_prompt @elastic/kibana-visualizations +x-pack/examples/third_party_vis_lens_example @elastic/kibana-visualizations +x-pack/plugins/threat_intelligence @elastic/security-threat-hunting-investigations +x-pack/plugins/timelines @elastic/security-threat-hunting-investigations +packages/kbn-timelion-grammar @elastic/kibana-visualizations +packages/kbn-timerange @elastic/obs-ux-logs-team +packages/kbn-tinymath @elastic/kibana-visualizations +packages/kbn-tooling-log @elastic/kibana-operations +x-pack/plugins/transform @elastic/ml-ui +x-pack/plugins/translations @elastic/kibana-localization +packages/kbn-transpose-utils @elastic/kibana-visualizations +x-pack/examples/triggers_actions_ui_example @elastic/response-ops +x-pack/plugins/triggers_actions_ui @elastic/response-ops +packages/kbn-triggers-actions-ui-types @elastic/response-ops +packages/kbn-try-in-console @elastic/search-kibana +packages/kbn-ts-projects @elastic/kibana-operations +packages/kbn-ts-type-check-cli @elastic/kibana-operations +packages/kbn-typed-react-router-config @elastic/obs-knowledge-team @elastic/obs-ux-management-team +packages/kbn-ui-actions-browser @elastic/appex-sharedux +x-pack/examples/ui_actions_enhanced_examples @elastic/appex-sharedux +src/plugins/ui_actions_enhanced @elastic/appex-sharedux +examples/ui_action_examples @elastic/appex-sharedux +examples/ui_actions_explorer @elastic/appex-sharedux +src/plugins/ui_actions @elastic/appex-sharedux +test/plugin_functional/plugins/ui_settings_plugin @elastic/kibana-core +packages/kbn-ui-shared-deps-npm @elastic/kibana-operations +packages/kbn-ui-shared-deps-src @elastic/kibana-operations +packages/kbn-ui-theme @elastic/kibana-operations +packages/kbn-unified-data-table @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations +packages/kbn-unified-doc-viewer @elastic/kibana-data-discovery +examples/unified_doc_viewer @elastic/kibana-core +src/plugins/unified_doc_viewer @elastic/kibana-data-discovery +packages/kbn-unified-field-list @elastic/kibana-data-discovery +examples/unified_field_list_examples @elastic/kibana-data-discovery +src/plugins/unified_histogram @elastic/kibana-data-discovery +src/plugins/unified_search @elastic/kibana-visualizations +packages/kbn-unsaved-changes-badge @elastic/kibana-data-discovery +packages/kbn-unsaved-changes-prompt @elastic/kibana-management +x-pack/plugins/upgrade_assistant @elastic/kibana-management +x-pack/plugins/observability_solution/uptime @elastic/obs-ux-management-team +x-pack/plugins/drilldowns/url_drilldown @elastic/appex-sharedux +src/plugins/url_forwarding @elastic/kibana-visualizations +src/plugins/usage_collection @elastic/kibana-core +test/plugin_functional/plugins/usage_collection @elastic/kibana-core +packages/kbn-use-tracked-promise @elastic/obs-ux-logs-team +packages/kbn-user-profile-components @elastic/kibana-security +examples/user_profile_examples @elastic/kibana-security +x-pack/test/security_api_integration/plugins/user_profiles_consumer @elastic/kibana-security +packages/kbn-utility-types @elastic/kibana-core +packages/kbn-utility-types-jest @elastic/kibana-operations +packages/kbn-utils @elastic/kibana-operations +x-pack/plugins/observability_solution/ux @elastic/obs-ux-infra_services-team +examples/v8_profiler_examples @elastic/response-ops +packages/kbn-validate-next-docs-cli @elastic/kibana-operations +src/plugins/vis_default_editor @elastic/kibana-visualizations +src/plugins/vis_types/gauge @elastic/kibana-visualizations +src/plugins/vis_types/heatmap @elastic/kibana-visualizations +src/plugins/vis_type_markdown @elastic/kibana-presentation +src/plugins/vis_types/metric @elastic/kibana-visualizations +src/plugins/vis_types/pie @elastic/kibana-visualizations +src/plugins/vis_types/table @elastic/kibana-visualizations +src/plugins/vis_types/tagcloud @elastic/kibana-visualizations +src/plugins/vis_types/timelion @elastic/kibana-visualizations +src/plugins/vis_types/timeseries @elastic/kibana-visualizations +src/plugins/vis_types/vega @elastic/kibana-visualizations +src/plugins/vis_types/vislib @elastic/kibana-visualizations +src/plugins/vis_types/xy @elastic/kibana-visualizations +packages/kbn-visualization-ui-components @elastic/kibana-visualizations +packages/kbn-visualization-utils @elastic/kibana-visualizations +src/plugins/visualizations @elastic/kibana-visualizations +x-pack/plugins/watcher @elastic/kibana-management +packages/kbn-web-worker-stub @elastic/kibana-operations +packages/kbn-whereis-pkg-cli @elastic/kibana-operations +packages/kbn-xstate-utils @elastic/obs-ux-logs-team +packages/kbn-yarn-lock-validator @elastic/kibana-operations +packages/kbn-zod @elastic/kibana-core +packages/kbn-zod-helpers @elastic/security-detection-rule-management +#### +## Everything below this line overrides the default assignments for each package. +## Items lower in the file have higher precedence: +## https://help.github.com/articles/about-codeowners/ +#### + +# The #CC# prefix delineates Code Coverage, +# used for the 'team' designator within Kibana Stats + +/x-pack/test/api_integration/apis/metrics_ui @elastic/obs-ux-logs-team @elastic/obs-ux-infra_services-team +x-pack/test_serverless/api_integration/test_suites/common/platform_security @elastic/kibana-security + +# Observability Entities Team (@elastic/obs-entities) +/x-pack/plugins/observability_solution/entities_data_access @elastic/obs-entities +/x-pack/packages/kbn-entities-schema @elastic/obs-entities +/x-pack/test/api_integration/apis/entity_manager/fixture_plugin @elastic/obs-entities +/x-pack/plugins/entity_manager @elastic/obs-entities +/x-pack/test/api_integration/apis/entity_manager @elastic/obs-entities + +# Data Discovery +/x-pack/test/api_integration/apis/kibana/kql_telemetry @elastic/kibana-data-discovery @elastic/kibana-visualizations +/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/es_archives/kibana_sample_data_flights_index_pattern @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/security/config.examples.ts @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts @elastic/kibana-data-discovery +/test/accessibility/apps/discover.ts @elastic/kibana-data-discovery +/test/api_integration/apis/data_views @elastic/kibana-data-discovery +/test/api_integration/apis/data_view_field_editor @elastic/kibana-data-discovery +/test/api_integration/apis/kql_telemetry @elastic/kibana-data-discovery +/test/api_integration/apis/scripts @elastic/kibana-data-discovery +/test/api_integration/apis/search @elastic/kibana-data-discovery +/test/examples/data_view_field_editor_example @elastic/kibana-data-discovery +/test/examples/discover_customization_examples @elastic/kibana-data-discovery +/test/examples/field_formats @elastic/kibana-data-discovery +/test/examples/partial_results @elastic/kibana-data-discovery +/test/examples/search @elastic/kibana-data-discovery +/test/examples/unified_field_list_examples @elastic/kibana-data-discovery +/test/functional/apps/context @elastic/kibana-data-discovery +/test/functional/apps/discover @elastic/kibana-data-discovery +/test/functional/apps/management/ccs_compatibility/_data_views_ccs.ts @elastic/kibana-data-discovery +/test/functional/apps/management/data_views @elastic/kibana-data-discovery +/test/plugin_functional/test_suites/data_plugin @elastic/kibana-data-discovery +/x-pack/test/accessibility/apps/group3/search_sessions.ts @elastic/kibana-data-discovery +/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @elastic/kibana-data-discovery +/x-pack/test/api_integration/apis/search @elastic/kibana-data-discovery +/x-pack/test/examples/search_examples @elastic/kibana-data-discovery +/x-pack/test/functional/apps/data_views @elastic/kibana-data-discovery +/x-pack/test/functional/apps/discover @elastic/kibana-data-discovery +/x-pack/test/functional/apps/saved_query_management @elastic/kibana-data-discovery +/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover @elastic/kibana-data-discovery +/x-pack/test/search_sessions_integration @elastic/kibana-data-discovery +/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @elastic/kibana-data-discovery +/x-pack/test/stack_functional_integration/apps/management/_index_pattern_create.js @elastic/kibana-data-discovery +/x-pack/test/upgrade/apps/discover @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/data_views @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/search_oss @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/search_xpack @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/context @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/discover @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/field_formats @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/partial_results @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/search @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/search_examples @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples @elastic/kibana-data-discovery +/x-pack/test_serverless/functional/test_suites/common/management/data_views @elastic/kibana-data-discovery +src/plugins/discover/public/context_awareness/profile_providers/security @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations + +# Platform Docs +/x-pack/test_serverless/functional/test_suites/security/screenshot_creation/index.ts @elastic/platform-docs +/x-pack/test_serverless/functional/test_suites/security/config.screenshots.ts @elastic/platform-docs + +# Visualizations +/x-pack/test/accessibility/apps/group3/graph.ts @elastic/kibana-visualizations +/x-pack/test/accessibility/apps/group2/lens.ts @elastic/kibana-visualizations +/src/plugins/visualize/ @elastic/kibana-visualizations +/x-pack/test/functional/apps/lens @elastic/kibana-visualizations +/x-pack/test/api_integration/apis/lens/ @elastic/kibana-visualizations +/test/functional/apps/visualize/ @elastic/kibana-visualizations +/x-pack/test/functional/apps/graph @elastic/kibana-visualizations +/test/api_integration/apis/event_annotations @elastic/kibana-visualizations +/x-pack/test_serverless/functional/test_suites/common/visualizations/ @elastic/kibana-visualizations +/x-pack/test_serverless/functional/fixtures/kbn_archiver/lens/ @elastic/kibana-visualizations +packages/kbn-monaco/src/esql @elastic/kibana-esql + +# Global Experience + +### Global Experience Reporting +/x-pack/test/functional/apps/dashboard/reporting/ @elastic/appex-sharedux +/x-pack/test/functional/apps/reporting/ @elastic/appex-sharedux +/x-pack/test/functional/apps/reporting_management/ @elastic/appex-sharedux +/x-pack/test/examples/screenshotting/ @elastic/appex-sharedux +/x-pack/test/functional/es_archives/lens/reporting/ @elastic/appex-sharedux +/x-pack/test/functional/es_archives/reporting/ @elastic/appex-sharedux +/x-pack/test/functional/fixtures/kbn_archiver/reporting/ @elastic/appex-sharedux +/x-pack/test/reporting_api_integration/ @elastic/appex-sharedux +/x-pack/test/reporting_functional/ @elastic/appex-sharedux +/x-pack/test/stack_functional_integration/apps/reporting/ @elastic/appex-sharedux +/docs/user/reporting @elastic/appex-sharedux +/docs/settings/reporting-settings.asciidoc @elastic/appex-sharedux +/docs/setup/configuring-reporting.asciidoc @elastic/appex-sharedux +/x-pack/test_serverless/**/test_suites/common/reporting/ @elastic/appex-sharedux +/x-pack/test/accessibility/apps/group3/reporting.ts @elastic/appex-sharedux + +### Global Experience Tagging +/x-pack/test/saved_object_tagging/ @elastic/appex-sharedux + +### Kibana React (to be deprecated) +/src/plugins/kibana_react/public/@elastic/appex-sharedux @elastic/kibana-presentation + +### Home Plugin and Packages +/src/plugins/home/public @elastic/appex-sharedux +/src/plugins/home/server/*.ts @elastic/appex-sharedux +/src/plugins/home/server/services/ @elastic/appex-sharedux + +### Code Coverage +#CC# /src/plugins/home/public @elastic/appex-sharedux +#CC# /src/plugins/home/server/services/ @elastic/appex-sharedux +#CC# /src/plugins/home/ @elastic/appex-sharedux +#CC# /x-pack/plugins/reporting/ @elastic/appex-sharedux +#CC# /x-pack/plugins/security_solution_serverless/ @elastic/appex-sharedux + +### Observability Plugins + + +# Observability AI Assistant +x-pack/test/observability_ai_assistant_api_integration @elastic/obs-ai-assistant +x-pack/test/observability_ai_assistant_functional @elastic/obs-ai-assistant +x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai-assistant + +# Infra Monitoring +## This plugin mostly contains the codebase for the infra services, but also includes some code for the Logs UI app. +## To keep @elastic/obs-ux-logs-team as codeowner of the plugin manifest without requiring a review for all the other code changes +## the priority on codeownership will be as follow: +## - infra -> both teams (automatically generated by script) +## - infra/{common,docs,public,server}/{sub-folders}/ -> @elastic/obs-ux-infra_services-team +## - Logs UI code exceptions -> @elastic/obs-ux-logs-team +## This should allow the infra team to work without dependencies on the @elastic/obs-ux-logs-team, which will maintain ownership of the Logs UI code only. + +## infra/{common,docs,public,server}/{sub-folders}/ -> @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/common @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/docs @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/alerting @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/apps @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/common @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/components @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/containers @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/hooks @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/images @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/lib @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/pages @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/services @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/test_utils @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/public/utils @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/server/lib @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/server/routes @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/server/saved_objects @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/server/services @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/server/usage @elastic/obs-ux-infra_services-team +/x-pack/plugins/observability_solution/infra/server/utils @elastic/obs-ux-infra_services-team + +## Logs UI code exceptions -> @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_stream_log_file.ts @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_page.ts @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/common/http_api/log_alerts @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/common/http_api/log_analysis @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/common/log_analysis @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/common/log_search_result @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/common/log_search_summary @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/common/log_text_scale @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/common/performance_tracing.ts @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/common/search_strategies/log_entries @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/docs/state_machines @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/public/apps/logs_app.tsx @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/public/components/log_stream @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/public/components/logging @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/public/containers/logs @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/public/observability_logs @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/public/pages/logs @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/server/lib/log_analysis @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/server/routes/log_alerts @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/server/routes/log_analysis @elastic/obs-ux-logs-team +/x-pack/plugins/observability_solution/infra/server/services/rules @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team +# Infra Monitoring tests +/x-pack/test/api_integration/apis/infra @elastic/obs-ux-infra_services-team +/x-pack/test/functional/apps/infra @elastic/obs-ux-infra_services-team +/x-pack/test/functional/apps/infra/logs @elastic/obs-ux-logs-team + +# Observability UX management team +/x-pack/test/api_integration/apis/slos @elastic/obs-ux-management-team +/x-pack/test/accessibility/apps/group1/uptime.ts @elastic/obs-ux-management-team +/x-pack/test/accessibility/apps/group3/observability.ts @elastic/obs-ux-management-team +/x-pack/packages/observability/alert_details @elastic/obs-ux-management-team +/x-pack/test/observability_functional @elastic/obs-ux-management-team +/x-pack/plugins/observability_solution/infra/public/alerting @elastic/obs-ux-management-team +/x-pack/plugins/observability_solution/infra/server/lib/alerting @elastic/obs-ux-management-team +/x-pack/test_serverless/**/test_suites/observability/custom_threshold_rule/ @elastic/obs-ux-management-team +/x-pack/test_serverless/**/test_suites/observability/slos/ @elastic/obs-ux-management-team +/x-pack/test_serverless/api_integration/test_suites/observability/es_query_rule @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/burn_rate_rule @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/services/alerting_api @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/services/slo_api @elastic/obs-ux-management-team +/x-pack/test_serverless/**/test_suites/observability/infra/ @elastic/obs-ux-infra_services-team + +# Elastic Stack Monitoring +/x-pack/test/functional/apps/monitoring @elastic/stack-monitoring +/x-pack/test/api_integration/apis/monitoring @elastic/stack-monitoring +/x-pack/test/api_integration/apis/monitoring_collection @elastic/stack-monitoring +/x-pack/test/accessibility/apps/group1/kibana_overview.ts @elastic/stack-monitoring +/x-pack/test/accessibility/apps/group3/stack_monitoring.ts @elastic/stack-monitoring + +# Fleet +/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 @elastic/fleet @elastic/kibana-operations +/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @elastic/fleet @elastic/obs-cloudnative-monitoring +/x-pack/test_serverless/**/test_suites/**/fleet/ @elastic/fleet + +# APM +/x-pack/test/functional/apps/apm/ @elastic/obs-ux-infra_services-team +/x-pack/test/apm_api_integration/ @elastic/obs-ux-infra_services-team +/src/apm.js @elastic/kibana-core @vigneshshanmugam +/packages/kbn-utility-types/src/dot.ts @dgieselaar +/packages/kbn-utility-types/src/dot_test.ts @dgieselaar +/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/ @elastic/obs-ux-infra_services-team +#CC# /src/plugins/apm_oss/ @elastic/apm-ui +#CC# /x-pack/plugins/observability_solution/observability/ @elastic/apm-ui + +# Uptime +/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/ @elastic/obs-ux-management-team +/x-pack/test/functional/apps/uptime @elastic/obs-ux-management-team +/x-pack/test/functional/es_archives/uptime @elastic/obs-ux-management-team +/x-pack/test/functional/services/uptime @elastic/obs-ux-management-team +/x-pack/test/api_integration/apis/uptime @elastic/obs-ux-management-team +/x-pack/test/api_integration/apis/synthetics @elastic/obs-ux-management-team +/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts @elastic/obs-ux-management-team +/x-pack/test/alerting_api_integration/observability/index.ts @elastic/obs-ux-management-team +/x-pack/test_serverless/api_integration/test_suites/observability/synthetics @elastic/obs-ux-management-team + +# obs-ux-logs-team +/x-pack/test_serverless/api_integration/test_suites/observability/index.feature_flags.ts @elastic/obs-ux-logs-team +/x-pack/test/api_integration/apis/logs_ui @elastic/obs-ux-logs-team +/x-pack/test/dataset_quality_api_integration @elastic/obs-ux-logs-team +/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration @elastic/obs-ux-logs-team +/x-pack/test/functional/apps/observability_logs_explorer @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer @elastic/obs-ux-logs-team +/x-pack/test/functional/apps/dataset_quality @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/test_suites/observability/dataset_quality @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/test_suites/observability/ @elastic/obs-ux-logs-team +/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview @elastic/obs-ux-logs-team +/x-pack/test/api_integration/apis/logs_shared @elastic/obs-ux-logs-team + +# Observability onboarding tour +/x-pack/plugins/observability_solution/observability_shared/public/components/tour @elastic/appex-sharedux +/x-pack/test/functional/apps/infra/tour.ts @elastic/appex-sharedux + +# Observability settings +/x-pack/plugins/observability_solution/observability/server/ui_settings.ts @elastic/obs-docs + +### END Observability Plugins + +# Presentation +/x-pack/test/functional/apps/dashboard @elastic/kibana-presentation +/x-pack/test/accessibility/apps/group3/maps.ts @elastic/kibana-presentation +/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts @elastic/kibana-presentation +/x-pack/test/accessibility/apps/group1/dashboard_links.ts @elastic/kibana-presentation +/x-pack/test/accessibility/apps/group1/dashboard_controls.ts @elastic/kibana-presentation +/test/functional/apps/dashboard/ @elastic/kibana-presentation +/test/functional/apps/dashboard_elements/ @elastic/kibana-presentation +/test/functional/services/dashboard/ @elastic/kibana-presentation +/x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation +/x-pack/test_serverless/functional/test_suites/search/dashboards/ @elastic/kibana-presentation +/test/plugin_functional/test_suites/panel_actions @elastic/kibana-presentation +/x-pack/test/functional/es_archives/canvas/logstash_lens @elastic/kibana-presentation +#CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation + +# Machine Learning +/x-pack/test/api_integration/apis/file_upload @elastic/ml-ui +/x-pack/test/accessibility/apps/group2/ml.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts @elastic/ml-ui +/x-pack/test/api_integration/apis/ml/ @elastic/ml-ui +/x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui +/x-pack/test/functional/apps/ml/ @elastic/ml-ui +/x-pack/test/functional/es_archives/ml/ @elastic/ml-ui +/x-pack/test/functional/services/ml/ @elastic/ml-ui +/x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui +/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/ml/ @elastic/ml-ui +/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/ @elastic/ml-ui +/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/ @elastic/ml-ui +/x-pack/test/screenshot_creation/apps/ml_docs @elastic/ml-ui +/x-pack/test/screenshot_creation/services/ml_screenshots.ts @elastic/ml-ui +/x-pack/test_serverless/**/test_suites/**/ml/ @elastic/ml-ui +/x-pack/test_serverless/**/test_suites/common/management/transforms/ @elastic/ml-ui + +# Additional plugins and packages maintained by the ML team. +/x-pack/test/accessibility/apps/group2/transform.ts @elastic/ml-ui +/x-pack/test/api_integration/apis/aiops/ @elastic/ml-ui +/x-pack/test/api_integration/apis/transform/ @elastic/ml-ui +/x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui +/x-pack/test/functional/apps/transform/ @elastic/ml-ui +/x-pack/test/functional/services/transform/ @elastic/ml-ui +/x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui + +# Maps +#CC# /x-pack/plugins/maps/ @elastic/kibana-gis +/x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis +/x-pack/test/functional/apps/maps/ @elastic/kibana-gis +/x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis +/x-pack/plugins/stack_alerts/server/rule_types/geo_containment @elastic/kibana-gis +/x-pack/plugins/stack_alerts/public/rule_types/geo_containment @elastic/kibana-gis +#CC# /x-pack/plugins/file_upload @elastic/kibana-gis + +# Operations +/src/dev/license_checker/config.ts @elastic/kibana-operations +/src/dev/ @elastic/kibana-operations +/src/setup_node_env/ @elastic/kibana-operations +/src/cli/keystore/ @elastic/kibana-operations +/src/cli/serve/ @elastic/kibana-operations +/src/cli_keystore/ @elastic/kibana-operations +/.github/workflows/ @elastic/kibana-operations +/vars/ @elastic/kibana-operations +/.bazelignore @elastic/kibana-operations +/.bazeliskversion @elastic/kibana-operations +/.bazelrc @elastic/kibana-operations +/.bazelrc.common @elastic/kibana-operations +/.bazelversion @elastic/kibana-operations +/WORKSPACE.bazel @elastic/kibana-operations +/.buildkite/ @elastic/kibana-operations +/.buildkite/scripts/steps/esql_grammar_sync.sh @elastic/kibana-esql +/.buildkite/scripts/steps/esql_generate_function_metadata.sh @elastic/kibana-esql +/.buildkite/pipelines/esql_grammar_sync.yml @elastic/kibana-esql +/.buildkite/scripts/steps/code_generation/security_solution_codegen.sh @elastic/security-detection-rule-management +/kbn_pm/ @elastic/kibana-operations +/x-pack/dev-tools @elastic/kibana-operations +/catalog-info.yaml @elastic/kibana-operations @elastic/kibana-tech-leads +/.devcontainer/ @elastic/kibana-operations +/.eslintrc.js @elastic/kibana-operations +/.eslintignore @elastic/kibana-operations + +# Appex QA +/x-pack/test/functional/config.*.* @elastic/appex-qa +/x-pack/test/api_integration/ftr_provider_context.d.ts @elastic/appex-qa # Maybe this should be a glob? +/x-pack/test/accessibility/services.ts @elastic/appex-qa +/x-pack/test/accessibility/page_objects.ts @elastic/appex-qa +/x-pack/test/accessibility/ftr_provider_context.d.ts @elastic/appex-qa +/x-pack/test_serverless/tsconfig.json @elastic/appex-qa +/x-pack/test_serverless/kibana.jsonc @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/common/README.md @elastic/appex-qa +/x-pack/test_serverless/functional/page_objects/index.ts @elastic/appex-qa +/x-pack/test_serverless/functional/ftr_provider_context.d.ts @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/common/management/index.ts @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/common/examples/index.ts @elastic/appex-qa +/x-pack/test_serverless/functional/page_objects/svl_common_page.ts @elastic/appex-qa +/x-pack/test_serverless/README.md @elastic/appex-qa +/x-pack/test_serverless/api_integration/ftr_provider_context.d.ts @elastic/appex-qa +/x-pack/test_serverless/api_integration/test_suites/common/README.md @elastic/appex-qa +/src/dev/code_coverage @elastic/appex-qa +/test/functional/services/common @elastic/appex-qa +/test/functional/services/lib @elastic/appex-qa +/test/functional/services/remote @elastic/appex-qa +/test/visual_regression @elastic/appex-qa +/x-pack/test/visual_regression @elastic/appex-qa +/packages/kbn-test/src/functional_test_runner @elastic/appex-qa +/packages/kbn-performance-testing-dataset-extractor @elastic/appex-qa +/x-pack/test_serverless/**/*config.base.ts @elastic/appex-qa +/x-pack/test_serverless/**/deployment_agnostic_services.ts @elastic/appex-qa +/x-pack/test_serverless/shared/ @elastic/appex-qa +/x-pack/test_serverless/**/test_suites/**/common_configs/ @elastic/appex-qa +/x-pack/test_serverless/api_integration/test_suites/common/elasticsearch_api @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/security/ftr/ @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/common/home_page/ @elastic/appex-qa +/x-pack/test_serverless/**/services/ @elastic/appex-qa +/packages/kbn-es/src/stateful_resources/roles.yml @elastic/appex-qa +x-pack/test/api_integration/deployment_agnostic/default_configs/ @elastic/appex-qa +x-pack/test/api_integration/deployment_agnostic/services/ @elastic/appex-qa +x-pack/test/**/deployment_agnostic/ @elastic/appex-qa #temporarily to monitor tests migration + +# Core +/x-pack/test/api_integration/apis/telemetry @elastic/kibana-core +/x-pack/test/api_integration/apis/status @elastic/kibana-core +/x-pack/test/api_integration/apis/stats @elastic/kibana-core +/x-pack/test/api_integration/apis/kibana/stats @elastic/kibana-core +/x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts @elastic/kibana-core +/config/ @elastic/kibana-core +/config/serverless.yml @elastic/kibana-core @elastic/kibana-security +/config/serverless.es.yml @elastic/kibana-core @elastic/kibana-security +/config/serverless.oblt.yml @elastic/kibana-core @elastic/kibana-security +/config/serverless.security.yml @elastic/kibana-core @elastic/kibana-security +/typings/ @elastic/kibana-core +/test/analytics @elastic/kibana-core +/packages/kbn-test/src/jest/setup/mocks.kbn_i18n_react.js @elastic/kibana-core +/x-pack/test/saved_objects_field_count/ @elastic/kibana-core +/x-pack/test_serverless/**/test_suites/common/saved_objects_management/ @elastic/kibana-core +/x-pack/test_serverless/api_integration/test_suites/common/core/ @elastic/kibana-core +/x-pack/test_serverless/api_integration/test_suites/**/telemetry/ @elastic/kibana-core +/x-pack/test/functional/es_archives/cases/migrations/8.8.0 @elastic/response-ops + +#CC# /src/core/server/csp/ @elastic/kibana-core +#CC# /src/plugins/saved_objects/ @elastic/kibana-core +#CC# /x-pack/plugins/cloud/ @elastic/kibana-core +#CC# /x-pack/plugins/features/ @elastic/kibana-core +#CC# /x-pack/plugins/global_search/ @elastic/kibana-core +#CC# /src/plugins/newsfeed @elastic/kibana-core +#CC# /x-pack/plugins/global_search_providers/ @elastic/kibana-core + +# AppEx AI Infra +/x-pack/plugins/inference @elastic/appex-ai-infra @elastic/obs-ai-assistant @elastic/security-generative-ai + +# AppEx Platform Services Security +//x-pack/test_serverless/api_integration/test_suites/common/security_response_headers.ts @elastic/kibana-security +/x-pack/test/api_integration/apis/es @elastic/kibana-security + +/x-pack/test/api_integration/apis/features @elastic/kibana-security + +# Kibana Telemetry +/.telemetryrc.json @elastic/kibana-core +/x-pack/.telemetryrc.json @elastic/kibana-core +/src/plugins/telemetry/schema/ @elastic/kibana-core +/x-pack/plugins/telemetry_collection_xpack/schema/ @elastic/kibana-core +x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kibana-core @shahinakmal + +# Kibana Localization +/src/dev/i18n_tools/ @elastic/kibana-localization @elastic/kibana-core +/src/core/public/i18n/ @elastic/kibana-localization @elastic/kibana-core +#CC# /x-pack/plugins/translations/ @elastic/kibana-localization @elastic/kibana-core + +# Kibana Platform Security +/.github/codeql @elastic/kibana-security +/.github/workflows/codeql.yml @elastic/kibana-security +/.github/workflows/codeql-stats.yml @elastic/kibana-security +/src/dev/eslint/security_eslint_rule_tests.ts @elastic/kibana-security +/src/core/server/integration_tests/config/check_dynamic_config.test.ts @elastic/kibana-security +/src/plugins/telemetry/server/config/telemetry_labels.ts @elastic/kibana-security +/packages/kbn-std/src/is_internal_url.test.ts @elastic/kibana-core @elastic/kibana-security +/packages/kbn-std/src/is_internal_url.ts @elastic/kibana-core @elastic/kibana-security +/packages/kbn-std/src/parse_next_url.test.ts @elastic/kibana-core @elastic/kibana-security +/packages/kbn-std/src/parse_next_url.ts @elastic/kibana-core @elastic/kibana-security +/test/interactive_setup_api_integration/ @elastic/kibana-security +/test/interactive_setup_functional/ @elastic/kibana-security +/test/plugin_functional/plugins/hardening @elastic/kibana-security +/test/plugin_functional/test_suites/core_plugins/rendering.ts @elastic/kibana-security +/test/plugin_functional/test_suites/hardening @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/login_page.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/roles.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/spaces.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/users.ts @elastic/kibana-security +/x-pack/test/api_integration/apis/security/ @elastic/kibana-security +/x-pack/test/api_integration/apis/spaces/ @elastic/kibana-security +/x-pack/test/ui_capabilities/ @elastic/kibana-security +/x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security +/x-pack/test/functional/apps/security/ @elastic/kibana-security +/x-pack/test/functional/apps/spaces/ @elastic/kibana-security +/x-pack/test/security_api_integration/ @elastic/kibana-security +/x-pack/test/security_functional/ @elastic/kibana-security +/x-pack/test/spaces_api_integration/ @elastic/kibana-security +/x-pack/test/saved_object_api_integration/ @elastic/kibana-security +/x-pack/test_serverless/**/test_suites/common/platform_security/ @elastic/kibana-security +/x-pack/test_serverless/**/test_suites/search/platform_security/ @elastic/kibana-security +/x-pack/test_serverless/**/test_suites/security/platform_security/ @elastic/kibana-security +/x-pack/test_serverless/**/test_suites/observability/platform_security/ @elastic/kibana-security +/packages/core/http/core-http-server-internal/src/cdn_config/ @elastic/kibana-security @elastic/kibana-core +#CC# /x-pack/plugins/security/ @elastic/kibana-security + +# Response Ops team +/x-pack/test/accessibility/apps/group3/rules_connectors.ts @elastic/response-ops +/x-pack/test/functional/es_archives/cases/default @elastic/response-ops +/x-pack/test_serverless/api_integration/test_suites/observability/config.ts @elastic/response-ops +/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @elastic/response-ops +/x-pack/test_serverless/functional/page_objects/svl_triggers_actions_ui_page.ts @elastic/response-ops +/x-pack/test_serverless/functional/page_objects/svl_rule_details_ui_page.ts @elastic/response-ops +/x-pack/test_serverless/functional/page_objects/svl_oblt_overview_page.ts @elastic/response-ops +/x-pack/test/alerting_api_integration/ @elastic/response-ops +/x-pack/test/alerting_api_integration/observability @elastic/obs-ux-management-team +/x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/response-ops +/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/response-ops +/x-pack/test/task_manager_claimer_mget/ @elastic/response-ops +/docs/user/alerting/ @elastic/response-ops +/docs/management/connectors/ @elastic/response-ops +/x-pack/test/cases_api_integration/ @elastic/response-ops +/x-pack/test/functional/services/cases/ @elastic/response-ops +/x-pack/test/functional_with_es_ssl/apps/cases/ @elastic/response-ops +/x-pack/test/api_integration/apis/cases/ @elastic/response-ops +/x-pack/test_serverless/functional/test_suites/observability/cases @elastic/response-ops +/x-pack/test_serverless/functional/test_suites/search/cases/ @elastic/response-ops +/x-pack/test_serverless/functional/test_suites/security/ftr/cases/ @elastic/response-ops +/x-pack/test_serverless/api_integration/test_suites/search/cases/ @elastic/response-ops +/x-pack/test_serverless/api_integration/test_suites/observability/cases/ @elastic/response-ops +/x-pack/test_serverless/api_integration/test_suites/security/cases/ @elastic/response-ops +/x-pack/test_serverless/functional/test_suites/search/screenshot_creation/response_ops_docs @elastic/response-ops +/x-pack/test_serverless/functional/test_suites/security/screenshot_creation/response_ops_docs @elastic/response-ops +/x-pack/test_serverless/functional/test_suites/observability/screenshot_creation/response_ops_docs @elastic/response-ops +/x-pack/test_serverless/api_integration/test_suites/common/alerting/ @elastic/response-ops +/x-pack/test/functional/es_archives/action_task_params @elastic/response-ops +/x-pack/test/functional/es_archives/actions @elastic/response-ops +/x-pack/test/functional/es_archives/alerting @elastic/response-ops +/x-pack/test/functional/es_archives/alerts @elastic/response-ops +/x-pack/test/functional/es_archives/alerts_legacy @elastic/response-ops +/x-pack/test/functional/es_archives/observability/alerts @elastic/response-ops +/x-pack/test/functional/es_archives/actions @elastic/response-ops +/x-pack/test/functional/es_archives/rules_scheduled_task_id @elastic/response-ops +/x-pack/test/functional/es_archives/alerting/8_2_0 @elastic/response-ops +/x-pack/test/functional/es_archives/cases/signals/default @elastic/response-ops +/x-pack/test/functional/es_archives/cases/signals/hosts_users @elastic/response-ops + +# Enterprise Search +/x-pack/test_serverless/functional/page_objects/svl_ingest_pipelines.ts @elastic/search-kibana +/x-pack/test/functional/apps/dev_tools/embedded_console.ts @elastic/search-kibana +/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @elastic/search-kibana +/x-pack/test/functional/page_objects/embedded_console.ts @elastic/search-kibana +/x-pack/test/functional_enterprise_search/ @elastic/search-kibana +/x-pack/plugins/enterprise_search/public/applications/shared/doc_links @elastic/platform-docs +/x-pack/test_serverless/api_integration/test_suites/search/serverless_search @elastic/search-kibana +/x-pack/test_serverless/functional/test_suites/search/ @elastic/search-kibana +/x-pack/test_serverless/functional/test_suites/search/config.ts @elastic/search-kibana @elastic/appex-qa +x-pack/test/api_integration/apis/management/index_management/inference_endpoints.ts @elastic/search-kibana +/x-pack/test_serverless/api_integration/test_suites/search @elastic/search-kibana +/x-pack/test_serverless/functional/page_objects/svl_api_keys.ts @elastic/search-kibana +/x-pack/test_serverless/functional/page_objects/svl_search_* @elastic/search-kibana +/x-pack/test/functional_search/ @elastic/search-kibana + +# Management Experience - Deployment Management +/x-pack/test/api_integration/services/index_management.ts @elastic/kibana-management +/x-pack/test/functional/services/grok_debugger.js @elastic/kibana-management +/x-pack/test/functional/apps/grok_debugger @elastic/kibana-management +/x-pack/test/functional/apps/index_lifecycle_management @elastic/kibana-management +/x-pack/test/functional/apps/index_management @elastic/kibana-management +/x-pack/test/api_integration/services/ingest_pipelines @elastic/kibana-management +/x-pack/test/functional/apps/watcher @elastic/kibana-management +/x-pack/test/api_integration/apis/watcher @elastic/kibana-management +/x-pack/test/api_integration/apis/upgrade_assistant @elastic/kibana-management +/x-pack/test/api_integration/apis/searchprofiler @elastic/kibana-management +/x-pack/test/api_integration/apis/console @elastic/kibana-management +/x-pack/test_serverless/**/test_suites/common/index_management/ @elastic/kibana-management +/x-pack/test_serverless/**/test_suites/common/management/index_management/ @elastic/kibana-management +/x-pack/test_serverless/**/test_suites/common/painless_lab/ @elastic/kibana-management +/x-pack/test_serverless/**/test_suites/common/console/ @elastic/kibana-management +/x-pack/test_serverless/api_integration/test_suites/common/management/ @elastic/kibana-management +/x-pack/test_serverless/api_integration/test_suites/common/search_profiler/ @elastic/kibana-management +/x-pack/test_serverless/functional/test_suites/**/advanced_settings.ts @elastic/kibana-management +/x-pack/test_serverless/functional/test_suites/common/management/disabled_uis.ts @elastic/kibana-management +/x-pack/test_serverless/functional/test_suites/common/management/ingest_pipelines.ts @elastic/kibana-management +/x-pack/test_serverless/functional/test_suites/common/management/landing_page.ts @elastic/kibana-management +/x-pack/test_serverless/functional/test_suites/common/dev_tools/ @elastic/kibana-management +/x-pack/test_serverless/**/test_suites/common/grok_debugger/ @elastic/kibana-management +/x-pack/test/api_integration/apis/management/ @elastic/kibana-management +/x-pack/test/functional/apps/rollup_job/ @elastic/kibana-management +/x-pack/test/api_integration/apis/grok_debugger @elastic/kibana-management +/x-pack/test/accessibility/apps/group1/advanced_settings.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/**/grok_debugger.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group1/helpers.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group1/home.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group1/index_lifecycle_management.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group1/ingest_node_pipelines.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group1/management.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group1/painless_lab.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group1/search_profiler.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group3/cross_cluster_replication.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group3/license_management.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group3/remote_clusters.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group3/rollup_jobs.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts @elastic/kibana-management +/x-pack/test/accessibility/apps/group3/watcher.ts @elastic/kibana-management + +#CC# /x-pack/plugins/cross_cluster_replication/ @elastic/kibana-management + +# Security Solution +/x-pack/test/common/services/security_solution @elastic/security-solution +/x-pack/test/api_integration/services/security_solution_*.gen.ts @elastic/security-solution +/x-pack/test/accessibility/apps/group3/security_solution.ts @elastic/security-solution +/x-pack/test_serverless/functional/test_suites/security/config.ts @elastic/security-solution @elastic/appex-qa +/x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts @elastic/security-solution +/x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts @elastic/security-solution +/x-pack/test_serverless/functional/test_suites/common/spaces/multiple_spaces_enabled.ts @elastic/security-solution +/x-pack/test/functional/es_archives/endpoint/ @elastic/security-solution +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution +/x-pack/test/security_solution_api_integration @elastic/security-solution +/x-pack/test/api_integration/apis/security_solution @elastic/security-solution +/x-pack/test/functional/es_archives/auditbeat/default @elastic/security-solution +/x-pack/test/functional/es_archives/auditbeat/hosts @elastic/security-solution +/x-pack/test_serverless/functional/page_objects/svl_management_page.ts @elastic/security-solution +/x-pack/test_serverless/api_integration/test_suites/security @elastic/security-solution + +/x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts @elastic/security-solution +/x-pack/test_serverless/functional/test_suites/security/index.ts @elastic/security-solution +#CC# /x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/test/functional/es_archives/cases/signals/duplicate_ids @elastic/response-ops + +# Security Solution OpenAPI bundles +/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_* @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_* @elastic/security-defend-workflows +/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_* @elastic/security-entity-analytics +/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_* @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_* @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_* @elastic/security-defend-workflows +/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_* @elastic/security-entity-analytics +/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_* @elastic/security-threat-hunting-investigations + +# Security Solution Offering plugins +# TODO: assign sub directories to sub teams +/x-pack/plugins/security_solution_ess/ @elastic/security-solution +/x-pack/plugins/security_solution_serverless/ @elastic/security-solution + +# GenAI in Security Solution +/x-pack/plugins/security_solution/public/assistant @elastic/security-generative-ai +/x-pack/plugins/security_solution/public/attack_discovery @elastic/security-generative-ai +/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant @elastic/security-generative-ai + +# Security Solution cross teams ownership +/x-pack/test/security_solution_cypress/cypress/fixtures @elastic/security-detections-response @elastic/security-threat-hunting +/x-pack/test/security_solution_cypress/cypress/helpers @elastic/security-detections-response @elastic/security-threat-hunting +/x-pack/test/security_solution_cypress/cypress/objects @elastic/security-detections-response @elastic/security-threat-hunting +/x-pack/test/security_solution_cypress/cypress/plugins @elastic/security-detections-response @elastic/security-threat-hunting +/x-pack/test/security_solution_cypress/cypress/screens/common @elastic/security-detections-response @elastic/security-threat-hunting +/x-pack/test/security_solution_cypress/cypress/support @elastic/security-detections-response @elastic/security-threat-hunting +/x-pack/test/security_solution_cypress/cypress/urls @elastic/security-threat-hunting-investigations @elastic/security-detection-engine + +/x-pack/plugins/security_solution/common/ecs @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/common/test @elastic/security-detections-response @elastic/security-threat-hunting + +/x-pack/plugins/security_solution/public/common/components/callouts @elastic/security-detections-response +/x-pack/plugins/security_solution/public/common/components/hover_actions @elastic/security-threat-hunting-explore @elastic/security-threat-hunting-investigations + +/x-pack/plugins/security_solution/server/routes @elastic/security-detections-response @elastic/security-threat-hunting +/x-pack/plugins/security_solution/server/utils @elastic/security-detections-response @elastic/security-threat-hunting +x-pack/test/security_solution_api_integration/test_suites/detections_response/utils @elastic/security-detections-response +x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry @elastic/security-detections-response +x-pack/test/security_solution_api_integration/test_suites/detections_response/user_roles @elastic/security-detections-response +x-pack/test/security_solution_api_integration/test_suites/explore @elastic/security-threat-hunting-explore +x-pack/test/security_solution_api_integration/test_suites/investigations @elastic/security-threat-hunting-investigations +x-pack/test/security_solution_api_integration/test_suites/sources @elastic/security-detections-response +/x-pack/test/common/utils/security_solution/detections_response @elastic/security-detections-response + +# Security Solution sub teams + +## Security Solution sub teams - security-engineering-productivity +## NOTE: It's important to keep this above other teams' sections because test automation doesn't process +## the CODEOWNERS file correctly. See https://github.com/elastic/kibana/issues/173307#issuecomment-1855858929 +/x-pack/test/security_solution_cypress/* @elastic/security-engineering-productivity +/x-pack/test/security_solution_cypress/cypress/* @elastic/security-engineering-productivity +/x-pack/test/security_solution_cypress/cypress/tasks/login.ts @elastic/security-engineering-productivity +/x-pack/test/security_solution_cypress/es_archives @elastic/security-engineering-productivity +/x-pack/test/security_solution_playwright @elastic/security-engineering-productivity +/x-pack/plugins/security_solution/scripts/run_cypress @MadameSheema @patrykkopycinski @maximpn @banderror + +## Security Solution sub teams - Threat Hunting + +/x-pack/plugins/security_solution/server/lib/siem_migrations @elastic/security-threat-hunting +/x-pack/plugins/security_solution/common/siem_migrations @elastic/security-threat-hunting + +## Security Solution Threat Hunting areas - Threat Hunting Investigations + +/x-pack/plugins/security_solution/common/api/timeline @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/common/search_strategy/timeline @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/common/types/timeline @elastic/security-threat-hunting-investigations + +/x-pack/test/security_solution_cypress/cypress/e2e/investigations @elastic/security-threat-hunting-investigations +/x-pack/test/security_solution_cypress/cypress/e2e/sourcerer/sourcerer_timeline.cy.ts @elastic/security-threat-hunting-investigations + +x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout @elastic/security-threat-hunting-investigations +x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/security-threat-hunting-investigations + +/x-pack/plugins/security_solution/common/timelines @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/common/components/alerts_viewer @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_action @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/common/components/event_details @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/common/components/events_viewer @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/common/components/markdown_editor @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/detections/components/alerts_kpis @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/detections/components/alerts_table @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/detections/components/alerts_info @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/flyout/document_details @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/flyout/shared @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/notes @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/resolver @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/threat_intelligence @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/timelines @elastic/security-threat-hunting-investigations + +/x-pack/plugins/security_solution/server/lib/timeline @elastic/security-threat-hunting-investigations + +## Security Solution Threat Hunting areas - Threat Hunting Explore +/x-pack/plugins/security_solution/common/api/tags @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/common/search_strategy/security_solution/network @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/common/search_strategy/security_solution/user @elastic/security-threat-hunting-explore + +/x-pack/test/security_solution_cypress/cypress/e2e/explore @elastic/security-threat-hunting-explore +/x-pack/test/security_solution_cypress/cypress/screens/hosts @elastic/security-threat-hunting-explore +/x-pack/test/security_solution_cypress/cypress/screens/network @elastic/security-threat-hunting-explore +/x-pack/test/security_solution_cypress/cypress/tasks/hosts @elastic/security-threat-hunting-explore +/x-pack/test/security_solution_cypress/cypress/tasks/network @elastic/security-threat-hunting-explore + +/x-pack/plugins/security_solution/public/app/actions @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/charts @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/header_page @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/header_section @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/inspect @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/last_event_time @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/links @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/matrix_histogram @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/navigation @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/news_feed @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/overview_description_list @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/page @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/sidebar_header @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/tables @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/top_n @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/components/with_hover_actions @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/containers/matrix_histogram @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/common/lib/cell_actions @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/cases @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/explore @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/overview @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/public/dashboards @elastic/security-threat-hunting-explore + +/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network @elastic/security-threat-hunting-explore +/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users @elastic/security-threat-hunting-explore + +/x-pack/test/functional/es_archives/auditbeat/overview @elastic/security-threat-hunting-explore +/x-pack/test/functional/es_archives/auditbeat/users @elastic/security-threat-hunting-explore + +/x-pack/test/functional/es_archives/auditbeat/uncommon_processes @elastic/security-threat-hunting-explore + +## Generative AI owner connectors +# OpenAI +/x-pack/plugins/stack_connectors/public/connector_types/openai @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra +/x-pack/plugins/stack_connectors/server/connector_types/openai @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra +/x-pack/plugins/stack_connectors/common/openai @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra +# Bedrock +/x-pack/plugins/stack_connectors/public/connector_types/bedrock @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra +/x-pack/plugins/stack_connectors/server/connector_types/bedrock @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra +/x-pack/plugins/stack_connectors/common/bedrock @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra + +# Gemini +/x-pack/plugins/stack_connectors/public/connector_types/gemini @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra +/x-pack/plugins/stack_connectors/server/connector_types/gemini @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra +/x-pack/plugins/stack_connectors/common/gemini @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra + +# Inference API +/x-pack/plugins/stack_connectors/public/connector_types/inference @elastic/appex-ai-infra @elastic/security-generative-ai @elastic/obs-ai-assistant +/x-pack/plugins/stack_connectors/server/connector_types/inference @elastic/appex-ai-infra @elastic/security-generative-ai @elastic/obs-ai-assistant +/x-pack/plugins/stack_connectors/common/inference @elastic/appex-ai-infra @elastic/security-generative-ai @elastic/obs-ai-assistant + +## Defend Workflows owner connectors +/x-pack/plugins/stack_connectors/public/connector_types/sentinelone @elastic/security-defend-workflows +/x-pack/plugins/stack_connectors/server/connector_types/sentinelone @elastic/security-defend-workflows +/x-pack/plugins/stack_connectors/common/sentinelone @elastic/security-defend-workflows +/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike @elastic/security-defend-workflows +/x-pack/plugins/stack_connectors/common/crowdstrike @elastic/security-defend-workflows + +## Security Solution shared OAS schemas +/x-pack/plugins/security_solution/common/api/model @elastic/security-detection-rule-management @elastic/security-detection-engine + +## Security Solution sub teams - Detection Rule Management +/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/common/api/detection_engine/rule_management @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/common/detection_engine/rule_management @elastic/security-detection-rule-management + +/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/docs/rfcs/detection_response @elastic/security-detection-rule-management @elastic/security-detection-engine +/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/rule_management @elastic/security-detection-rule-management +/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management @elastic/security-detection-rule-management + +/x-pack/plugins/security_solution/public/common/components/health_truncate_text @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/common/components/links_to_docs @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/common/components/ml_popover @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/common/components/popover_items @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detection_engine/rule_management @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detections/components/callouts @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detections/components/rules @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detections/mitre @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/rules @elastic/security-detection-rule-management + +/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine + +/x-pack/plugins/security_solution/scripts/openapi @elastic/security-detection-rule-management + +## Security Solution sub teams - Detection Engine +/x-pack/plugins/security_solution/common/api/detection_engine/alert_tags @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/index_management @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/signals @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/signals_migration @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/cti @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/field_maps @elastic/security-detection-engine +/x-pack/test/functional/es_archives/entity/risks @elastic/security-detection-engine +/x-pack/test/functional/es_archives/entity/host_risk @elastic/security-detection-engine +/x-pack/test/api_integration/apis/lists @elastic/security-detection-engine + +/x-pack/plugins/security_solution/public/sourcerer @elastic/security-threat-hunting-investigations +/x-pack/plugins/security_solution/public/detection_engine/rule_creation @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/detection_engine/rule_gaps @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/detections/pages/alerts @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/exceptions @elastic/security-detection-engine + +/x-pack/plugins/security_solution/server/lib/detection_engine/migrations @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals @elastic/security-detection-engine + +/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine @elastic/security-detection-engine + +/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine @elastic/security-detection-engine +/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/rule_gaps.ts @elastic/security-detection-engine +/x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists @elastic/security-detection-engine +/x-pack/test/functional/es_archives/asset_criticality @elastic/security-detection-engine + +## Security Threat Intelligence - Under Security Platform +/x-pack/plugins/security_solution/public/common/components/threat_match @elastic/security-detection-engine + +## Security Solution sub teams - security-defend-workflows +/x-pack/test/api_integration/apis/osquery @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/management/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/common/lib/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/common/components/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/common/hooks/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/common/mock/endpoint @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/common/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/common/api/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/lists_integration/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/lib/license/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/fleet_integration/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/scripts/endpoint/ @elastic/security-defend-workflows +/x-pack/test/security_solution_endpoint/ @elastic/security-defend-workflows +/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/ @elastic/security-defend-workflows +/x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management @elastic/security-defend-workflows +/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management @elastic/security-defend-workflows +/x-pack/plugins/security_solution_serverless/server/endpoint @elastic/security-defend-workflows + +## Security Solution sub teams - security-telemetry (Data Engineering) +x-pack/plugins/security_solution/server/usage/ @elastic/security-data-analytics +x-pack/plugins/security_solution/server/lib/telemetry/ @elastic/security-data-analytics + +## Security Solution sub teams - adaptive-workload-protection +x-pack/plugins/security_solution/public/common/components/sessions_viewer @elastic/kibana-cloud-security-posture +x-pack/plugins/security_solution/public/kubernetes @elastic/kibana-cloud-security-posture + +## Security Solution sub teams - Entity Analytics +x-pack/plugins/security_solution/common/entity_analytics @elastic/security-entity-analytics +x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score @elastic/security-entity-analytics +x-pack/plugins/security_solution/public/entity_analytics @elastic/security-entity-analytics +x-pack/plugins/security_solution/server/lib/entity_analytics @elastic/security-entity-analytics +x-pack/plugins/security_solution/server/lib/risk_score @elastic/security-entity-analytics +x-pack/test/security_solution_api_integration/test_suites/entity_analytics @elastic/security-entity-analytics +x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics @elastic/security-entity-analytics +x-pack/plugins/security_solution/public/flyout/entity_details @elastic/security-entity-analytics +x-pack/plugins/security_solution/common/api/entity_analytics @elastic/security-entity-analytics + +## Security Solution sub teams - GenAI +x-pack/test/security_solution_api_integration/test_suites/genai @elastic/security-generative-ai + +# Security Defend Workflows - OSQuery Ownership +x-pack/plugins/osquery @elastic/security-defend-workflows +/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_response_actions @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/detections/components/osquery @elastic/security-defend-workflows + +# Cloud Defend +/x-pack/plugins/cloud_defend/ @elastic/kibana-cloud-security-posture +/x-pack/plugins/security_solution/public/cloud_defend @elastic/kibana-cloud-security-posture + +# Cloud Security Posture +/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.* @elastic/kibana-cloud-security-posture +/x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture +/x-pack/test/api_integration/apis/cloud_security_posture/ @elastic/kibana-cloud-security-posture +/x-pack/test/cloud_security_posture_functional/ @elastic/kibana-cloud-security-posture +/x-pack/test/cloud_security_posture_api/ @elastic/kibana-cloud-security-posture +/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/ @elastic/kibana-cloud-security-posture +/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.basic.ts @elastic/kibana-cloud-security-posture +/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.essentials.ts @elastic/kibana-cloud-security-posture +/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/ @elastic/kibana-cloud-security-posture +/x-pack/plugins/fleet/public/components/cloud_security_posture @elastic/fleet @elastic/kibana-cloud-security-posture +/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/cloud_security_posture @elastic/fleet @elastic/kibana-cloud-security-posture +/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.* @elastic/fleet @elastic/kibana-cloud-security-posture +/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/cloud_posture_third_party_support_callout.* @elastic/fleet @elastic/kibana-cloud-security-posture +/x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture +/x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/misconfiguration_contextual_flyout.cy.ts @elastic/kibana-cloud-security-posture +/x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/vulnerabilities_contextual_flyout.cy.ts @elastic/kibana-cloud-security-posture + +# Security Solution onboarding tour +/x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/security-threat-hunting-explore + +# Security Service Integrations +x-pack/plugins/security_solution/common/security_integrations @elastic/security-service-integrations +x-pack/plugins/security_solution/public/security_integrations @elastic/security-service-integrations +x-pack/plugins/security_solution/server/security_integrations @elastic/security-service-integrations +x-pack/plugins/security_solution/server/lib/security_integrations @elastic/security-service-integrations + +# Kibana design +# scss overrides should be below this line for specificity +**/*.scss @elastic/kibana-design + +# Observability design +/x-pack/plugins/fleet/**/*.scss @elastic/observability-design +/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design + +# Ent. Search design +/x-pack/plugins/enterprise_search/**/*.scss @elastic/search-design +/x-pack/test/accessibility/apps/group3/enterprise_search.ts @elastic/search-kibana + +# Security design +/x-pack/plugins/endpoint/**/*.scss @elastic/security-design +/x-pack/plugins/security_solution/**/*.scss @elastic/security-design +/x-pack/plugins/security_solution_ess/**/*.scss @elastic/security-design +/x-pack/plugins/security_solution_serverless/**/*.scss @elastic/security-design + +# Logstash +/x-pack/test/api_integration/apis/logstash @elastic/logstash +#CC# /x-pack/plugins/logstash/ @elastic/logstash + +# EUI team +/src/plugins/kibana_react/public/page_template/ @elastic/eui-team @elastic/appex-sharedux + +# Landing page for guided onboarding in Home plugin +/src/plugins/home/public/application/components/guided_onboarding @elastic/appex-sharedux + +# Changes to translation files should not ping code reviewers +x-pack/plugins/translations/translations + +# Profiling api integration testing +x-pack/test/profiling_api_integration @elastic/obs-ux-infra_services-team + +# Observability shared profiling +x-pack/plugins/observability_solution/observability_shared/public/components/profiling @elastic/obs-ux-infra_services-team + +# Shared UX +/x-pack/test/api_integration/apis/content_management @elastic/appex-sharedux +/x-pack/test/accessibility/apps/group3/tags.ts @elastic/appex-sharedux +/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts @elastic/appex-sharedux +/x-pack/test_serverless/functional/test_suites/common/spaces/spaces_selection.ts @elastic/appex-sharedux +/x-pack/test_serverless/functional/test_suites/common/spaces/index.ts @elastic/appex-sharedux +packages/react @elastic/appex-sharedux +test/functional/page_objects/solution_navigation.ts @elastic/appex-sharedux +/x-pack/test_serverless/functional/page_objects/svl_common_navigation.ts @elastic/appex-sharedux +/x-pack/test_serverless/functional/fixtures/kbn_archiver/reporting @elastic/appex-sharedux +/x-pack/test_serverless/functional/page_objects/svl_sec_landing_page.ts @elastic/appex-sharedux +/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts @elastic/appex-sharedux + +# OpenAPI spec files +oas_docs/.spectral.yaml @elastic/platform-docs +oas_docs/kibana.info.serverless.yaml @elastic/platform-docs +oas_docs/kibana.info.yaml @elastic/platform-docs + +# Plugin manifests +/src/plugins/**/kibana.jsonc @elastic/kibana-core +/x-pack/plugins/**/kibana.jsonc @elastic/kibana-core + +# Temporary Encrypted Saved Objects (ESO) guarding +# This additional code-ownership is meant to be a temporary precaution to notify the Kibana platform security team +# when an encrypted saved object is changed. Very careful review is necessary to ensure any changes are compatible +# with serverless zero downtime upgrades (ZDT). This section should be removed only when proper guidance for +# maintaining ESOs has been documented and consuming teams have acclimated to ZDT changes. +x-pack/plugins/actions/server/saved_objects/index.ts @elastic/response-ops @elastic/kibana-security +x-pack/plugins/alerting/server/saved_objects/index.ts @elastic/response-ops @elastic/kibana-security +x-pack/plugins/fleet/server/saved_objects/index.ts @elastic/fleet @elastic/kibana-security +x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts @elastic/obs-ux-management-team @elastic/kibana-security +x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics_monitor.ts @elastic/obs-ux-management-team @elastic/kibana-security +x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics_param.ts @elastic/obs-ux-management-team @elastic/kibana-security + +# Specialised GitHub workflows for the Observability robots +/.github/workflows/deploy-my-kibana.yml @elastic/observablt-robots @elastic/kibana-operations +/.github/workflows/oblt-github-commands @elastic/observablt-robots @elastic/kibana-operations +/.github/workflows/undeploy-my-kibana.yml @elastic/observablt-robots @elastic/kibana-operations + +#### +## These rules are always last so they take ultimate priority over everything else +#### diff --git a/x-pack/plugins/osquery/cypress/cypress_base.config.ts b/x-pack/plugins/osquery/cypress/cypress_base.config.ts index 820df131c700f..d37ebf246576e 100644 --- a/x-pack/plugins/osquery/cypress/cypress_base.config.ts +++ b/x-pack/plugins/osquery/cypress/cypress_base.config.ts @@ -10,8 +10,8 @@ import path from 'path'; import { safeLoad as loadYaml } from 'js-yaml'; import { readFileSync } from 'fs'; import type { YamlRoleDefinitions } from '@kbn/test-suites-serverless/shared/lib'; -import { setupUserDataLoader } from '@kbn/test-suites-serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks'; import { samlAuthentication } from '@kbn/security-solution-plugin/public/management/cypress/support/saml_authentication'; +import { setupUserDataLoader } from './support/setup_data_loader_tasks'; import { getFailedSpecVideos } from './support/filter_videos'; const ROLES_YAML_FILE_PATH = path.join( diff --git a/x-pack/plugins/osquery/cypress/support/e2e.ts b/x-pack/plugins/osquery/cypress/support/e2e.ts index 3a989aa235575..7426498cd2832 100644 --- a/x-pack/plugins/osquery/cypress/support/e2e.ts +++ b/x-pack/plugins/osquery/cypress/support/e2e.ts @@ -34,11 +34,16 @@ registerCypressGrep(); import type { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/security-solution-plugin/scripts/run_cypress/utils'; import { login } from '@kbn/security-solution-plugin/public/management/cypress/tasks/login'; +import type { LoadedRoleAndUser } from '@kbn/test-suites-serverless/shared/lib'; import type { ServerlessRoleName } from './roles'; import { waitUntil } from '../tasks/wait_until'; import { isCloudServerless, isServerless } from '../tasks/serverless'; +export interface LoadUserAndRoleCyTaskOptions { + name: ServerlessRoleName; +} + declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { @@ -49,6 +54,12 @@ declare global { } interface Chainable { + task( + name: 'loadUserAndRole', + arg: LoadUserAndRoleCyTaskOptions, + options?: Partial + ): Chainable; + getBySel(...args: Parameters): Chainable>; getBySelContains( diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks.ts b/x-pack/plugins/osquery/cypress/support/setup_data_loader_tasks.ts similarity index 77% rename from x-pack/test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks.ts rename to x-pack/plugins/osquery/cypress/support/setup_data_loader_tasks.ts index 65cbcf5aac212..938fa67585f88 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks.ts +++ b/x-pack/plugins/osquery/cypress/support/setup_data_loader_tasks.ts @@ -6,12 +6,12 @@ */ import { createRuntimeServices } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; -import { LoadUserAndRoleCyTaskOptions } from '../cypress'; -import { +import { SecurityRoleAndUserLoader } from '@kbn/test-suites-serverless/shared/lib'; +import type { LoadedRoleAndUser, - SecurityRoleAndUserLoader, YamlRoleDefinitions, -} from '../../../../../shared/lib'; +} from '@kbn/test-suites-serverless/shared/lib'; +import type { LoadUserAndRoleCyTaskOptions } from './e2e'; interface AdditionalDefinitions { roleDefinitions?: YamlRoleDefinitions; @@ -33,9 +33,7 @@ export const setupUserDataLoader = ( }); const roleAndUserLoaderPromise: Promise = stackServicesPromise.then( - ({ kbnClient, log }) => { - return new SecurityRoleAndUserLoader(kbnClient, log, roleDefinitions); - } + ({ kbnClient, log }) => new SecurityRoleAndUserLoader(kbnClient, log, roleDefinitions) ); on('task', { @@ -43,8 +41,7 @@ export const setupUserDataLoader = ( * Loads a user/role into Kibana. Used from `login()` task. * @param name */ - loadUserAndRole: async ({ name }: LoadUserAndRoleCyTaskOptions): Promise => { - return (await roleAndUserLoaderPromise).load(name, additionalRoleName); - }, + loadUserAndRole: async ({ name }: LoadUserAndRoleCyTaskOptions): Promise => + (await roleAndUserLoaderPromise).load(name, additionalRoleName), }); }; diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.base.ts b/x-pack/test/defend_workflows_cypress/serverless_config.base.ts similarity index 93% rename from x-pack/test_serverless/functional/test_suites/security/cypress/security_config.base.ts rename to x-pack/test/defend_workflows_cypress/serverless_config.base.ts index 515ea0c52efee..07d514687e954 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.base.ts +++ b/x-pack/test/defend_workflows_cypress/serverless_config.base.ts @@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const svlSharedConfig = await readConfigFile( - require.resolve('../../../../shared/config.base.ts') + require.resolve('@kbn/test-suites-serverless/shared/config.base') ); return { diff --git a/x-pack/test/defend_workflows_cypress/serverless_config.ts b/x-pack/test/defend_workflows_cypress/serverless_config.ts index 38c9c5040e8d3..c8dde0ebcff5d 100644 --- a/x-pack/test/defend_workflows_cypress/serverless_config.ts +++ b/x-pack/test/defend_workflows_cypress/serverless_config.ts @@ -14,9 +14,7 @@ import { DefendWorkflowsCypressCliTestRunner } from './runner'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const defendWorkflowsCypressConfig = await readConfigFile( - require.resolve( - '@kbn/test-suites-serverless/functional/test_suites/security/cypress/security_config.base' - ) + require.resolve('./serverless_config.base.ts') ); const config = defendWorkflowsCypressConfig.getAll(); const hostIp = getLocalhostRealIp(); diff --git a/x-pack/test/osquery_cypress/serverless_cli_config.ts b/x-pack/test/osquery_cypress/serverless_cli_config.ts index 0ed1be5e332d3..67df33aa34a68 100644 --- a/x-pack/test/osquery_cypress/serverless_cli_config.ts +++ b/x-pack/test/osquery_cypress/serverless_cli_config.ts @@ -12,9 +12,7 @@ import { startOsqueryCypress } from './runner'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const securitySolutionCypressConfig = await readConfigFile( - require.resolve( - '@kbn/test-suites-serverless/functional/test_suites/security/cypress/security_config.base' - ) + require.resolve('./serverless_config.base.ts') ); return { diff --git a/x-pack/test/osquery_cypress/serverless_config.base.ts b/x-pack/test/osquery_cypress/serverless_config.base.ts new file mode 100644 index 0000000000000..07d514687e954 --- /dev/null +++ b/x-pack/test/osquery_cypress/serverless_config.base.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const svlSharedConfig = await readConfigFile( + require.resolve('@kbn/test-suites-serverless/shared/config.base') + ); + + return { + ...svlSharedConfig.getAll(), + esTestCluster: { + ...svlSharedConfig.get('esTestCluster'), + serverArgs: [ + ...svlSharedConfig.get('esTestCluster.serverArgs'), + // define custom es server here + // API Keys is enabled at the top level + ], + }, + kbnTestServer: { + ...svlSharedConfig.get('kbnTestServer'), + serverArgs: [ + ...svlSharedConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--csp.warnLegacyBrowsers=false', + '--serverless=security', + ], + }, + }; +} diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/.eslintrc.json b/x-pack/test_serverless/functional/test_suites/security/cypress/.eslintrc.json deleted file mode 100644 index 22a4d052afdc5..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "plugins": ["cypress"], - "extends": [ - "plugin:cypress/recommended" - ], - "env": { - "cypress/globals": true - }, - "rules": { - "cypress/no-force": "warn", - "import/no-extraneous-dependencies": "off" - } -} diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/.gitignore b/x-pack/test_serverless/functional/test_suites/security/cypress/.gitignore deleted file mode 100644 index c23080c54def2..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -videos -screenshots -downloads \ No newline at end of file diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/README.md b/x-pack/test_serverless/functional/test_suites/security/cypress/README.md deleted file mode 100644 index da13d4e99ce85..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# Security Serverless Tests - -Before considering adding a new Cypress tests, please make sure you have added unit and API tests first and the behaviour can only be exercised with Cypress. - -Note that, the aim of Cypress is to test that the user interface operates as expected, hence, you should not be using this tool to test REST API or data contracts. - -## Folder Structure - -Below you can find the folder structure used on our Cypress tests. - -### e2e/ - -Cypress convention starting version 10 (previously known as integration). Contains the specs that are going to be executed. - -### fixtures/ - -Cypress convention. Fixtures are used as external pieces of static data when we stub responses. - -### screens/ - -Contains the elements we want to interact with in our tests. - -Each file inside the screens folder represents a screen in our application. When the screens are complex, e.g. Hosts with its multiple tabs, the page is represented by a folder and the different important parts are represented by files. - -Example: - -- screens -- hosts -- all_hosts.ts -- authentications.ts -- events.ts -- main.ts -- uncommon_processes.ts - -### tasks/ - -_Tasks_ are functions that may be reused across tests. - -Each file inside the tasks folder represents a screen of our application. When the screens are complex, e.g. Hosts with its multiple tabs, the page is represented by a folder and the different important parts are represented by files. - -Example: -- tasks -- hosts -- all_hosts.ts -- authentications.ts -- events.ts -- main.ts -- uncommon_processes.ts - -## Run tests - -Currently serverless tests are not included in any pipeline, so the execution for now should be done in our local machines. - -### Visual mode - -- Navigate to `x-pack/test_serverless/functional/test_suites/security/cypress` -- Execute `yarn cypress:serverless:open` -- Select `E2E testing` -- Click on `Start E2E testing in chrome` -- Click on the test - -### Headless mode - -- Navigate to `x-pack/test_serverless/functional/test_suites/security/cypress` -- Execute `yarn cypress:serverless:run` diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts deleted file mode 100644 index 1db2cc6e0119f..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts +++ /dev/null @@ -1,40 +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 { defineCypressConfig } from '@kbn/cypress-config'; -import { dataLoaders as setupEndpointDataLoaders } from '@kbn/security-solution-plugin/public/management/cypress/support/data_loaders'; -import { setupUserDataLoader } from './support/setup_data_loader_tasks'; - -export default defineCypressConfig({ - defaultCommandTimeout: 60000, - execTimeout: 60000, - pageLoadTimeout: 60000, - responseTimeout: 60000, - screenshotsFolder: '../../../../../../target/kibana-security-solution/cypress/screenshots', - trashAssetsBeforeRuns: false, - video: false, - viewportHeight: 946, - viewportWidth: 1680, - numTestsKeptInMemory: 10, - env: { - KIBANA_USERNAME: 'system_indices_superuser', - KIBANA_PASSWORD: 'changeme', - ELASTICSEARCH_USERNAME: 'system_indices_superuser', - ELASTICSEARCH_PASSWORD: 'changeme', - }, - e2e: { - experimentalRunAllSpecs: true, - experimentalMemoryManagement: true, - supportFile: './support/e2e.js', - specPattern: './e2e/**/*.cy.ts', - setupNodeEvents: (on, config) => { - // Reuse data loaders from endpoint management cypress setup - setupEndpointDataLoaders(on, config); - setupUserDataLoader(on, config, {}); - }, - }, -}); diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/cypress.d.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/cypress.d.ts deleted file mode 100644 index a3e6066621aa1..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/cypress.d.ts +++ /dev/null @@ -1,207 +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 { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/security-solution-plugin/scripts/run_cypress/utils'; -import { - DeleteIndexedFleetEndpointPoliciesResponse, - IndexedFleetEndpointPolicyResponse, -} from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_fleet_endpoint_policy'; -import { CasePostRequest } from '@kbn/cases-plugin/common/api'; -import { - DeletedIndexedCase, - IndexedCase, -} from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_case'; -import { - HostActionResponse, - IndexEndpointHostsCyTaskOptions, -} from '@kbn/security-solution-plugin/public/management/cypress/types'; -import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; -import { DeleteIndexedEndpointHostsResponse } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_endpoint_hosts'; -import { - DeletedIndexedEndpointRuleAlerts, - IndexedEndpointRuleAlerts, -} from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_endpoint_rule_alerts'; -import { - HostPolicyResponse, - LogsEndpointActionResponse, -} from '@kbn/security-solution-plugin/common/endpoint/types'; -import { IndexedEndpointPolicyResponse } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_endpoint_policy_response'; -import { DeleteAllEndpointDataResponse } from '@kbn/security-solution-plugin/scripts/endpoint/common/delete_all_endpoint_data'; -import { LoadedRoleAndUser, ServerlessRoleName } from '../../../../shared/lib'; - -export interface LoadUserAndRoleCyTaskOptions { - name: ServerlessRoleName; -} - -declare global { - namespace Cypress { - interface SuiteConfigOverrides { - env?: { - ftrConfig: SecuritySolutionDescribeBlockFtrConfig; - }; - } - - interface Chainable { - /** - * Get Elements by `data-test-subj`. Note that his is a parent query and can only be used - * from `cy` - * - * @param args - * - * @example - * // Correct: - * cy.getByTestSubj('some-subject); - * - * // Incorrect: - * cy.get('someElement').getByTestSubj('some-subject); - */ - getByTestSubj( - ...args: Parameters['get']> - ): Chainable>; - - /** - * Finds elements by `data-test-subj` from within another. Can not be used directly from `cy`. - * - * @example - * // Correct: - * cy.get('someElement').findByTestSubj('some-subject); - * - * // Incorrect: - * cy.findByTestSubj('some-subject); - */ - findByTestSubj( - ...args: Parameters['find']> - ): Chainable>; - - /** - * Continuously call provided callback function until it either return `true` - * or fail if `timeout` is reached. - * @param fn - * @param options - */ - waitUntil( - fn: (subject?: any) => boolean | Promise | Chainable, - options?: Partial<{ - interval: number; - timeout: number; - }> - ): Chainable; - - // ---------------------------------------------------- - // - // TASKS - // - // ---------------------------------------------------- - task( - name: 'loadUserAndRole', - arg: LoadUserAndRoleCyTaskOptions, - options?: Partial - ): Chainable; - - task( - name: 'indexFleetEndpointPolicy', - arg: { - policyName: string; - endpointPackageVersion: string; - }, - options?: Partial - ): Chainable; - - task( - name: 'deleteIndexedFleetEndpointPolicies', - arg: IndexedFleetEndpointPolicyResponse, - options?: Partial - ): Chainable; - - task( - name: 'indexCase', - arg?: Partial, - options?: Partial - ): Chainable; - - task( - name: 'deleteIndexedCase', - arg: IndexedCase['data'], - options?: Partial - ): Chainable; - - task( - name: 'indexEndpointHosts', - arg?: IndexEndpointHostsCyTaskOptions, - options?: Partial - ): Chainable; - - task( - name: 'deleteIndexedEndpointHosts', - arg: IndexedHostsAndAlertsResponse, - options?: Partial - ): Chainable; - - task( - name: 'indexEndpointRuleAlerts', - arg?: { endpointAgentId: string; count?: number }, - options?: Partial - ): Chainable; - - task( - name: 'deleteIndexedEndpointRuleAlerts', - arg: IndexedEndpointRuleAlerts['alerts'], - options?: Partial - ): Chainable; - - task( - name: 'indexEndpointPolicyResponse', - arg: HostPolicyResponse, - options?: Partial - ): Chainable; - - task( - name: 'deleteIndexedEndpointPolicyResponse', - arg: IndexedEndpointPolicyResponse, - options?: Partial - ): Chainable; - - task( - name: 'sendHostActionResponse', - arg: HostActionResponse, - options?: Partial - ): Chainable; - - task( - name: 'deleteAllEndpointData', - arg: { endpointAgentIds: string[] }, - options?: Partial - ): Chainable; - - task( - name: 'createFileOnEndpoint', - arg: { hostname: string; path: string; content: string }, - options?: Partial - ): Chainable; - - task( - name: 'uploadFileToEndpoint', - arg: { hostname: string; srcPath: string; destPath: string }, - options?: Partial - ): Chainable; - - task( - name: 'installPackagesOnEndpoint', - arg: { hostname: string; packages: string[] }, - options?: Partial - ): Chainable; - - task( - name: 'readZippedFileContentOnEndpoint', - arg: { hostname: string; path: string; password?: string }, - options?: Partial - ): Chainable; - } - } -} diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/serverless.cy.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/serverless.cy.ts deleted file mode 100644 index 7000fe8ecca16..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/serverless.cy.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LEFT_NAVIGATION } from '../screens/landing_page'; -import { navigatesToLandingPage } from '../tasks/navigation'; - -describe('Serverless', () => { - it('Should navigate to the landing page', () => { - cy.visit('/', { - auth: { - username: 'elastic_serverless', - password: 'changeme', - }, - }); - navigatesToLandingPage(); - cy.get(LEFT_NAVIGATION).should('exist'); - }); -}); diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/package.json b/x-pack/test_serverless/functional/test_suites/security/cypress/package.json deleted file mode 100644 index ef8534585d4d0..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "author": "Elastic", - "name": "@kbn/security-solution-serverless", - "version": "1.0.0", - "private": true, - "license": "Elastic License 2.0", - "scripts": { - "cypress": "NODE_OPTIONS=--openssl-legacy-provider node ../../../../../../node_modules/.bin/cypress", - "cypress:open": "NODE_OPTIONS=--openssl-legacy-provider node ../../../../../plugins/security_solution/scripts/start_cypress_parallel open --config-file ../../../x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts --ftr-config-file ../../../../../../x-pack/test_serverless/functional/test_suites/security/cypress/security_config", - "cypress:run": "NODE_OPTIONS=--openssl-legacy-provider node ../../../../../plugins/security_solution/scripts/start_cypress_parallel run --browser chrome --config-file ../../../x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts --ftr-config-file ../../../../../../x-pack/test_serverless/functional/test_suites/security/cypress/security_config --reporter ../../../../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; status=$?; yarn junit:merge && exit $status", - "junit:merge": "../../../../../../node_modules/.bin/mochawesome-merge ../../../../../../target/kibana-security-serverless/cypress/results/mochawesome*.json > ../../../../../../target/kibana-security-serverless/cypress/results/output.json && ../../../../../../node_modules/.bin/marge ../../../../../../target/kibana-security-serverless/cypress/results/output.json --reportDir ../../../../../../target/kibana-security-serverless/cypress/results && mkdir -p ../../../../../../target/junit && cp ../../../../../../target/kibana-security-serverless/cypress/results/*.xml ../../../../../../target/junit/" - } -} \ No newline at end of file diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/reporter_config.json b/x-pack/test_serverless/functional/test_suites/security/cypress/reporter_config.json deleted file mode 100644 index 616e2382a8516..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/reporter_config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "reporterEnabled": "mochawesome, mocha-junit-reporter", - "reporterOptions": { - "html": false, - "json": true, - "mochaFile": "../../../../../../target/kibana-security-serverless/cypress/results/TEST-security-solution-cypress-[hash].xml", - "overwrite": false, - "reportDir": "../../../../../../target/kibana-security-serverless/cypress/results" - } -} diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/runner.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/runner.ts deleted file mode 100644 index a83d8afbaefdc..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/runner.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export type { FtrProviderContext } from '../../../ftr_provider_context'; - -export async function SecuritySolutionCypressTestRunner( - { getService }: FtrProviderContext, - envVars?: Record -) { - const config = getService('config'); - - return { - FORCE_COLOR: '1', - ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), - ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), - ...envVars, - }; -} diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/screens/index.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/screens/index.ts deleted file mode 100644 index 194bf6301191a..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/screens/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './landing_page'; diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/screens/landing_page.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/screens/landing_page.ts deleted file mode 100644 index 5b7450bd0492d..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/screens/landing_page.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const LEFT_NAVIGATION = '[data-test-subj="securitySolutionNavHeading"]'; diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts deleted file mode 100644 index e9b8a16c0b9c7..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.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 { FtrConfigProviderContext } from '@kbn/test'; - -import { ES_RESOURCES } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/serverless'; -import type { FtrProviderContext } from './runner'; -import { SecuritySolutionCypressTestRunner } from './runner'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const securitySolutionCypressConfig = await readConfigFile( - require.resolve('./security_config.base.ts') - ); - - return { - ...securitySolutionCypressConfig.getAll(), - - esServerlessOptions: { - ...(securitySolutionCypressConfig.has('esServerlessOptions') - ? securitySolutionCypressConfig.get('esServerlessOptions') ?? {} - : {}), - resources: Object.values(ES_RESOURCES), - }, - - testRunner: (context: FtrProviderContext) => SecuritySolutionCypressTestRunner(context), - }; -} diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/support/commands.js b/x-pack/test_serverless/functional/test_suites/security/cypress/support/commands.js deleted file mode 100644 index 73895fbbec589..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/support/commands.js +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This is will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/support/e2e.js b/x-pack/test_serverless/functional/test_suites/security/cypress/support/e2e.js deleted file mode 100644 index 6095b2ada6c81..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/support/e2e.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -import './commands'; -import 'cypress-real-events/support'; -import '@kbn/security-solution-plugin/public/management/cypress/support/e2e'; - -Cypress.on('uncaught:exception', () => { - return false; -}); diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/support/index.d.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/support/index.d.ts deleted file mode 100644 index 6928ba89a56f0..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/support/index.d.ts +++ /dev/null @@ -1,52 +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. - */ - -declare namespace Cypress { - interface Chainable { - promisify(): Promise; - attachFile(fileName: string, fileType?: string): Chainable; - waitUntil( - fn: (subject: Subject) => boolean | Chainable, - options?: { - interval: number; - timeout: number; - } - ): Chainable; - } -} - -declare namespace Mocha { - interface SuiteFunction { - (title: string, ftrConfig: Record, fn: (this: Suite) => void): Suite; - ( - title: string, - ftrConfig?: Record, - config: Cypress.TestConfigOverrides, - fn: (this: Suite) => void - ): Suite; - } - - interface ExclusiveSuiteFunction { - (title: string, ftrConfig: Record, fn: (this: Suite) => void): Suite; - ( - title: string, - ftrConfig?: Record, - config: Cypress.TestConfigOverrides, - fn: (this: Suite) => void - ): Suite; - } - - interface PendingSuiteFunction { - (title: string, ftrConfig: Record, fn: (this: Suite) => void): Suite; - ( - title: string, - ftrConfig?: Record, - config: Cypress.TestConfigOverrides, - fn: (this: Suite) => void - ): Suite | void; - } -} diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/endpoint_management/index_endpoint_hosts.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/endpoint_management/index_endpoint_hosts.ts deleted file mode 100644 index 81ea6d009814d..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/endpoint_management/index_endpoint_hosts.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - DeleteIndexedHostsAndAlertsResponse, - IndexedHostsAndAlertsResponse, -} from '@kbn/security-solution-plugin/common/endpoint/index_data'; -import { IndexEndpointHostsCyTaskOptions } from '@kbn/security-solution-plugin/public/management/cypress/types'; - -export interface CyIndexEndpointHosts { - data: IndexedHostsAndAlertsResponse; - cleanup: () => Cypress.Chainable; -} - -export const indexEndpointHosts = ( - options: IndexEndpointHostsCyTaskOptions = {} -): Cypress.Chainable => { - return cy.task('indexEndpointHosts', options, { timeout: 240000 }).then((indexHosts) => { - return { - data: indexHosts, - cleanup: () => { - cy.log( - 'Deleting Endpoint Host data', - indexHosts.hosts.map((host) => `${host.host.name} (${host.host.id})`) - ); - - return cy.task('deleteIndexedEndpointHosts', indexHosts); - }, - }; - }); -}; diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts deleted file mode 100644 index 7ff366ea2cd14..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { request } from '@kbn/security-solution-plugin/public/management/cypress/tasks/common'; -import { LoginState } from '@kbn/security-plugin/common/login_state'; -import type { ServerlessRoleName } from '../../../../../shared/lib'; -import { ServerlessRoleName as RoleName } from '../../../../../shared/lib/security/types'; -import { STANDARD_HTTP_HEADERS } from '../../../../../shared/lib/security/default_http_headers'; - -/** - * Send login via API - * @param username - * @param password - * - * @private - */ -const sendApiLoginRequest = ( - username: string, - password: string -): Cypress.Chainable<{ username: string; password: string }> => { - const baseUrl = Cypress.config().baseUrl; - - cy.log(`Authenticating [${username}] via ${baseUrl}`); - - const headers = { ...STANDARD_HTTP_HEADERS }; - return request({ headers, url: `${baseUrl}/internal/security/login_state` }) - .then((loginState) => { - const basicProvider = loginState.body.selector.providers.find( - (provider) => provider.type === 'basic' - ); - return request({ - url: `${baseUrl}/internal/security/login`, - method: 'POST', - headers, - body: { - providerType: basicProvider?.type, - providerName: basicProvider?.name, - currentURL: '/', - params: { username, password }, - }, - }); - }) - .then(() => ({ username, password })); -}; - -interface CyLoginTask { - (user?: ServerlessRoleName | 'elastic'): ReturnType; - - /** - * Login using any username/password - * @param username - * @param password - */ - with(username: string, password: string): ReturnType; -} - -/** - * Login to Kibana using API (not login page). By default, user will be logged in using - * the username and password defined via `KIBANA_USERNAME` and `KIBANA_PASSWORD` cypress env - * variables. - * @param user Defaults to `soc_manager` - */ -export const login: CyLoginTask = ( - user: ServerlessRoleName | 'elastic' = RoleName.SOC_MANAGER -): ReturnType => { - let username = Cypress.env('KIBANA_USERNAME'); - let password = Cypress.env('KIBANA_PASSWORD'); - - if (user && user !== 'elastic') { - return cy.task('loadUserAndRole', { name: user }).then((loadedUser) => { - username = loadedUser.username; - password = loadedUser.password; - - return sendApiLoginRequest(username, password); - }); - } else { - return sendApiLoginRequest(username, password); - } -}; - -login.with = (username: string, password: string): ReturnType => { - return sendApiLoginRequest(username, password); -}; diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/navigation.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/navigation.ts deleted file mode 100644 index af00f418747c5..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/navigation.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const navigatesToLandingPage = () => { - cy.visit('/app/security/get_started'); -}; diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index fc52752b513b2..4f34639270fe9 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -37,7 +37,6 @@ "@kbn/server-route-repository", "@kbn/core-chrome-browser", "@kbn/security-plugin", - "@kbn/security-solution-plugin", "@kbn/security-solution-plugin/public/management/cypress", "@kbn/tooling-log", "@kbn/cases-plugin", From de6da8aa280609b722df7eddff57cbfab48b7cfa Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 7 Nov 2024 11:41:13 +0100 Subject: [PATCH 09/47] [8.x] [SecuritySolution][SIEM migrations] Implement background task API (#197997) (#199209) # Backport This will backport the following commits from `main` to `8.x`: - [[SecuritySolution][SIEM migrations] Implement background task API (#197997)](https://github.com/elastic/kibana/pull/197997) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../common/api/quickstart_client.gen.ts | 89 +++++- .../common/siem_migrations/constants.ts | 21 +- .../model/api/rules/rules_migration.gen.ts | 63 +++- .../api/rules/rules_migration.schema.yaml | 139 ++++++++- .../model/rule_migration.gen.ts | 68 ++++- .../model/rule_migration.schema.yaml | 80 ++++- x-pack/plugins/security_solution/kibana.jsonc | 3 +- .../routes/__mocks__/request_context.ts | 6 +- .../lib/siem_migrations/__mocks__/mocks.ts | 8 +- .../__mocks__/siem_migrations_service.ts | 9 + .../siem_migrations/rules/__mocks__/mocks.ts | 51 +++- .../__mocks__/siem_rule_migrations_client.ts | 9 + .../lib/siem_migrations/rules/api/create.ts | 30 +- .../lib/siem_migrations/rules/api/get.ts | 47 +++ .../lib/siem_migrations/rules/api/index.ts | 10 + .../lib/siem_migrations/rules/api/start.ts | 91 ++++++ .../lib/siem_migrations/rules/api/stats.ts | 47 +++ .../siem_migrations/rules/api/stats_all.ts | 39 +++ .../lib/siem_migrations/rules/api/stop.ts | 50 +++ .../rules/data_stream/__mocks__/mocks.ts | 4 +- .../rule_migrations_data_client.ts | 275 +++++++++++++++++ .../rule_migrations_data_stream.test.ts | 57 ++-- .../rule_migrations_data_stream.ts | 44 ++- .../data_stream/rule_migrations_field_map.ts | 3 +- .../siem_rule_migrations_service.test.ts | 72 ++--- .../rules/siem_rule_migrations_service.ts | 59 ++-- .../siem_migrations/rules/task/agent/graph.ts | 43 +++ .../siem_migrations/rules/task/agent/index.ts | 8 + .../agent/nodes/match_prebuilt_rule/index.ts | 8 + .../match_prebuilt_rule.ts | 59 ++++ .../nodes/match_prebuilt_rule/prompts.ts | 35 +++ .../esql_knowledge_base_caller.ts | 36 +++ .../task/agent/nodes/translate_query/index.ts | 7 + .../agent/nodes/translate_query/prompt.ts | 39 +++ .../nodes/translate_query/translate_query.ts | 56 ++++ .../siem_migrations/rules/task/agent/state.ts | 32 ++ .../siem_migrations/rules/task/agent/types.ts | 23 ++ .../rules/task/rule_migrations_task_runner.ts | 285 ++++++++++++++++++ .../lib/siem_migrations/rules/task/types.ts | 70 +++++ .../rules/task/util/actions_client_chat.ts | 93 ++++++ .../rules/task/util/prebuilt_rules.test.ts | 105 +++++++ .../rules/task/util/prebuilt_rules.ts | 77 +++++ .../server/lib/siem_migrations/rules/types.ts | 48 ++- .../siem_migrations_service.test.ts | 22 +- .../siem_migrations_service.ts | 14 +- .../server/lib/siem_migrations/types.ts | 8 +- .../server/plugin_contract.ts | 2 + .../server/request_context_factory.ts | 10 +- .../plugins/security_solution/server/types.ts | 6 +- .../plugins/security_solution/tsconfig.json | 1 + .../services/security_solution_api.gen.ts | 87 +++++- 51 files changed, 2346 insertions(+), 202 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/__mocks__/siem_migrations_service.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/__mocks__/siem_rule_migrations_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats_all.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/prompts.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/esql_knowledge_base_caller.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompt.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/translate_query.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/types.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_runner.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/types.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/actions_client_chat.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/prebuilt_rules.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/prebuilt_rules.ts diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index 065601f1cf4ab..ca28715a1524e 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -364,7 +364,16 @@ import type { import type { CreateRuleMigrationRequestBodyInput, CreateRuleMigrationResponse, + GetAllStatsRuleMigrationResponse, + GetRuleMigrationRequestParamsInput, GetRuleMigrationResponse, + GetRuleMigrationStatsRequestParamsInput, + GetRuleMigrationStatsResponse, + StartRuleMigrationRequestParamsInput, + StartRuleMigrationRequestBodyInput, + StartRuleMigrationResponse, + StopRuleMigrationRequestParamsInput, + StopRuleMigrationResponse, } from '../siem_migrations/model/api/rules/rules_migration.gen'; export interface ClientOptions { @@ -1205,6 +1214,21 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Retrieves the rule migrations stats for all migrations stored in the system + */ + async getAllStatsRuleMigration() { + this.log.info(`${new Date().toISOString()} Calling API GetAllStatsRuleMigration`); + return this.kbnClient + .request({ + path: '/internal/siem_migrations/rules/stats', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Get the criticality record for a specific asset. */ @@ -1395,13 +1419,28 @@ finalize it. .catch(catchAxiosErrorFormatAndThrow); } /** - * Retrieves the rule migrations stored in the system + * Retrieves the rule documents stored in the system given the rule migration id */ - async getRuleMigration() { + async getRuleMigration(props: GetRuleMigrationProps) { this.log.info(`${new Date().toISOString()} Calling API GetRuleMigration`); return this.kbnClient .request({ - path: '/internal/siem_migrations/rules', + path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } + /** + * Retrieves the stats of a SIEM rules migration using the migration id provided + */ + async getRuleMigrationStats(props: GetRuleMigrationStatsProps) { + this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationStats`); + return this.kbnClient + .request({ + path: replaceParams('/internal/siem_migrations/rules/{migration_id}/stats', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, @@ -1913,6 +1952,22 @@ detection engine rules. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Starts a SIEM rules migration using the migration id provided + */ + async startRuleMigration(props: StartRuleMigrationProps) { + this.log.info(`${new Date().toISOString()} Calling API StartRuleMigration`); + return this.kbnClient + .request({ + path: replaceParams('/internal/siem_migrations/rules/{migration_id}/start', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'PUT', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } async stopEntityEngine(props: StopEntityEngineProps) { this.log.info(`${new Date().toISOString()} Calling API StopEntityEngine`); return this.kbnClient @@ -1925,6 +1980,21 @@ detection engine rules. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Stops a running SIEM rules migration using the migration id provided + */ + async stopRuleMigration(props: StopRuleMigrationProps) { + this.log.info(`${new Date().toISOString()} Calling API StopRuleMigration`); + return this.kbnClient + .request({ + path: replaceParams('/internal/siem_migrations/rules/{migration_id}/stop', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'PUT', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Suggests user profiles. */ @@ -2161,6 +2231,12 @@ export interface GetRuleExecutionResultsProps { query: GetRuleExecutionResultsRequestQueryInput; params: GetRuleExecutionResultsRequestParamsInput; } +export interface GetRuleMigrationProps { + params: GetRuleMigrationRequestParamsInput; +} +export interface GetRuleMigrationStatsProps { + params: GetRuleMigrationStatsRequestParamsInput; +} export interface GetTimelineProps { query: GetTimelineRequestQueryInput; } @@ -2237,9 +2313,16 @@ export interface SetAlertTagsProps { export interface StartEntityEngineProps { params: StartEntityEngineRequestParamsInput; } +export interface StartRuleMigrationProps { + params: StartRuleMigrationRequestParamsInput; + body: StartRuleMigrationRequestBodyInput; +} export interface StopEntityEngineProps { params: StopEntityEngineRequestParamsInput; } +export interface StopRuleMigrationProps { + params: StopRuleMigrationRequestParamsInput; +} export interface SuggestUserProfilesProps { query: SuggestUserProfilesRequestQueryInput; } diff --git a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts index 96ca75679f112..f2efc646a8101 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts @@ -8,9 +8,24 @@ export const SIEM_MIGRATIONS_PATH = '/internal/siem_migrations' as const; export const SIEM_RULE_MIGRATIONS_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const; -export enum SiemMigrationsStatus { +export const SIEM_RULE_MIGRATIONS_ALL_STATS_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/stats` as const; +export const SIEM_RULE_MIGRATIONS_GET_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}` as const; +export const SIEM_RULE_MIGRATIONS_START_PATH = + `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}/start` as const; +export const SIEM_RULE_MIGRATIONS_STATS_PATH = + `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}/stats` as const; +export const SIEM_RULE_MIGRATIONS_STOP_PATH = + `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}/stop` as const; + +export enum SiemMigrationStatus { PENDING = 'pending', PROCESSING = 'processing', - FINISHED = 'finished', - ERROR = 'error', + COMPLETED = 'completed', + FAILED = 'failed', +} + +export enum SiemMigrationRuleTranslationResult { + FULL = 'full', + PARTIAL = 'partial', + UNTRANSLATABLE = 'untranslatable', } diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rules_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rules_migration.gen.ts index fa8a1cc8a6778..120505ec43cb7 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rules_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rules_migration.gen.ts @@ -16,7 +16,13 @@ import { z } from '@kbn/zod'; -import { OriginalRule, RuleMigration } from '../../rule_migration.gen'; +import { + OriginalRule, + RuleMigrationAllTaskStats, + RuleMigration, + RuleMigrationTaskStats, +} from '../../rule_migration.gen'; +import { ConnectorId, LangSmithOptions } from '../common.gen'; export type CreateRuleMigrationRequestBody = z.infer; export const CreateRuleMigrationRequestBody = z.array(OriginalRule); @@ -30,5 +36,60 @@ export const CreateRuleMigrationResponse = z.object({ migration_id: z.string(), }); +export type GetAllStatsRuleMigrationResponse = z.infer; +export const GetAllStatsRuleMigrationResponse = RuleMigrationAllTaskStats; + +export type GetRuleMigrationRequestParams = z.infer; +export const GetRuleMigrationRequestParams = z.object({ + migration_id: z.string(), +}); +export type GetRuleMigrationRequestParamsInput = z.input; + export type GetRuleMigrationResponse = z.infer; export const GetRuleMigrationResponse = z.array(RuleMigration); + +export type GetRuleMigrationStatsRequestParams = z.infer; +export const GetRuleMigrationStatsRequestParams = z.object({ + migration_id: z.string(), +}); +export type GetRuleMigrationStatsRequestParamsInput = z.input< + typeof GetRuleMigrationStatsRequestParams +>; + +export type GetRuleMigrationStatsResponse = z.infer; +export const GetRuleMigrationStatsResponse = RuleMigrationTaskStats; + +export type StartRuleMigrationRequestParams = z.infer; +export const StartRuleMigrationRequestParams = z.object({ + migration_id: z.string(), +}); +export type StartRuleMigrationRequestParamsInput = z.input; + +export type StartRuleMigrationRequestBody = z.infer; +export const StartRuleMigrationRequestBody = z.object({ + connector_id: ConnectorId, + langsmith_options: LangSmithOptions.optional(), +}); +export type StartRuleMigrationRequestBodyInput = z.input; + +export type StartRuleMigrationResponse = z.infer; +export const StartRuleMigrationResponse = z.object({ + /** + * Indicates the migration has been started. `false` means the migration does not need to be started. + */ + started: z.boolean(), +}); + +export type StopRuleMigrationRequestParams = z.infer; +export const StopRuleMigrationRequestParams = z.object({ + migration_id: z.string(), +}); +export type StopRuleMigrationRequestParamsInput = z.input; + +export type StopRuleMigrationResponse = z.infer; +export const StopRuleMigrationResponse = z.object({ + /** + * Indicates the migration has been stopped. + */ + stopped: z.boolean(), +}); diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rules_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rules_migration.schema.yaml index 40596ba7e712d..7b06c3d6a22ac 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rules_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rules_migration.schema.yaml @@ -10,8 +10,7 @@ paths: x-codegen-enabled: true description: Creates a new SIEM rules migration using the original vendor rules provided tags: - - SIEM Migrations - - Rule Migrations + - SIEM Rule Migrations requestBody: required: true content: @@ -33,20 +32,146 @@ paths: migration_id: type: string description: The migration id created. + + /internal/siem_migrations/rules/stats: get: - summary: Retrieves rule migrations - operationId: GetRuleMigration + summary: Retrieves the stats for all rule migrations + operationId: GetAllStatsRuleMigration x-codegen-enabled: true - description: Retrieves the rule migrations stored in the system + description: Retrieves the rule migrations stats for all migrations stored in the system tags: - - SIEM Migrations - - Rule Migrations + - SIEM Rule Migrations responses: 200: description: Indicates rule migrations have been retrieved correctly. + content: + application/json: + schema: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationAllTaskStats' + + /internal/siem_migrations/rules/{migration_id}: + get: + summary: Retrieves all the rules of a migration + operationId: GetRuleMigration + x-codegen-enabled: true + description: Retrieves the rule documents stored in the system given the rule migration id + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + type: string + description: The migration id to start + responses: + 200: + description: Indicates rule migration have been retrieved correctly. content: application/json: schema: type: array items: $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration' + 204: + description: Indicates the migration id was not found. + + /internal/siem_migrations/rules/{migration_id}/start: + put: + summary: Starts a rule migration + operationId: StartRuleMigration + x-codegen-enabled: true + description: Starts a SIEM rules migration using the migration id provided + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + type: string + description: The migration id to start + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - connector_id + properties: + connector_id: + $ref: '../common.schema.yaml#/components/schemas/ConnectorId' + langsmith_options: + $ref: '../common.schema.yaml#/components/schemas/LangSmithOptions' + responses: + 200: + description: Indicates the migration start request has been processed successfully. + content: + application/json: + schema: + type: object + required: + - started + properties: + started: + type: boolean + description: Indicates the migration has been started. `false` means the migration does not need to be started. + 204: + description: Indicates the migration id was not found. + + /internal/siem_migrations/rules/{migration_id}/stats: + get: + summary: Gets a rule migration task stats + operationId: GetRuleMigrationStats + x-codegen-enabled: true + description: Retrieves the stats of a SIEM rules migration using the migration id provided + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + type: string + description: The migration id to start + responses: + 200: + description: Indicates the migration stats has been retrieved correctly. + content: + application/json: + schema: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTaskStats' + 204: + description: Indicates the migration id was not found. + + /internal/siem_migrations/rules/{migration_id}/stop: + put: + summary: Stops an existing rule migration + operationId: StopRuleMigration + x-codegen-enabled: true + description: Stops a running SIEM rules migration using the migration id provided + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + type: string + description: The migration id to stop + responses: + 200: + description: Indicates migration task stop has been processed successfully. + content: + application/json: + schema: + type: object + required: + - stopped + properties: + stopped: + type: boolean + description: Indicates the migration has been stopped. + 204: + description: Indicates the migration id was not found running. diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 0e07ef2f208da..fe00c4b4df1c6 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -71,11 +71,11 @@ export const ElasticRule = z.object({ /** * The translated elastic query. */ - query: z.string(), + query: z.string().optional(), /** * The translated elastic query language. */ - query_language: z.literal('esql').default('esql'), + query_language: z.literal('esql').optional(), /** * The Elastic prebuilt rule id matched. */ @@ -99,16 +99,20 @@ export const RuleMigration = z.object({ * The migration id. */ migration_id: z.string(), + /** + * The username of the user who created the migration. + */ + created_by: z.string(), original_rule: OriginalRule, elastic_rule: ElasticRule.optional(), /** - * The translation state. + * The rule translation result. */ - translation_state: z.enum(['complete', 'partial', 'untranslatable']).optional(), + translation_result: z.enum(['full', 'partial', 'untranslatable']).optional(), /** - * The status of the rule migration. + * The status of the rule migration process. */ - status: z.enum(['pending', 'processing', 'finished', 'error']).default('pending'), + status: z.enum(['pending', 'processing', 'completed', 'failed']).default('pending'), /** * The comments for the migration including a summary from the LLM in markdown. */ @@ -122,3 +126,55 @@ export const RuleMigration = z.object({ */ updated_by: z.string().optional(), }); + +/** + * The rule migration task stats object. + */ +export type RuleMigrationTaskStats = z.infer; +export const RuleMigrationTaskStats = z.object({ + /** + * Indicates if the migration task status. + */ + status: z.enum(['ready', 'running', 'stopped', 'finished']), + /** + * The rules migration stats. + */ + rules: z.object({ + /** + * The total number of rules to migrate. + */ + total: z.number().int(), + /** + * The number of rules that are pending migration. + */ + pending: z.number().int(), + /** + * The number of rules that are being migrated. + */ + processing: z.number().int(), + /** + * The number of rules that have been migrated successfully. + */ + completed: z.number().int(), + /** + * The number of rules that have failed migration. + */ + failed: z.number().int(), + }), + /** + * The moment of the last update. + */ + last_updated_at: z.string().optional(), +}); + +export type RuleMigrationAllTaskStats = z.infer; +export const RuleMigrationAllTaskStats = z.array( + RuleMigrationTaskStats.merge( + z.object({ + /** + * The migration id + */ + migration_id: z.string(), + }) + ) +); diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 9ec825389a52b..c9841856a6914 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -48,8 +48,6 @@ components: description: The migrated elastic rule. required: - title - - query - - query_language properties: title: type: string @@ -68,7 +66,6 @@ components: description: The translated elastic query language. enum: - esql - default: esql prebuilt_rule_id: type: string description: The Elastic prebuilt rule id matched. @@ -84,32 +81,36 @@ components: - migration_id - original_rule - status + - created_by properties: - "@timestamp": + '@timestamp': type: string description: The moment of creation migration_id: type: string description: The migration id. + created_by: + type: string + description: The username of the user who created the migration. original_rule: $ref: '#/components/schemas/OriginalRule' elastic_rule: $ref: '#/components/schemas/ElasticRule' - translation_state: + translation_result: type: string - description: The translation state. - enum: - - complete + description: The rule translation result. + enum: # should match SiemMigrationRuleTranslationResult enum at ../constants.ts + - full - partial - untranslatable status: type: string - description: The status of the rule migration. + description: The status of the rule migration process. enum: # should match SiemMigrationsStatus enum at ../constants.ts - pending - processing - - finished - - error + - completed + - failed default: pending comments: type: array @@ -122,3 +123,60 @@ components: updated_by: type: string description: The user who last updated the migration + + RuleMigrationTaskStats: + type: object + description: The rule migration task stats object. + required: + - status + - rules + properties: + status: + type: string + description: Indicates if the migration task status. + enum: + - ready + - running + - stopped + - finished + rules: + type: object + description: The rules migration stats. + required: + - total + - pending + - processing + - completed + - failed + properties: + total: + type: integer + description: The total number of rules to migrate. + pending: + type: integer + description: The number of rules that are pending migration. + processing: + type: integer + description: The number of rules that are being migrated. + completed: + type: integer + description: The number of rules that have been migrated successfully. + failed: + type: integer + description: The number of rules that have failed migration. + last_updated_at: + type: string + description: The moment of the last update. + + RuleMigrationAllTaskStats: + type: array + items: + allOf: + - $ref: '#/components/schemas/RuleMigrationTaskStats' + - type: object + required: + - migration_id + properties: + migration_id: + type: string + description: The migration id diff --git a/x-pack/plugins/security_solution/kibana.jsonc b/x-pack/plugins/security_solution/kibana.jsonc index e48a9794b7e5c..8c8b77d48bc9f 100644 --- a/x-pack/plugins/security_solution/kibana.jsonc +++ b/x-pack/plugins/security_solution/kibana.jsonc @@ -54,7 +54,8 @@ "savedSearch", "unifiedDocViewer", "charts", - "entityManager" + "entityManager", + "inference" ], "optionalPlugins": [ "encryptedSavedObjects", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index ebc1706b309f8..d2aacbdeaeeaf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -79,7 +79,8 @@ export const createMockClients = () => { internalFleetServices: { packages: packageServiceMock.createClient(), }, - siemMigrationsClient: siemMigrationsServiceMock.createClient(), + siemRuleMigrationsClient: siemMigrationsServiceMock.createRulesClient(), + getInferenceClient: jest.fn(), }; }; @@ -165,7 +166,8 @@ const createSecuritySolutionRequestContextMock = ( getAssetCriticalityDataClient: jest.fn(() => clients.assetCriticalityDataClient), getAuditLogger: jest.fn(() => mockAuditLogger), getEntityStoreDataClient: jest.fn(() => clients.entityStoreDataClient), - getSiemMigrationsClient: jest.fn(() => clients.siemMigrationsClient), + getSiemRuleMigrationsClient: jest.fn(() => clients.siemRuleMigrationsClient), + getInferenceClient: jest.fn(() => clients.getInferenceClient()), }; }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/__mocks__/mocks.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/__mocks__/mocks.ts index fcf119e19ece5..af961d48db5b1 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/__mocks__/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/__mocks__/mocks.ts @@ -7,18 +7,16 @@ import { createRuleMigrationClient } from '../rules/__mocks__/mocks'; -const createClient = () => ({ rules: createRuleMigrationClient() }); - export const mockSetup = jest.fn().mockResolvedValue(undefined); -export const mockCreateClient = jest.fn().mockReturnValue(createClient()); +export const mockCreateClient = jest.fn().mockReturnValue(createRuleMigrationClient()); export const mockStop = jest.fn(); export const siemMigrationsServiceMock = { create: () => jest.fn().mockImplementation(() => ({ setup: mockSetup, - createClient: mockCreateClient, + createRulesClient: mockCreateClient, stop: mockStop, })), - createClient: () => createClient(), + createRulesClient: () => createRuleMigrationClient(), }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/__mocks__/siem_migrations_service.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/__mocks__/siem_migrations_service.ts new file mode 100644 index 0000000000000..659929d47570f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/__mocks__/siem_migrations_service.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. + */ + +import { siemMigrationsServiceMock } from './mocks'; +export const SiemMigrationsService = siemMigrationsServiceMock.create(); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/__mocks__/mocks.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/__mocks__/mocks.ts index 8233151f513e4..8811a54195e2b 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/__mocks__/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/__mocks__/mocks.ts @@ -5,17 +5,56 @@ * 2.0. */ -import type { SiemRuleMigrationsClient } from '../types'; - -export const createRuleMigrationClient = (): SiemRuleMigrationsClient => ({ +export const createRuleMigrationDataClient = jest.fn().mockImplementation(() => ({ create: jest.fn().mockResolvedValue({ success: true }), - search: jest.fn().mockResolvedValue([]), + getRules: jest.fn().mockResolvedValue([]), + takePending: jest.fn().mockResolvedValue([]), + saveFinished: jest.fn().mockResolvedValue({ success: true }), + saveError: jest.fn().mockResolvedValue({ success: true }), + releaseProcessing: jest.fn().mockResolvedValue({ success: true }), + releaseProcessable: jest.fn().mockResolvedValue({ success: true }), + getStats: jest.fn().mockResolvedValue({ + status: 'done', + rules: { + total: 1, + finished: 1, + processing: 0, + pending: 0, + failed: 0, + }, + }), + getAllStats: jest.fn().mockResolvedValue([]), +})); + +export const createRuleMigrationTaskClient = () => ({ + start: jest.fn().mockResolvedValue({ started: true }), + stop: jest.fn().mockResolvedValue({ stopped: true }), + getStats: jest.fn().mockResolvedValue({ + status: 'done', + rules: { + total: 1, + finished: 1, + processing: 0, + pending: 0, + failed: 0, + }, + }), + getAllStats: jest.fn().mockResolvedValue([]), }); +export const createRuleMigrationClient = () => ({ + data: createRuleMigrationDataClient(), + task: createRuleMigrationTaskClient(), +}); + +export const MockSiemRuleMigrationsClient = jest.fn().mockImplementation(createRuleMigrationClient); + export const mockSetup = jest.fn(); -export const mockGetClient = jest.fn().mockReturnValue(createRuleMigrationClient()); +export const mockCreateClient = jest.fn().mockReturnValue(createRuleMigrationClient()); +export const mockStop = jest.fn(); export const MockSiemRuleMigrationsService = jest.fn().mockImplementation(() => ({ setup: mockSetup, - getClient: mockGetClient, + createClient: mockCreateClient, + stop: mockStop, })); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/__mocks__/siem_rule_migrations_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/__mocks__/siem_rule_migrations_client.ts new file mode 100644 index 0000000000000..98032605ed233 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/__mocks__/siem_rule_migrations_client.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. + */ + +import { MockSiemRuleMigrationsClient } from './mocks'; +export const SiemRuleMigrationsClient = MockSiemRuleMigrationsClient; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts index f4c52e9b444b8..e2505ca83beed 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts @@ -8,14 +8,11 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { v4 as uuidV4 } from 'uuid'; -import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import type { CreateRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; import { CreateRuleMigrationRequestBody } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; -import { - SIEM_RULE_MIGRATIONS_PATH, - SiemMigrationsStatus, -} from '../../../../../common/siem_migrations/constants'; +import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; +import type { CreateRuleMigrationInput } from '../data_stream/rule_migrations_data_client'; export const registerSiemRuleMigrationsCreateRoute = ( router: SecuritySolutionPluginRouter, @@ -25,11 +22,7 @@ export const registerSiemRuleMigrationsCreateRoute = ( .post({ path: SIEM_RULE_MIGRATIONS_PATH, access: 'internal', - security: { - authz: { - requiredPrivileges: ['securitySolution'], - }, - }, + security: { authz: { requiredPrivileges: ['securitySolution'] } }, }) .addVersion( { @@ -41,27 +34,22 @@ export const registerSiemRuleMigrationsCreateRoute = ( async (context, req, res): Promise> => { const originalRules = req.body; try { - const ctx = await context.resolve(['core', 'actions', 'securitySolution']); - - const siemMigrationClient = ctx.securitySolution.getSiemMigrationsClient(); + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); const migrationId = uuidV4(); - const timestamp = new Date().toISOString(); - const ruleMigrations = originalRules.map((originalRule) => ({ - '@timestamp': timestamp, + const ruleMigrations = originalRules.map((originalRule) => ({ migration_id: migrationId, original_rule: originalRule, - status: SiemMigrationsStatus.PENDING, })); - await siemMigrationClient.rules.create(ruleMigrations); + + await ruleMigrationsClient.data.create(ruleMigrations); return res.ok({ body: { migration_id: migrationId } }); } catch (err) { logger.error(err); - return res.badRequest({ - body: err.message, - }); + return res.badRequest({ body: err.message }); } } ); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts new file mode 100644 index 0000000000000..0efb6706918f5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { GetRuleMigrationRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { SIEM_RULE_MIGRATIONS_GET_PATH } from '../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; + +export const registerSiemRuleMigrationsGetRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_RULE_MIGRATIONS_GET_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { + version: '1', + validate: { + request: { params: buildRouteValidationWithZod(GetRuleMigrationRequestParams) }, + }, + }, + async (context, req, res): Promise> => { + const migrationId = req.params.migration_id; + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + + const migrationRules = await ruleMigrationsClient.data.getRules(migrationId); + + return res.ok({ body: migrationRules }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts index 0de49eb7df92b..f37eb2108a8a4 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts @@ -8,10 +8,20 @@ import type { Logger } from '@kbn/core/server'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { registerSiemRuleMigrationsCreateRoute } from './create'; +import { registerSiemRuleMigrationsGetRoute } from './get'; +import { registerSiemRuleMigrationsStartRoute } from './start'; +import { registerSiemRuleMigrationsStatsRoute } from './stats'; +import { registerSiemRuleMigrationsStopRoute } from './stop'; +import { registerSiemRuleMigrationsStatsAllRoute } from './stats_all'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, logger: Logger ) => { registerSiemRuleMigrationsCreateRoute(router, logger); + registerSiemRuleMigrationsStatsAllRoute(router, logger); + registerSiemRuleMigrationsGetRoute(router, logger); + registerSiemRuleMigrationsStartRoute(router, logger); + registerSiemRuleMigrationsStatsRoute(router, logger); + registerSiemRuleMigrationsStopRoute(router, logger); }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts new file mode 100644 index 0000000000000..f97a4f2ce2398 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/start.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 type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { APMTracer } from '@kbn/langchain/server/tracers/apm'; +import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; +import type { StartRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { + StartRuleMigrationRequestBody, + StartRuleMigrationRequestParams, +} from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { SIEM_RULE_MIGRATIONS_START_PATH } from '../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; + +export const registerSiemRuleMigrationsStartRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .put({ + path: SIEM_RULE_MIGRATIONS_START_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: buildRouteValidationWithZod(StartRuleMigrationRequestParams), + body: buildRouteValidationWithZod(StartRuleMigrationRequestBody), + }, + }, + }, + async (context, req, res): Promise> => { + const migrationId = req.params.migration_id; + const { langsmith_options: langsmithOptions, connector_id: connectorId } = req.body; + + try { + const ctx = await context.resolve([ + 'core', + 'actions', + 'alerting', + 'securitySolution', + 'licensing', + ]); + if (!ctx.licensing.license.hasAtLeast('enterprise')) { + return res.forbidden({ + body: 'You must have a trial or enterprise license to use this feature', + }); + } + + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + const inferenceClient = ctx.securitySolution.getInferenceClient(); + const actionsClient = ctx.actions.getActionsClient(); + const soClient = ctx.core.savedObjects.client; + const rulesClient = ctx.alerting.getRulesClient(); + + const invocationConfig = { + callbacks: [ + new APMTracer({ projectName: langsmithOptions?.project_name ?? 'default' }, logger), + ...getLangSmithTracer({ ...langsmithOptions, logger }), + ], + }; + + const { exists, started } = await ruleMigrationsClient.task.start({ + migrationId, + connectorId, + invocationConfig, + inferenceClient, + actionsClient, + soClient, + rulesClient, + }); + + if (!exists) { + return res.noContent(); + } + return res.ok({ body: { started } }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts new file mode 100644 index 0000000000000..8316e01fc6a9b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { GetRuleMigrationStatsResponse } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { GetRuleMigrationStatsRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { SIEM_RULE_MIGRATIONS_STATS_PATH } from '../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; + +export const registerSiemRuleMigrationsStatsRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_RULE_MIGRATIONS_STATS_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { + version: '1', + validate: { + request: { params: buildRouteValidationWithZod(GetRuleMigrationStatsRequestParams) }, + }, + }, + async (context, req, res): Promise> => { + const migrationId = req.params.migration_id; + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + + const stats = await ruleMigrationsClient.task.getStats(migrationId); + + return res.ok({ body: stats }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats_all.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats_all.ts new file mode 100644 index 0000000000000..dd2f2f503e19d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stats_all.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import type { GetAllStatsRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { SIEM_RULE_MIGRATIONS_ALL_STATS_PATH } from '../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; + +export const registerSiemRuleMigrationsStatsAllRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { version: '1', validate: {} }, + async (context, req, res): Promise> => { + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + + const allStats = await ruleMigrationsClient.task.getAllStats(); + + return res.ok({ body: allStats }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts new file mode 100644 index 0000000000000..4767106910186 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { StopRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { StopRuleMigrationRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rules_migration.gen'; +import { SIEM_RULE_MIGRATIONS_STOP_PATH } from '../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; + +export const registerSiemRuleMigrationsStopRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .put({ + path: SIEM_RULE_MIGRATIONS_STOP_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { + version: '1', + validate: { + request: { params: buildRouteValidationWithZod(StopRuleMigrationRequestParams) }, + }, + }, + async (context, req, res): Promise> => { + const migrationId = req.params.migration_id; + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + + const { exists, stopped } = await ruleMigrationsClient.task.stop(migrationId); + + if (!exists) { + return res.noContent(); + } + return res.ok({ body: { stopped } }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/__mocks__/mocks.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/__mocks__/mocks.ts index 103c0f9b0c952..1d9a181d2de5b 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/__mocks__/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/__mocks__/mocks.ts @@ -7,9 +7,9 @@ export const mockIndexName = 'mocked_data_stream_name'; export const mockInstall = jest.fn().mockResolvedValue(undefined); -export const mockInstallSpace = jest.fn().mockResolvedValue(mockIndexName); +export const mockCreateClient = jest.fn().mockReturnValue({}); export const MockRuleMigrationsDataStream = jest.fn().mockImplementation(() => ({ install: mockInstall, - installSpace: mockInstallSpace, + createClient: mockCreateClient, })); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_client.ts new file mode 100644 index 0000000000000..83808901a0bd1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_client.ts @@ -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 { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server'; +import assert from 'assert'; +import type { + AggregationsFilterAggregate, + AggregationsMaxAggregate, + AggregationsStringTermsAggregate, + AggregationsStringTermsBucket, + QueryDslQueryContainer, + SearchHit, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/types'; +import type { StoredRuleMigration } from '../types'; +import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; +import type { + RuleMigration, + RuleMigrationTaskStats, +} from '../../../../../common/siem_migrations/model/rule_migration.gen'; + +export type CreateRuleMigrationInput = Omit; +export type RuleMigrationDataStats = Omit; +export type RuleMigrationAllDataStats = Array; + +export class RuleMigrationsDataClient { + constructor( + private dataStreamNamePromise: Promise, + private currentUser: AuthenticatedUser, + private esClient: ElasticsearchClient, + private logger: Logger + ) {} + + /** Indexes an array of rule migrations to be processed */ + async create(ruleMigrations: CreateRuleMigrationInput[]): Promise { + const index = await this.dataStreamNamePromise; + await this.esClient + .bulk({ + refresh: 'wait_for', + operations: ruleMigrations.flatMap((ruleMigration) => [ + { create: { _index: index } }, + { + ...ruleMigration, + '@timestamp': new Date().toISOString(), + status: SiemMigrationStatus.PENDING, + created_by: this.currentUser.username, + }, + ]), + }) + .catch((error) => { + this.logger.error(`Error creating rule migrations: ${error.message}`); + throw error; + }); + } + + /** Retrieves an array of rule documents of a specific migrations */ + async getRules(migrationId: string): Promise { + const index = await this.dataStreamNamePromise; + const query = this.getFilterQuery(migrationId); + + const storedRuleMigrations = await this.esClient + .search({ index, query, sort: '_doc' }) + .catch((error) => { + this.logger.error(`Error searching getting rule migrations: ${error.message}`); + throw error; + }) + .then((response) => this.processHits(response.hits.hits)); + return storedRuleMigrations; + } + + /** + * Retrieves `pending` rule migrations with the provided id and updates their status to `processing`. + * This operation is not atomic at migration level: + * - Multiple tasks can process different migrations simultaneously. + * - Multiple tasks should not process the same migration simultaneously. + */ + async takePending(migrationId: string, size: number): Promise { + const index = await this.dataStreamNamePromise; + const query = this.getFilterQuery(migrationId, SiemMigrationStatus.PENDING); + + const storedRuleMigrations = await this.esClient + .search({ index, query, sort: '_doc', size }) + .catch((error) => { + this.logger.error(`Error searching for rule migrations: ${error.message}`); + throw error; + }) + .then((response) => + this.processHits(response.hits.hits, { status: SiemMigrationStatus.PROCESSING }) + ); + + await this.esClient + .bulk({ + refresh: 'wait_for', + operations: storedRuleMigrations.flatMap(({ _id, _index, status }) => [ + { update: { _id, _index } }, + { + doc: { + status, + updated_by: this.currentUser.username, + updated_at: new Date().toISOString(), + }, + }, + ]), + }) + .catch((error) => { + this.logger.error( + `Error updating for rule migrations status to processing: ${error.message}` + ); + throw error; + }); + + return storedRuleMigrations; + } + + /** Updates one rule migration with the provided data and sets the status to `completed` */ + async saveFinished({ _id, _index, ...ruleMigration }: StoredRuleMigration): Promise { + const doc = { + ...ruleMigration, + status: SiemMigrationStatus.COMPLETED, + updated_by: this.currentUser.username, + updated_at: new Date().toISOString(), + }; + await this.esClient + .update({ index: _index, id: _id, doc, refresh: 'wait_for' }) + .catch((error) => { + this.logger.error(`Error updating rule migration status to completed: ${error.message}`); + throw error; + }); + } + + /** Updates one rule migration with the provided data and sets the status to `failed` */ + async saveError({ _id, _index, ...ruleMigration }: StoredRuleMigration): Promise { + const doc = { + ...ruleMigration, + status: SiemMigrationStatus.FAILED, + updated_by: this.currentUser.username, + updated_at: new Date().toISOString(), + }; + await this.esClient + .update({ index: _index, id: _id, doc, refresh: 'wait_for' }) + .catch((error) => { + this.logger.error(`Error updating rule migration status to completed: ${error.message}`); + throw error; + }); + } + + /** Updates all the rule migration with the provided id with status `processing` back to `pending` */ + async releaseProcessing(migrationId: string): Promise { + const index = await this.dataStreamNamePromise; + const query = this.getFilterQuery(migrationId, SiemMigrationStatus.PROCESSING); + const script = { source: `ctx._source['status'] = '${SiemMigrationStatus.PENDING}'` }; + await this.esClient.updateByQuery({ index, query, script, refresh: false }).catch((error) => { + this.logger.error(`Error releasing rule migrations status to pending: ${error.message}`); + throw error; + }); + } + + /** Updates all the rule migration with the provided id with status `processing` or `failed` back to `pending` */ + async releaseProcessable(migrationId: string): Promise { + const index = await this.dataStreamNamePromise; + const query = this.getFilterQuery(migrationId, [ + SiemMigrationStatus.PROCESSING, + SiemMigrationStatus.FAILED, + ]); + const script = { source: `ctx._source['status'] = '${SiemMigrationStatus.PENDING}'` }; + await this.esClient.updateByQuery({ index, query, script, refresh: true }).catch((error) => { + this.logger.error(`Error releasing rule migrations status to pending: ${error.message}`); + throw error; + }); + } + + /** Retrieves the stats for the rule migrations with the provided id */ + async getStats(migrationId: string): Promise { + const index = await this.dataStreamNamePromise; + const query = this.getFilterQuery(migrationId); + const aggregations = { + pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } }, + processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, + completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } }, + failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } }, + lastUpdatedAt: { max: { field: 'updated_at' } }, + }; + const result = await this.esClient + .search({ index, query, aggregations, _source: false }) + .catch((error) => { + this.logger.error(`Error getting rule migrations stats: ${error.message}`); + throw error; + }); + + const { pending, processing, completed, lastUpdatedAt, failed } = result.aggregations ?? {}; + return { + rules: { + total: this.getTotalHits(result), + pending: (pending as AggregationsFilterAggregate)?.doc_count ?? 0, + processing: (processing as AggregationsFilterAggregate)?.doc_count ?? 0, + completed: (completed as AggregationsFilterAggregate)?.doc_count ?? 0, + failed: (failed as AggregationsFilterAggregate)?.doc_count ?? 0, + }, + last_updated_at: (lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string, + }; + } + + /** Retrieves the stats for all the rule migrations aggregated by migration id */ + async getAllStats(): Promise { + const index = await this.dataStreamNamePromise; + const aggregations = { + migrationIds: { + terms: { field: 'migration_id' }, + aggregations: { + pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } }, + processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, + completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } }, + failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } }, + lastUpdatedAt: { max: { field: 'updated_at' } }, + }, + }, + }; + const result = await this.esClient + .search({ index, aggregations, _source: false }) + .catch((error) => { + this.logger.error(`Error getting all rule migrations stats: ${error.message}`); + throw error; + }); + + const migrationsAgg = result.aggregations?.migrationIds as AggregationsStringTermsAggregate; + const buckets = (migrationsAgg?.buckets as AggregationsStringTermsBucket[]) ?? []; + return buckets.map((bucket) => ({ + migration_id: bucket.key, + rules: { + total: bucket.doc_count, + pending: bucket.pending?.doc_count ?? 0, + processing: bucket.processing?.doc_count ?? 0, + completed: bucket.completed?.doc_count ?? 0, + failed: bucket.failed?.doc_count ?? 0, + }, + last_updated_at: bucket.lastUpdatedAt?.value_as_string, + })); + } + + private getFilterQuery( + migrationId: string, + status?: SiemMigrationStatus | SiemMigrationStatus[] + ): QueryDslQueryContainer { + const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }]; + if (status) { + if (Array.isArray(status)) { + filter.push({ terms: { status } }); + } else { + filter.push({ term: { status } }); + } + } + return { bool: { filter } }; + } + + private processHits( + hits: Array>, + override: Partial = {} + ): StoredRuleMigration[] { + return hits.map(({ _id, _index, _source }) => { + assert(_id, 'RuleMigration document should have _id'); + assert(_source, 'RuleMigration document should have _source'); + return { ..._source, ...override, _id, _index }; + }); + } + + private getTotalHits(response: SearchResponse) { + return typeof response.hits.total === 'number' + ? response.hits.total + : response.hits.total?.value ?? 0; + } +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_stream.test.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_stream.test.ts index 56510da48f1bb..467d26a380945 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_stream.test.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_stream.test.ts @@ -11,9 +11,19 @@ import type { InstallParams } from '@kbn/data-stream-adapter'; import { DataStreamSpacesAdapter } from '@kbn/data-stream-adapter'; import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { loggerMock } from '@kbn/logging-mocks'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { securityServiceMock } from '@kbn/core-security-server-mocks'; jest.mock('@kbn/data-stream-adapter'); +// This mock is required to have a way to await the data stream name promise +const mockDataStreamNamePromise = jest.fn(); +jest.mock('./rule_migrations_data_client', () => ({ + RuleMigrationsDataClient: jest.fn((dataStreamNamePromise: Promise) => { + mockDataStreamNamePromise.mockReturnValue(dataStreamNamePromise); + }), +})); + const MockedDataStreamSpacesAdapter = DataStreamSpacesAdapter as unknown as jest.MockedClass< typeof DataStreamSpacesAdapter >; @@ -21,18 +31,21 @@ const MockedDataStreamSpacesAdapter = DataStreamSpacesAdapter as unknown as jest const esClient = elasticsearchServiceMock.createStart().client.asInternalUser; describe('SiemRuleMigrationsDataStream', () => { + const kibanaVersion = '8.16.0'; + const logger = loggingSystemMock.createLogger(); + beforeEach(() => { jest.clearAllMocks(); }); describe('constructor', () => { it('should create DataStreamSpacesAdapter', () => { - new RuleMigrationsDataStream({ kibanaVersion: '8.13.0' }); + new RuleMigrationsDataStream(logger, kibanaVersion); expect(MockedDataStreamSpacesAdapter).toHaveBeenCalledTimes(1); }); it('should create component templates', () => { - new RuleMigrationsDataStream({ kibanaVersion: '8.13.0' }); + new RuleMigrationsDataStream(logger, kibanaVersion); const [dataStreamSpacesAdapter] = MockedDataStreamSpacesAdapter.mock.instances; expect(dataStreamSpacesAdapter.setComponentTemplate).toHaveBeenCalledWith( expect.objectContaining({ name: '.kibana.siem-rule-migrations' }) @@ -40,7 +53,7 @@ describe('SiemRuleMigrationsDataStream', () => { }); it('should create index templates', () => { - new RuleMigrationsDataStream({ kibanaVersion: '8.13.0' }); + new RuleMigrationsDataStream(logger, kibanaVersion); const [dataStreamSpacesAdapter] = MockedDataStreamSpacesAdapter.mock.instances; expect(dataStreamSpacesAdapter.setIndexTemplate).toHaveBeenCalledWith( expect.objectContaining({ name: '.kibana.siem-rule-migrations' }) @@ -50,22 +63,20 @@ describe('SiemRuleMigrationsDataStream', () => { describe('install', () => { it('should install data stream', async () => { - const dataStream = new RuleMigrationsDataStream({ kibanaVersion: '8.13.0' }); - const params: InstallParams = { + const dataStream = new RuleMigrationsDataStream(logger, kibanaVersion); + const params: Omit = { esClient, - logger: loggerMock.create(), pluginStop$: new Subject(), }; await dataStream.install(params); const [dataStreamSpacesAdapter] = MockedDataStreamSpacesAdapter.mock.instances; - expect(dataStreamSpacesAdapter.install).toHaveBeenCalledWith(params); + expect(dataStreamSpacesAdapter.install).toHaveBeenCalledWith(expect.objectContaining(params)); }); it('should log error', async () => { - const dataStream = new RuleMigrationsDataStream({ kibanaVersion: '8.13.0' }); - const params: InstallParams = { + const dataStream = new RuleMigrationsDataStream(logger, kibanaVersion); + const params: Omit = { esClient, - logger: loggerMock.create(), pluginStop$: new Subject(), }; const [dataStreamSpacesAdapter] = MockedDataStreamSpacesAdapter.mock.instances; @@ -73,13 +84,16 @@ describe('SiemRuleMigrationsDataStream', () => { (dataStreamSpacesAdapter.install as jest.Mock).mockRejectedValueOnce(error); await dataStream.install(params); - expect(params.logger.error).toHaveBeenCalledWith(expect.any(String), error); + expect(logger.error).toHaveBeenCalledWith(expect.any(String), error); }); }); - describe('installSpace', () => { + describe('createClient', () => { + const currentUser = securityServiceMock.createMockAuthenticatedUser(); + const createClientParams = { spaceId: 'space1', currentUser, esClient }; + it('should install space data stream', async () => { - const dataStream = new RuleMigrationsDataStream({ kibanaVersion: '8.13.0' }); + const dataStream = new RuleMigrationsDataStream(logger, kibanaVersion); const params: InstallParams = { esClient, logger: loggerMock.create(), @@ -89,19 +103,23 @@ describe('SiemRuleMigrationsDataStream', () => { (dataStreamSpacesAdapter.install as jest.Mock).mockResolvedValueOnce(undefined); await dataStream.install(params); - await dataStream.installSpace('space1'); + dataStream.createClient(createClientParams); + await mockDataStreamNamePromise(); expect(dataStreamSpacesAdapter.getInstalledSpaceName).toHaveBeenCalledWith('space1'); expect(dataStreamSpacesAdapter.installSpace).toHaveBeenCalledWith('space1'); }); it('should not install space data stream if install not executed', async () => { - const dataStream = new RuleMigrationsDataStream({ kibanaVersion: '8.13.0' }); - await expect(dataStream.installSpace('space1')).rejects.toThrowError(); + const dataStream = new RuleMigrationsDataStream(logger, kibanaVersion); + await expect(async () => { + dataStream.createClient(createClientParams); + await mockDataStreamNamePromise(); + }).rejects.toThrowError(); }); it('should throw error if main install had error', async () => { - const dataStream = new RuleMigrationsDataStream({ kibanaVersion: '8.13.0' }); + const dataStream = new RuleMigrationsDataStream(logger, kibanaVersion); const params: InstallParams = { esClient, logger: loggerMock.create(), @@ -112,7 +130,10 @@ describe('SiemRuleMigrationsDataStream', () => { (dataStreamSpacesAdapter.install as jest.Mock).mockRejectedValueOnce(error); await dataStream.install(params); - await expect(dataStream.installSpace('space1')).rejects.toThrowError(error); + await expect(async () => { + dataStream.createClient(createClientParams); + await mockDataStreamNamePromise(); + }).rejects.toThrowError(error); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_stream.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_stream.ts index 83eb471e0cee3..a5855cefb1324 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_stream.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_data_stream.ts @@ -6,51 +6,69 @@ */ import { DataStreamSpacesAdapter, type InstallParams } from '@kbn/data-stream-adapter'; +import type { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server'; import { ruleMigrationsFieldMap } from './rule_migrations_field_map'; +import { RuleMigrationsDataClient } from './rule_migrations_data_client'; const TOTAL_FIELDS_LIMIT = 2500; const DATA_STREAM_NAME = '.kibana.siem-rule-migrations'; -const ECS_COMPONENT_TEMPLATE_NAME = 'ecs'; + +interface RuleMigrationsDataStreamCreateClientParams { + spaceId: string; + currentUser: AuthenticatedUser; + esClient: ElasticsearchClient; +} export class RuleMigrationsDataStream { - private readonly dataStream: DataStreamSpacesAdapter; + private readonly dataStreamAdapter: DataStreamSpacesAdapter; private installPromise?: Promise; - constructor({ kibanaVersion }: { kibanaVersion: string }) { - this.dataStream = new DataStreamSpacesAdapter(DATA_STREAM_NAME, { + constructor(private logger: Logger, kibanaVersion: string) { + this.dataStreamAdapter = new DataStreamSpacesAdapter(DATA_STREAM_NAME, { kibanaVersion, totalFieldsLimit: TOTAL_FIELDS_LIMIT, }); - this.dataStream.setComponentTemplate({ + this.dataStreamAdapter.setComponentTemplate({ name: DATA_STREAM_NAME, fieldMap: ruleMigrationsFieldMap, }); - this.dataStream.setIndexTemplate({ + this.dataStreamAdapter.setIndexTemplate({ name: DATA_STREAM_NAME, - componentTemplateRefs: [DATA_STREAM_NAME, ECS_COMPONENT_TEMPLATE_NAME], + componentTemplateRefs: [DATA_STREAM_NAME], }); } - async install(params: InstallParams) { + async install(params: Omit) { try { - this.installPromise = this.dataStream.install(params); + this.installPromise = this.dataStreamAdapter.install({ ...params, logger: this.logger }); await this.installPromise; } catch (err) { - params.logger.error(`Error installing siem rule migrations data stream. ${err.message}`, err); + this.logger.error(`Error installing siem rule migrations data stream. ${err.message}`, err); } } - async installSpace(spaceId: string): Promise { + createClient({ + spaceId, + currentUser, + esClient, + }: RuleMigrationsDataStreamCreateClientParams): RuleMigrationsDataClient { + const dataStreamNamePromise = this.installSpace(spaceId); + return new RuleMigrationsDataClient(dataStreamNamePromise, currentUser, esClient, this.logger); + } + + // Installs the data stream for the specific space. it will only install if it hasn't been installed yet. + // The adapter stores the data stream name promise, it will return it directly when the data stream is known to be installed. + private async installSpace(spaceId: string): Promise { if (!this.installPromise) { throw new Error('Siem rule migrations data stream not installed'); } // wait for install to complete, may reject if install failed, routes should handle this await this.installPromise; - let dataStreamName = await this.dataStream.getInstalledSpaceName(spaceId); + let dataStreamName = await this.dataStreamAdapter.getInstalledSpaceName(spaceId); if (!dataStreamName) { - dataStreamName = await this.dataStream.installSpace(spaceId); + dataStreamName = await this.dataStreamAdapter.installSpace(spaceId); } return dataStreamName; } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_field_map.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_field_map.ts index ba9a706957bcb..a65cd45b832e9 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_field_map.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data_stream/rule_migrations_field_map.ts @@ -11,6 +11,7 @@ import type { RuleMigration } from '../../../../../common/siem_migrations/model/ export const ruleMigrationsFieldMap: FieldMap> = { '@timestamp': { type: 'date', required: false }, migration_id: { type: 'keyword', required: true }, + created_by: { type: 'keyword', required: true }, status: { type: 'keyword', required: true }, original_rule: { type: 'nested', required: true }, 'original_rule.vendor': { type: 'keyword', required: true }, @@ -28,7 +29,7 @@ export const ruleMigrationsFieldMap: FieldMap> 'elastic_rule.severity': { type: 'keyword', required: false }, 'elastic_rule.prebuilt_rule_id': { type: 'keyword', required: false }, 'elastic_rule.id': { type: 'keyword', required: false }, - translation_state: { type: 'keyword', required: false }, + translation_result: { type: 'keyword', required: false }, comments: { type: 'text', array: true, required: false }, updated_at: { type: 'date', required: false }, updated_by: { type: 'keyword', required: false }, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.test.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.test.ts index 390d302264cea..5c611d85e0464 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.test.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.test.ts @@ -8,25 +8,28 @@ import { loggingSystemMock, elasticsearchServiceMock, httpServerMock, + securityServiceMock, } from '@kbn/core/server/mocks'; import { SiemRuleMigrationsService } from './siem_rule_migrations_service'; import { Subject } from 'rxjs'; -import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; import { MockRuleMigrationsDataStream, mockInstall, - mockInstallSpace, - mockIndexName, + mockCreateClient, } from './data_stream/__mocks__/mocks'; -import type { KibanaRequest } from '@kbn/core/server'; +import type { SiemRuleMigrationsCreateClientParams } from './types'; jest.mock('./data_stream/rule_migrations_data_stream'); +jest.mock('./task/rule_migrations_task_runner', () => ({ + RuleMigrationsTaskRunner: jest.fn(), +})); describe('SiemRuleMigrationsService', () => { let ruleMigrationsService: SiemRuleMigrationsService; const kibanaVersion = '8.16.0'; const esClusterClient = elasticsearchServiceMock.createClusterClient(); + const currentUser = securityServiceMock.createMockAuthenticatedUser(); const logger = loggingSystemMock.createLogger(); const pluginStop$ = new Subject(); @@ -36,7 +39,7 @@ describe('SiemRuleMigrationsService', () => { }); it('should instantiate the rule migrations data stream adapter', () => { - expect(MockRuleMigrationsDataStream).toHaveBeenCalledWith({ kibanaVersion }); + expect(MockRuleMigrationsDataStream).toHaveBeenCalledWith(logger, kibanaVersion); }); describe('when setup is called', () => { @@ -45,22 +48,26 @@ describe('SiemRuleMigrationsService', () => { expect(mockInstall).toHaveBeenCalledWith({ esClient: esClusterClient.asInternalUser, - logger, pluginStop$, }); }); }); - describe('when getClient is called', () => { - let request: KibanaRequest; + describe('when createClient is called', () => { + let createClientParams: SiemRuleMigrationsCreateClientParams; + beforeEach(() => { - request = httpServerMock.createKibanaRequest(); + createClientParams = { + spaceId: 'default', + currentUser, + request: httpServerMock.createKibanaRequest(), + }; }); describe('without setup', () => { it('should throw an error', () => { expect(() => { - ruleMigrationsService.getClient({ spaceId: 'default', request }); + ruleMigrationsService.createClient(createClientParams); }).toThrowError('ES client not available, please call setup first'); }); }); @@ -71,44 +78,19 @@ describe('SiemRuleMigrationsService', () => { }); it('should call installSpace', () => { - ruleMigrationsService.getClient({ spaceId: 'default', request }); - - expect(mockInstallSpace).toHaveBeenCalledWith('default'); - }); - - it('should return a client with create and search methods after setup', () => { - const client = ruleMigrationsService.getClient({ spaceId: 'default', request }); - - expect(client).toHaveProperty('create'); - expect(client).toHaveProperty('search'); + ruleMigrationsService.createClient(createClientParams); + expect(mockCreateClient).toHaveBeenCalledWith({ + spaceId: createClientParams.spaceId, + currentUser: createClientParams.currentUser, + esClient: esClusterClient.asScoped().asCurrentUser, + }); }); - it('should call ES bulk create API with the correct parameters with create is called', async () => { - const client = ruleMigrationsService.getClient({ spaceId: 'default', request }); - - const ruleMigrations = [{ migration_id: 'dummy_migration_id' } as RuleMigration]; - await client.create(ruleMigrations); - - expect(esClusterClient.asScoped().asCurrentUser.bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: [{ create: { _index: mockIndexName } }, { migration_id: 'dummy_migration_id' }], - refresh: 'wait_for', - }) - ); - }); - - it('should call ES search API with the correct parameters with search is called', async () => { - const client = ruleMigrationsService.getClient({ spaceId: 'default', request }); - - const term = { migration_id: 'dummy_migration_id' }; - await client.search(term); + it('should return data and task clients', () => { + const client = ruleMigrationsService.createClient(createClientParams); - expect(esClusterClient.asScoped().asCurrentUser.search).toHaveBeenCalledWith( - expect.objectContaining({ - index: mockIndexName, - body: { query: { term } }, - }) - ); + expect(client).toHaveProperty('data'); + expect(client).toHaveProperty('task'); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts index 5b20f957cb6fa..1bf9dcf11fd95 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts @@ -5,52 +5,67 @@ * 2.0. */ +import assert from 'assert'; import type { IClusterClient, Logger } from '@kbn/core/server'; import { RuleMigrationsDataStream } from './data_stream/rule_migrations_data_stream'; import type { - SiemRuleMigrationsClient, SiemRulesMigrationsSetupParams, - SiemRuleMigrationsGetClientParams, + SiemRuleMigrationsCreateClientParams, + SiemRuleMigrationsClient, } from './types'; +import { RuleMigrationsTaskRunner } from './task/rule_migrations_task_runner'; export class SiemRuleMigrationsService { - private dataStreamAdapter: RuleMigrationsDataStream; + private rulesDataStream: RuleMigrationsDataStream; private esClusterClient?: IClusterClient; + private taskRunner: RuleMigrationsTaskRunner; constructor(private logger: Logger, kibanaVersion: string) { - this.dataStreamAdapter = new RuleMigrationsDataStream({ kibanaVersion }); + this.rulesDataStream = new RuleMigrationsDataStream(this.logger, kibanaVersion); + this.taskRunner = new RuleMigrationsTaskRunner(this.logger); } setup({ esClusterClient, ...params }: SiemRulesMigrationsSetupParams) { this.esClusterClient = esClusterClient; const esClient = esClusterClient.asInternalUser; - this.dataStreamAdapter.install({ ...params, esClient, logger: this.logger }).catch((err) => { + + this.rulesDataStream.install({ ...params, esClient }).catch((err) => { this.logger.error(`Error installing data stream for rule migrations: ${err.message}`); throw err; }); } - getClient({ spaceId, request }: SiemRuleMigrationsGetClientParams): SiemRuleMigrationsClient { - if (!this.esClusterClient) { - throw new Error('ES client not available, please call setup first'); - } - // Installs the data stream for the specific space. it will only install if it hasn't been installed yet. - // The adapter stores the data stream name promise, it will return it directly when the data stream is known to be installed. - const dataStreamNamePromise = this.dataStreamAdapter.installSpace(spaceId); + createClient({ + spaceId, + currentUser, + request, + }: SiemRuleMigrationsCreateClientParams): SiemRuleMigrationsClient { + assert(currentUser, 'Current user must be authenticated'); + assert(this.esClusterClient, 'ES client not available, please call setup first'); const esClient = this.esClusterClient.asScoped(request).asCurrentUser; + const dataClient = this.rulesDataStream.createClient({ spaceId, currentUser, esClient }); + return { - create: async (ruleMigrations) => { - const _index = await dataStreamNamePromise; - return esClient.bulk({ - refresh: 'wait_for', - body: ruleMigrations.flatMap((ruleMigration) => [{ create: { _index } }, ruleMigration]), - }); - }, - search: async (term) => { - const index = await dataStreamNamePromise; - return esClient.search({ index, body: { query: { term } } }); + data: dataClient, + task: { + start: (params) => { + return this.taskRunner.start({ ...params, currentUser, dataClient }); + }, + stop: (migrationId) => { + return this.taskRunner.stop({ migrationId, dataClient }); + }, + getStats: async (migrationId) => { + return this.taskRunner.getStats({ migrationId, dataClient }); + }, + getAllStats: async () => { + return this.taskRunner.getAllStats({ dataClient }); + }, }, }; } + + stop() { + this.taskRunner.stopAll(); + } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts new file mode 100644 index 0000000000000..a44197d82850f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/graph.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { END, START, StateGraph } from '@langchain/langgraph'; +import { migrateRuleState } from './state'; +import type { MigrateRuleGraphParams, MigrateRuleState } from './types'; +import { getTranslateQueryNode } from './nodes/translate_query'; +import { getMatchPrebuiltRuleNode } from './nodes/match_prebuilt_rule'; + +export function getRuleMigrationAgent({ + model, + inferenceClient, + prebuiltRulesMap, + connectorId, + logger, +}: MigrateRuleGraphParams) { + const matchPrebuiltRuleNode = getMatchPrebuiltRuleNode({ model, prebuiltRulesMap, logger }); + const translationNode = getTranslateQueryNode({ inferenceClient, connectorId, logger }); + + const translateRuleGraph = new StateGraph(migrateRuleState) + // Nodes + .addNode('matchPrebuiltRule', matchPrebuiltRuleNode) + .addNode('translation', translationNode) + // Edges + .addEdge(START, 'matchPrebuiltRule') + .addConditionalEdges('matchPrebuiltRule', matchedPrebuiltRuleConditional) + .addEdge('translation', END); + + const graph = translateRuleGraph.compile(); + graph.name = 'Rule Migration Graph'; // Customizes the name displayed in LangSmith + return graph; +} + +const matchedPrebuiltRuleConditional = (state: MigrateRuleState) => { + if (state.elastic_rule?.prebuilt_rule_id) { + return END; + } + return 'translation'; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/index.ts new file mode 100644 index 0000000000000..febf5fc85f5a0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getRuleMigrationAgent } from './graph'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/index.ts new file mode 100644 index 0000000000000..2d8b81d00eafb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getMatchPrebuiltRuleNode } from './match_prebuilt_rule'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts new file mode 100644 index 0000000000000..4a0404acf653d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import { StringOutputParser } from '@langchain/core/output_parsers'; +import type { ChatModel } from '../../../util/actions_client_chat'; +import type { GraphNode } from '../../types'; +import { filterPrebuiltRules, type PrebuiltRulesMapByName } from '../../../util/prebuilt_rules'; +import { MATCH_PREBUILT_RULE_PROMPT } from './prompts'; + +interface GetMatchPrebuiltRuleNodeParams { + model: ChatModel; + prebuiltRulesMap: PrebuiltRulesMapByName; + logger: Logger; +} + +export const getMatchPrebuiltRuleNode = + ({ model, prebuiltRulesMap }: GetMatchPrebuiltRuleNodeParams): GraphNode => + async (state) => { + const mitreAttackIds = state.original_rule.mitre_attack_ids; + if (!mitreAttackIds?.length) { + return {}; + } + const filteredPrebuiltRulesMap = filterPrebuiltRules(prebuiltRulesMap, mitreAttackIds); + if (filteredPrebuiltRulesMap.size === 0) { + return {}; + } + + const outputParser = new StringOutputParser(); + const matchPrebuiltRule = MATCH_PREBUILT_RULE_PROMPT.pipe(model).pipe(outputParser); + + const elasticSecurityRules = Array(filteredPrebuiltRulesMap.keys()).join('\n'); + const response = await matchPrebuiltRule.invoke({ + elasticSecurityRules, + ruleTitle: state.original_rule.title, + }); + const cleanResponse = response.trim(); + if (cleanResponse === 'no_match') { + return {}; + } + + const result = filteredPrebuiltRulesMap.get(cleanResponse); + if (result != null) { + return { + elastic_rule: { + title: result.rule.name, + description: result.rule.description, + prebuilt_rule_id: result.rule.rule_id, + id: result.installedRuleId, + }, + }; + } + + return {}; + }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/prompts.ts new file mode 100644 index 0000000000000..434636d0519b1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/prompts.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChatPromptTemplate } from '@langchain/core/prompts'; +export const MATCH_PREBUILT_RULE_PROMPT = ChatPromptTemplate.fromMessages([ + [ + 'system', + `You are an expert assistant in Cybersecurity, your task is to help migrating a SIEM detection rule, from Splunk Security to Elastic Security. +You will be provided with a Splunk Detection Rule name by the user, your goal is to try find an Elastic Detection Rule that covers the same threat, if any. +The list of Elastic Detection Rules suggested is provided in the context below. + +Guidelines: +If there is no Elastic rule in the list that covers the same threat, answer only with the string: no_match +If there is one Elastic rule in the list that covers the same threat, answer only with its name without any further explanation. +If there are multiple rules in the list that cover the same threat, answer with the most specific of them, for example: "Linux User Account Creation" is more specific than "User Account Creation". + + +{elasticSecurityRules} + +`, + ], + [ + 'human', + `The Splunk Detection Rule is: +<> +{ruleTitle} +<> +`, + ], + ['ai', 'Please find the answer below:'], +]); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/esql_knowledge_base_caller.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/esql_knowledge_base_caller.ts new file mode 100644 index 0000000000000..2277f2fae41a9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/esql_knowledge_base_caller.ts @@ -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 type { Logger } from '@kbn/core/server'; +import { naturalLanguageToEsql, type InferenceClient } from '@kbn/inference-plugin/server'; +import { lastValueFrom } from 'rxjs'; + +export type EsqlKnowledgeBaseCaller = (input: string) => Promise; + +type GetEsqlTranslatorToolParams = (params: { + inferenceClient: InferenceClient; + connectorId: string; + logger: Logger; +}) => EsqlKnowledgeBaseCaller; + +export const getEsqlKnowledgeBase: GetEsqlTranslatorToolParams = + ({ inferenceClient: client, connectorId, logger }) => + async (input: string) => { + const { content } = await lastValueFrom( + naturalLanguageToEsql({ + client, + connectorId, + input, + logger: { + debug: (source) => { + logger.debug(typeof source === 'function' ? source() : source); + }, + }, + }) + ); + return content; + }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/index.ts new file mode 100644 index 0000000000000..7d247f755e9da --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { getTranslateQueryNode } from './translate_query'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompt.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompt.ts new file mode 100644 index 0000000000000..0b97faf7dc96f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/prompt.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MigrateRuleState } from '../../types'; + +export const getEsqlTranslationPrompt = ( + state: MigrateRuleState +): string => `You are a helpful cybersecurity (SIEM) expert agent. Your task is to migrate "detection rules" from Splunk to Elastic Security. +Below you will find Splunk rule information: the title, description and the SPL (Search Processing Language) query. +Your goal is to translate the SPL query into an equivalent Elastic Security Query Language (ES|QL) query. + +Guidelines: +- Start the translation process by analyzing the SPL query and identifying the key components. +- Always use logs* index pattern for the ES|QL translated query. +- If, in the SPL query, you find a lookup list or macro that, based only on its name, you can not translate with confidence to ES|QL, mention it in the summary and +add a placeholder in the query with the format [macro:(parameters)] or [lookup:] including the [] keys, example: [macro:my_macro(first_param,second_param)] or [lookup:my_lookup]. + +The output will be parsed and should contain: +- First, the ES|QL query inside an \`\`\`esql code block. +- At the end, the summary of the translation process followed in markdown, starting with "## Migration Summary". + +This is the Splunk rule information: + +<> +${state.original_rule.title} +<> + +<> +${state.original_rule.description} +<> + +<> +${state.original_rule.query} +<> +`; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/translate_query.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/translate_query.ts new file mode 100644 index 0000000000000..00e1e60c7b5f3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/translate_query/translate_query.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import type { InferenceClient } from '@kbn/inference-plugin/server'; +import type { GraphNode } from '../../types'; +import { getEsqlKnowledgeBase } from './esql_knowledge_base_caller'; +import { getEsqlTranslationPrompt } from './prompt'; +import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; + +interface GetTranslateQueryNodeParams { + inferenceClient: InferenceClient; + connectorId: string; + logger: Logger; +} + +export const getTranslateQueryNode = ({ + inferenceClient, + connectorId, + logger, +}: GetTranslateQueryNodeParams): GraphNode => { + const esqlKnowledgeBaseCaller = getEsqlKnowledgeBase({ inferenceClient, connectorId, logger }); + return async (state) => { + const input = getEsqlTranslationPrompt(state); + const response = await esqlKnowledgeBaseCaller(input); + + const esqlQuery = response.match(/```esql\n([\s\S]*?)\n```/)?.[1] ?? ''; + const summary = response.match(/## Migration Summary[\s\S]*$/)?.[0] ?? ''; + + const translationResult = getTranslationResult(esqlQuery); + + return { + response, + comments: [summary], + translation_result: translationResult, + elastic_rule: { + title: state.original_rule.title, + description: state.original_rule.description, + severity: 'low', + query: esqlQuery, + query_language: 'esql', + }, + }; + }; +}; + +const getTranslationResult = (esqlQuery: string): SiemMigrationRuleTranslationResult => { + if (esqlQuery.match(/\[(macro|lookup):[\s\S]*\]/)) { + return SiemMigrationRuleTranslationResult.PARTIAL; + } + return SiemMigrationRuleTranslationResult.FULL; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts new file mode 100644 index 0000000000000..c1e510bdc052d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { BaseMessage } from '@langchain/core/messages'; +import { Annotation, messagesStateReducer } from '@langchain/langgraph'; +import type { + ElasticRule, + OriginalRule, + RuleMigration, +} from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { SiemMigrationRuleTranslationResult } from '../../../../../../common/siem_migrations/constants'; + +export const migrateRuleState = Annotation.Root({ + messages: Annotation({ + reducer: messagesStateReducer, + default: () => [], + }), + original_rule: Annotation(), + elastic_rule: Annotation({ + reducer: (state, action) => ({ ...state, ...action }), + }), + translation_result: Annotation(), + comments: Annotation({ + reducer: (current, value) => (value ? (current ?? []).concat(value) : current), + default: () => [], + }), + response: Annotation(), +}); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/types.ts new file mode 100644 index 0000000000000..643d200e4b0bf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import type { InferenceClient } from '@kbn/inference-plugin/server'; +import type { migrateRuleState } from './state'; +import type { ChatModel } from '../util/actions_client_chat'; +import type { PrebuiltRulesMapByName } from '../util/prebuilt_rules'; + +export type MigrateRuleState = typeof migrateRuleState.State; +export type GraphNode = (state: MigrateRuleState) => Promise>; + +export interface MigrateRuleGraphParams { + inferenceClient: InferenceClient; + model: ChatModel; + connectorId: string; + prebuiltRulesMap: PrebuiltRulesMapByName; + logger: Logger; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_runner.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_runner.ts new file mode 100644 index 0000000000000..6ae7294fb5257 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_runner.ts @@ -0,0 +1,285 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import { AbortError, abortSignalToPromise } from '@kbn/kibana-utils-plugin/server'; +import type { RunnableConfig } from '@langchain/core/runnables'; +import type { + RuleMigrationAllTaskStats, + RuleMigrationTaskStats, +} from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationDataStats } from '../data_stream/rule_migrations_data_client'; +import type { + RuleMigrationTaskStartParams, + RuleMigrationTaskStartResult, + RuleMigrationTaskStatsParams, + RuleMigrationTaskStopParams, + RuleMigrationTaskStopResult, + RuleMigrationTaskPrepareParams, + RuleMigrationTaskRunParams, + MigrationAgent, + RuleMigrationAllTaskStatsParams, +} from './types'; +import { getRuleMigrationAgent } from './agent'; +import type { MigrateRuleState } from './agent/types'; +import { retrievePrebuiltRulesMap } from './util/prebuilt_rules'; +import { ActionsClientChat } from './util/actions_client_chat'; + +interface TaskLogger { + info: (msg: string) => void; + debug: (msg: string) => void; + error: (msg: string, error: Error) => void; +} +const getTaskLogger = (logger: Logger): TaskLogger => { + const prefix = '[ruleMigrationsTask]: '; + return { + info: (msg) => logger.info(`${prefix}${msg}`), + debug: (msg) => logger.debug(`${prefix}${msg}`), + error: (msg, error) => logger.error(`${prefix}${msg}: ${error.message}`), + }; +}; + +const ITERATION_BATCH_SIZE = 50 as const; +const ITERATION_SLEEP_SECONDS = 10 as const; + +export class RuleMigrationsTaskRunner { + private migrationsRunning: Map; + private taskLogger: TaskLogger; + + constructor(private logger: Logger) { + this.migrationsRunning = new Map(); + this.taskLogger = getTaskLogger(logger); + } + + /** Starts a rule migration task */ + async start(params: RuleMigrationTaskStartParams): Promise { + const { migrationId, dataClient } = params; + if (this.migrationsRunning.has(migrationId)) { + return { exists: true, started: false }; + } + // Just in case some previous execution was interrupted without releasing + await dataClient.releaseProcessable(migrationId); + + const { rules } = await dataClient.getStats(migrationId); + if (rules.total === 0) { + return { exists: false, started: false }; + } + if (rules.pending === 0) { + return { exists: true, started: false }; + } + + const abortController = new AbortController(); + + // Await the preparation to make sure the agent is created properly so the task can run + const agent = await this.prepare({ ...params, abortController }); + + // not awaiting the `run` promise to execute the task in the background + this.run({ ...params, agent, abortController }).catch((err) => { + // All errors in the `run` method are already catch, this should never happen, but just in case + this.taskLogger.error(`Unexpected error running the migration ID:${migrationId}`, err); + }); + + return { exists: true, started: true }; + } + + private async prepare({ + connectorId, + inferenceClient, + actionsClient, + rulesClient, + soClient, + abortController, + }: RuleMigrationTaskPrepareParams): Promise { + const prebuiltRulesMap = await retrievePrebuiltRulesMap({ soClient, rulesClient }); + + const actionsClientChat = new ActionsClientChat(connectorId, actionsClient, this.logger); + const model = await actionsClientChat.createModel({ + signal: abortController.signal, + temperature: 0.05, + }); + + const agent = getRuleMigrationAgent({ + connectorId, + model, + inferenceClient, + prebuiltRulesMap, + logger: this.logger, + }); + return agent; + } + + private async run({ + migrationId, + agent, + dataClient, + currentUser, + invocationConfig, + abortController, + }: RuleMigrationTaskRunParams): Promise { + if (this.migrationsRunning.has(migrationId)) { + // This should never happen, but just in case + throw new Error(`Task already running for migration ID:${migrationId} `); + } + this.taskLogger.info(`Starting migration ID:${migrationId}`); + + this.migrationsRunning.set(migrationId, { user: currentUser.username, abortController }); + const config: RunnableConfig = { + ...invocationConfig, + // signal: abortController.signal, // not working properly https://github.com/langchain-ai/langgraphjs/issues/319 + }; + + const abortPromise = abortSignalToPromise(abortController.signal); + + try { + const sleep = async (seconds: number) => { + this.taskLogger.debug(`Sleeping ${seconds}s for migration ID:${migrationId}`); + await Promise.race([ + new Promise((resolve) => setTimeout(resolve, seconds * 1000)), + abortPromise.promise, + ]); + }; + + let isDone: boolean = false; + do { + const ruleMigrations = await dataClient.takePending(migrationId, ITERATION_BATCH_SIZE); + this.taskLogger.debug( + `Processing ${ruleMigrations.length} rules for migration ID:${migrationId}` + ); + + await Promise.all( + ruleMigrations.map(async (ruleMigration) => { + this.taskLogger.debug( + `Starting migration of rule "${ruleMigration.original_rule.title}"` + ); + try { + const start = Date.now(); + + const ruleMigrationResult: MigrateRuleState = await Promise.race([ + agent.invoke({ original_rule: ruleMigration.original_rule }, config), + abortPromise.promise, // workaround for the issue with the langGraph signal + ]); + + const duration = (Date.now() - start) / 1000; + this.taskLogger.debug( + `Migration of rule "${ruleMigration.original_rule.title}" finished in ${duration}s` + ); + + await dataClient.saveFinished({ + ...ruleMigration, + elastic_rule: ruleMigrationResult.elastic_rule, + translation_result: ruleMigrationResult.translation_result, + comments: ruleMigrationResult.comments, + }); + } catch (error) { + if (error instanceof AbortError) { + throw error; + } + this.taskLogger.error( + `Error migrating rule "${ruleMigration.original_rule.title}"`, + error + ); + await dataClient.saveError({ + ...ruleMigration, + comments: [`Error migrating rule: ${error.message}`], + }); + } + }) + ); + + this.taskLogger.debug(`Batch processed successfully for migration ID:${migrationId}`); + + const { rules } = await dataClient.getStats(migrationId); + isDone = rules.pending === 0; + if (!isDone) { + await sleep(ITERATION_SLEEP_SECONDS); + } + } while (!isDone); + + this.taskLogger.info(`Finished migration ID:${migrationId}`); + } catch (error) { + await dataClient.releaseProcessing(migrationId); + + if (error instanceof AbortError) { + this.taskLogger.info(`Abort signal received, stopping migration ID:${migrationId}`); + return; + } else { + this.taskLogger.error(`Error processing migration ID:${migrationId}`, error); + } + } finally { + this.migrationsRunning.delete(migrationId); + abortPromise.cleanup(); + } + } + + /** Returns the stats of a migration */ + async getStats({ + migrationId, + dataClient, + }: RuleMigrationTaskStatsParams): Promise { + const dataStats = await dataClient.getStats(migrationId); + const status = this.getTaskStatus(migrationId, dataStats.rules); + return { status, ...dataStats }; + } + + /** Returns the stats of all migrations */ + async getAllStats({ + dataClient, + }: RuleMigrationAllTaskStatsParams): Promise { + const allDataStats = await dataClient.getAllStats(); + return allDataStats.map((dataStats) => { + const status = this.getTaskStatus(dataStats.migration_id, dataStats.rules); + return { status, ...dataStats }; + }); + } + + private getTaskStatus( + migrationId: string, + dataStats: RuleMigrationDataStats['rules'] + ): RuleMigrationTaskStats['status'] { + if (this.migrationsRunning.has(migrationId)) { + return 'running'; + } + if (dataStats.pending === dataStats.total) { + return 'ready'; + } + if (dataStats.completed + dataStats.failed === dataStats.total) { + return 'finished'; + } + return 'stopped'; + } + + /** Stops one running migration */ + async stop({ + migrationId, + dataClient, + }: RuleMigrationTaskStopParams): Promise { + try { + const migrationRunning = this.migrationsRunning.get(migrationId); + if (migrationRunning) { + migrationRunning.abortController.abort(); + return { exists: true, stopped: true }; + } + + const { rules } = await dataClient.getStats(migrationId); + if (rules.total > 0) { + return { exists: true, stopped: false }; + } + return { exists: false, stopped: false }; + } catch (err) { + this.taskLogger.error(`Error stopping migration ID:${migrationId}`, err); + return { exists: true, stopped: false }; + } + } + + /** Stops all running migrations */ + stopAll() { + this.migrationsRunning.forEach((migrationRunning) => { + migrationRunning.abortController.abort(); + }); + this.migrationsRunning.clear(); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/types.ts new file mode 100644 index 0000000000000..e26a5b7216f48 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/types.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AuthenticatedUser, SavedObjectsClientContract } from '@kbn/core/server'; +import type { RunnableConfig } from '@langchain/core/runnables'; +import type { InferenceClient } from '@kbn/inference-plugin/server'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { RuleMigrationsDataClient } from '../data_stream/rule_migrations_data_client'; +import type { getRuleMigrationAgent } from './agent'; + +export type MigrationAgent = ReturnType; + +export interface RuleMigrationTaskStartParams { + migrationId: string; + currentUser: AuthenticatedUser; + connectorId: string; + invocationConfig: RunnableConfig; + inferenceClient: InferenceClient; + actionsClient: ActionsClient; + rulesClient: RulesClient; + soClient: SavedObjectsClientContract; + dataClient: RuleMigrationsDataClient; +} + +export interface RuleMigrationTaskPrepareParams { + connectorId: string; + inferenceClient: InferenceClient; + actionsClient: ActionsClient; + rulesClient: RulesClient; + soClient: SavedObjectsClientContract; + abortController: AbortController; +} + +export interface RuleMigrationTaskRunParams { + migrationId: string; + currentUser: AuthenticatedUser; + invocationConfig: RunnableConfig; + agent: MigrationAgent; + dataClient: RuleMigrationsDataClient; + abortController: AbortController; +} + +export interface RuleMigrationTaskStopParams { + migrationId: string; + dataClient: RuleMigrationsDataClient; +} + +export interface RuleMigrationTaskStatsParams { + migrationId: string; + dataClient: RuleMigrationsDataClient; +} + +export interface RuleMigrationAllTaskStatsParams { + dataClient: RuleMigrationsDataClient; +} + +export interface RuleMigrationTaskStartResult { + started: boolean; + exists: boolean; +} + +export interface RuleMigrationTaskStopResult { + stopped: boolean; + exists: boolean; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/actions_client_chat.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/actions_client_chat.ts new file mode 100644 index 0000000000000..204978c901df6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/actions_client_chat.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ActionsClientSimpleChatModel } from '@kbn/langchain/server'; +import { + ActionsClientBedrockChatModel, + ActionsClientChatOpenAI, + ActionsClientChatVertexAI, +} from '@kbn/langchain/server'; +import type { Logger } from '@kbn/core/server'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import type { ActionsClientChatOpenAIParams } from '@kbn/langchain/server/language_models/chat_openai'; +import type { CustomChatModelInput as ActionsClientBedrockChatModelParams } from '@kbn/langchain/server/language_models/bedrock_chat'; +import type { CustomChatModelInput as ActionsClientChatVertexAIParams } from '@kbn/langchain/server/language_models/gemini_chat'; +import type { CustomChatModelInput as ActionsClientSimpleChatModelParams } from '@kbn/langchain/server/language_models/simple_chat_model'; + +export type ChatModel = + | ActionsClientSimpleChatModel + | ActionsClientChatOpenAI + | ActionsClientBedrockChatModel + | ActionsClientChatVertexAI; + +export type ActionsClientChatModelClass = + | typeof ActionsClientSimpleChatModel + | typeof ActionsClientChatOpenAI + | typeof ActionsClientBedrockChatModel + | typeof ActionsClientChatVertexAI; + +export type ChatModelParams = Partial & + Partial & + Partial & + Partial & { + /** Enables the streaming mode of the response, disabled by default */ + streaming?: boolean; + }; + +const llmTypeDictionary: Record = { + [`.gen-ai`]: `openai`, + [`.bedrock`]: `bedrock`, + [`.gemini`]: `gemini`, +}; + +export class ActionsClientChat { + constructor( + private readonly connectorId: string, + private readonly actionsClient: ActionsClient, + private readonly logger: Logger + ) {} + + public async createModel(params?: ChatModelParams): Promise { + const connector = await this.actionsClient.get({ id: this.connectorId }); + if (!connector) { + throw new Error(`Connector not found: ${this.connectorId}`); + } + + const llmType = this.getLLMType(connector.actionTypeId); + const ChatModelClass = this.getLLMClass(llmType); + + const model = new ChatModelClass({ + actionsClient: this.actionsClient, + connectorId: this.connectorId, + logger: this.logger, + llmType, + model: connector.config?.defaultModel, + ...params, + streaming: params?.streaming ?? false, // disabling streaming by default, for some reason is enabled when omitted + }); + return model; + } + + private getLLMType(actionTypeId: string): string | undefined { + if (llmTypeDictionary[actionTypeId]) { + return llmTypeDictionary[actionTypeId]; + } + throw new Error(`Unknown LLM type for action type ID: ${actionTypeId}`); + } + + private getLLMClass(llmType?: string): ActionsClientChatModelClass { + switch (llmType) { + case 'bedrock': + return ActionsClientBedrockChatModel; + case 'gemini': + return ActionsClientChatVertexAI; + case 'openai': + default: + return ActionsClientChatOpenAI; + } + } +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/prebuilt_rules.test.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/prebuilt_rules.test.ts new file mode 100644 index 0000000000000..55256d0ad0fdd --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/prebuilt_rules.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import type { PrebuiltRulesMapByName } from './prebuilt_rules'; +import { filterPrebuiltRules, retrievePrebuiltRulesMap } from './prebuilt_rules'; +import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; + +jest.mock( + '../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client', + () => ({ createPrebuiltRuleObjectsClient: jest.fn() }) +); +jest.mock( + '../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client', + () => ({ createPrebuiltRuleAssetsClient: jest.fn() }) +); + +const mitreAttackIds = 'T1234'; +const rule1 = { + name: 'rule one', + id: 'rule1', + threat: [ + { + framework: 'MITRE ATT&CK', + technique: [{ id: mitreAttackIds, name: 'tactic one' }], + }, + ], +}; +const rule2 = { + name: 'rule two', + id: 'rule2', +}; + +const defaultRuleVersionsTriad = new Map([ + ['rule1', { target: rule1 }], + ['rule2', { target: rule2, current: rule2 }], +]); +const mockFetchRuleVersionsTriad = jest.fn().mockResolvedValue(defaultRuleVersionsTriad); +jest.mock( + '../../../../detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad', + () => ({ + fetchRuleVersionsTriad: () => mockFetchRuleVersionsTriad(), + }) +); + +const defaultParams = { + soClient: savedObjectsClientMock.create(), + rulesClient: rulesClientMock.create(), +}; + +describe('retrievePrebuiltRulesMap', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('when prebuilt rule is installed', () => { + it('should return isInstalled flag', async () => { + const prebuiltRulesMap = await retrievePrebuiltRulesMap(defaultParams); + expect(prebuiltRulesMap.size).toBe(2); + expect(prebuiltRulesMap.get('rule one')).toEqual( + expect.objectContaining({ installedRuleId: undefined }) + ); + expect(prebuiltRulesMap.get('rule two')).toEqual( + expect.objectContaining({ installedRuleId: rule2.id }) + ); + }); + }); +}); + +describe('filterPrebuiltRules', () => { + let prebuiltRulesMap: PrebuiltRulesMapByName; + + beforeEach(async () => { + prebuiltRulesMap = await retrievePrebuiltRulesMap(defaultParams); + jest.clearAllMocks(); + }); + + describe('when splunk rule contains empty mitreAttackIds', () => { + it('should return empty rules map', async () => { + const filteredPrebuiltRules = filterPrebuiltRules(prebuiltRulesMap, []); + expect(filteredPrebuiltRules.size).toBe(0); + }); + }); + + describe('when splunk rule does not match mitreAttackIds', () => { + it('should return empty rules map', async () => { + const filteredPrebuiltRules = filterPrebuiltRules(prebuiltRulesMap, [`${mitreAttackIds}_2`]); + expect(filteredPrebuiltRules.size).toBe(0); + }); + }); + + describe('when splunk rule contains matching mitreAttackIds', () => { + it('should return the filtered rules map', async () => { + const filteredPrebuiltRules = filterPrebuiltRules(prebuiltRulesMap, [mitreAttackIds]); + expect(filteredPrebuiltRules.size).toBe(1); + expect(filteredPrebuiltRules.get('rule one')).toEqual( + expect.objectContaining({ rule: rule1 }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/prebuilt_rules.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/prebuilt_rules.ts new file mode 100644 index 0000000000000..ade6632aaa5b5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/prebuilt_rules.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import type { PrebuiltRuleAsset } from '../../../../detection_engine/prebuilt_rules'; +import { fetchRuleVersionsTriad } from '../../../../detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad'; +import { createPrebuiltRuleObjectsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client'; +import { createPrebuiltRuleAssetsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; + +export interface PrebuiltRuleMapped { + rule: PrebuiltRuleAsset; + installedRuleId?: string; +} + +export type PrebuiltRulesMapByName = Map; + +interface RetrievePrebuiltRulesParams { + soClient: SavedObjectsClientContract; + rulesClient: RulesClient; +} + +export const retrievePrebuiltRulesMap = async ({ + soClient, + rulesClient, +}: RetrievePrebuiltRulesParams): Promise => { + const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + + const prebuiltRulesMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, + }); + const prebuiltRulesByName: PrebuiltRulesMapByName = new Map(); + prebuiltRulesMap.forEach((ruleVersions) => { + const rule = ruleVersions.target || ruleVersions.current; + if (rule) { + prebuiltRulesByName.set(rule.name, { + rule, + installedRuleId: ruleVersions.current?.id, + }); + } + }); + return prebuiltRulesByName; +}; + +export const filterPrebuiltRules = ( + prebuiltRulesByName: PrebuiltRulesMapByName, + mitreAttackIds: string[] +) => { + const filteredPrebuiltRulesByName = new Map(); + if (mitreAttackIds?.length) { + // If this rule has MITRE ATT&CK IDs, remove unrelated prebuilt rules + prebuiltRulesByName.forEach(({ rule }, ruleName) => { + const mitreAttackThreat = rule.threat?.filter( + ({ framework }) => framework === 'MITRE ATT&CK' + ); + if (!mitreAttackThreat) { + // If this rule has no MITRE ATT&CK reference we skip it + return; + } + + const sameTechnique = mitreAttackThreat.find((threat) => + threat.technique?.some(({ id }) => mitreAttackIds?.includes(id)) + ); + + if (sameTechnique) { + filteredPrebuiltRulesByName.set(ruleName, prebuiltRulesByName.get(ruleName)); + } + }); + } + return filteredPrebuiltRulesByName; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/types.ts index 1892032a21723..78ec2ef89c7a3 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/types.ts @@ -5,10 +5,29 @@ * 2.0. */ -import type { BulkResponse, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import type { IClusterClient, KibanaRequest } from '@kbn/core/server'; +import type { + AuthenticatedUser, + IClusterClient, + KibanaRequest, + SavedObjectsClientContract, +} from '@kbn/core/server'; import type { Subject } from 'rxjs'; -import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import type { InferenceClient } from '@kbn/inference-plugin/server'; +import type { RunnableConfig } from '@langchain/core/runnables'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { + RuleMigration, + RuleMigrationAllTaskStats, + RuleMigrationTaskStats, +} from '../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationsDataClient } from './data_stream/rule_migrations_data_client'; +import type { RuleMigrationTaskStopResult, RuleMigrationTaskStartResult } from './task/types'; + +export interface StoredRuleMigration extends RuleMigration { + _id: string; + _index: string; +} export interface SiemRulesMigrationsSetupParams { esClusterClient: IClusterClient; @@ -16,15 +35,28 @@ export interface SiemRulesMigrationsSetupParams { tasksTimeoutMs?: number; } -export interface SiemRuleMigrationsGetClientParams { +export interface SiemRuleMigrationsCreateClientParams { request: KibanaRequest; + currentUser: AuthenticatedUser | null; spaceId: string; } -export interface RuleMigrationSearchParams { - migration_id?: string; +export interface SiemRuleMigrationsStartTaskParams { + migrationId: string; + connectorId: string; + invocationConfig: RunnableConfig; + inferenceClient: InferenceClient; + actionsClient: ActionsClient; + rulesClient: RulesClient; + soClient: SavedObjectsClientContract; } + export interface SiemRuleMigrationsClient { - create: (body: RuleMigration[]) => Promise; - search: (params: RuleMigrationSearchParams) => Promise; + data: RuleMigrationsDataClient; + task: { + start: (params: SiemRuleMigrationsStartTaskParams) => Promise; + stop: (migrationId: string) => Promise; + getStats: (migrationId: string) => Promise; + getAllStats: () => Promise; + }; } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/siem_migrations_service.test.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/siem_migrations_service.test.ts index 3d9e5b9fe179b..adf77756cce34 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/siem_migrations_service.test.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/siem_migrations_service.test.ts @@ -8,9 +8,15 @@ import { loggingSystemMock, elasticsearchServiceMock, httpServerMock, + securityServiceMock, } from '@kbn/core/server/mocks'; import { SiemMigrationsService } from './siem_migrations_service'; -import { MockSiemRuleMigrationsService, mockSetup, mockGetClient } from './rules/__mocks__/mocks'; +import { + MockSiemRuleMigrationsService, + mockSetup, + mockCreateClient, + mockStop, +} from './rules/__mocks__/mocks'; import type { ConfigType } from '../../config'; jest.mock('./rules/siem_rule_migrations_service'); @@ -25,6 +31,7 @@ describe('SiemMigrationsService', () => { let siemMigrationsService: SiemMigrationsService; const kibanaVersion = '8.16.0'; + const currentUser = securityServiceMock.createMockAuthenticatedUser(); const esClusterClient = elasticsearchServiceMock.createClusterClient(); const logger = loggingSystemMock.createLogger(); @@ -57,17 +64,22 @@ describe('SiemMigrationsService', () => { }); }); - describe('when createClient is called', () => { + describe('when createRulesClient is called', () => { it('should create rules client', async () => { - const request = httpServerMock.createKibanaRequest(); - siemMigrationsService.createClient({ spaceId: 'default', request }); - expect(mockGetClient).toHaveBeenCalledWith({ spaceId: 'default', request }); + const createRulesClientParams = { + spaceId: 'default', + request: httpServerMock.createKibanaRequest(), + currentUser, + }; + siemMigrationsService.createRulesClient(createRulesClientParams); + expect(mockCreateClient).toHaveBeenCalledWith(createRulesClientParams); }); }); describe('when stop is called', () => { it('should trigger the pluginStop subject', async () => { siemMigrationsService.stop(); + expect(mockStop).toHaveBeenCalled(); expect(mockReplaySubject$.next).toHaveBeenCalled(); expect(mockReplaySubject$.complete).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/siem_migrations_service.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/siem_migrations_service.ts index b84281eb13d9b..7a85dd625feec 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/siem_migrations_service.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/siem_migrations_service.ts @@ -9,11 +9,8 @@ import type { Logger } from '@kbn/core/server'; import { ReplaySubject, type Subject } from 'rxjs'; import type { ConfigType } from '../../config'; import { SiemRuleMigrationsService } from './rules/siem_rule_migrations_service'; -import type { - SiemMigrationsClient, - SiemMigrationsSetupParams, - SiemMigrationsGetClientParams, -} from './types'; +import type { SiemMigrationsSetupParams, SiemMigrationsCreateClientParams } from './types'; +import type { SiemRuleMigrationsClient } from './rules/types'; export class SiemMigrationsService { private pluginStop$: Subject; @@ -30,13 +27,12 @@ export class SiemMigrationsService { } } - createClient(params: SiemMigrationsGetClientParams): SiemMigrationsClient { - return { - rules: this.rules.getClient(params), - }; + createRulesClient(params: SiemMigrationsCreateClientParams): SiemRuleMigrationsClient { + return this.rules.createClient(params); } stop() { + this.rules.stop(); this.pluginStop$.next(); this.pluginStop$.complete(); } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/types.ts index b5647ff65e214..d2af1e2518722 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/types.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/types.ts @@ -6,15 +6,11 @@ */ import type { IClusterClient } from '@kbn/core/server'; -import type { SiemRuleMigrationsClient, SiemRuleMigrationsGetClientParams } from './rules/types'; +import type { SiemRuleMigrationsCreateClientParams } from './rules/types'; export interface SiemMigrationsSetupParams { esClusterClient: IClusterClient; tasksTimeoutMs?: number; } -export type SiemMigrationsGetClientParams = SiemRuleMigrationsGetClientParams; - -export interface SiemMigrationsClient { - rules: SiemRuleMigrationsClient; -} +export type SiemMigrationsCreateClientParams = SiemRuleMigrationsCreateClientParams; diff --git a/x-pack/plugins/security_solution/server/plugin_contract.ts b/x-pack/plugins/security_solution/server/plugin_contract.ts index c7ec67c1b07fc..c178f0654d9bd 100644 --- a/x-pack/plugins/security_solution/server/plugin_contract.ts +++ b/x-pack/plugins/security_solution/server/plugin_contract.ts @@ -45,6 +45,7 @@ import type { SharePluginStart } from '@kbn/share-plugin/server'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server'; import type { ElasticAssistantPluginStart } from '@kbn/elastic-assistant-plugin/server'; +import type { InferenceServerStart } from '@kbn/inference-plugin/server'; import type { ProductFeaturesService } from './lib/product_features_service/product_features_service'; import type { ExperimentalFeatures } from '../common'; @@ -88,6 +89,7 @@ export interface SecuritySolutionPluginStartDependencies { telemetry?: TelemetryPluginStart; share: SharePluginStart; actions: ActionsPluginStartContract; + inference: InferenceServerStart; } export interface SecuritySolutionPluginSetup { diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index f91f3c055a25b..fbe7be692e523 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -166,10 +166,16 @@ export class RequestContextFactory implements IRequestContextFactory { }) ), - getSiemMigrationsClient: memoize(() => - siemMigrationsService.createClient({ request, spaceId: getSpaceId() }) + getSiemRuleMigrationsClient: memoize(() => + siemMigrationsService.createRulesClient({ + request, + currentUser: coreContext.security.authc.getCurrentUser(), + spaceId: getSpaceId(), + }) ), + getInferenceClient: memoize(() => startPlugins.inference.getClient({ request })), + getExceptionListClient: () => { if (!lists) { return null; diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 1355904dbe7f7..7afbf5dcff6d2 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -20,6 +20,7 @@ import type { AlertsClient, IRuleDataService } from '@kbn/rule-registry-plugin/s import type { Readable } from 'stream'; import type { AuditLogger } from '@kbn/security-plugin-types-server'; +import type { InferenceClient } from '@kbn/inference-plugin/server'; import type { Immutable } from '../common/endpoint/types'; import { AppClient } from './client'; import type { ConfigType } from './config'; @@ -35,7 +36,7 @@ import type { RiskScoreDataClient } from './lib/entity_analytics/risk_score/risk import type { AssetCriticalityDataClient } from './lib/entity_analytics/asset_criticality'; import type { IDetectionRulesClient } from './lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface'; import type { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client'; -import type { SiemMigrationsClient } from './lib/siem_migrations/types'; +import type { SiemRuleMigrationsClient } from './lib/siem_migrations/rules/types'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext { @@ -58,7 +59,8 @@ export interface SecuritySolutionApiRequestHandlerContext { getRiskScoreDataClient: () => RiskScoreDataClient; getAssetCriticalityDataClient: () => AssetCriticalityDataClient; getEntityStoreDataClient: () => EntityStoreDataClient; - getSiemMigrationsClient: () => SiemMigrationsClient; + getSiemRuleMigrationsClient: () => SiemRuleMigrationsClient; + getInferenceClient: () => InferenceClient; } export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext<{ diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index cbdd2aed3496f..df743a666108e 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -229,5 +229,6 @@ "@kbn/data-stream-adapter", "@kbn/core-lifecycle-server", "@kbn/core-user-profile-common", + "@kbn/langchain", ] } diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 1ddbbf2ed7365..0b1338fee46e2 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -95,6 +95,8 @@ import { GetRuleExecutionResultsRequestQueryInput, GetRuleExecutionResultsRequestParamsInput, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen'; +import { GetRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rules_migration.gen'; +import { GetRuleMigrationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rules_migration.gen'; import { GetTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timeline/get_timeline_route.gen'; import { GetTimelinesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timelines/get_timelines_route.gen'; import { ImportRulesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/import_rules/import_rules_route.gen'; @@ -127,7 +129,12 @@ import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; import { SetAlertTagsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_tags/set_alert_tags/set_alert_tags.gen'; import { StartEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/start.gen'; +import { + StartRuleMigrationRequestParamsInput, + StartRuleMigrationRequestBodyInput, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rules_migration.gen'; import { StopEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stop.gen'; +import { StopRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rules_migration.gen'; import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; import { TriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { UpdateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/update_rule/update_rule_route.gen'; @@ -755,6 +762,16 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + /** + * Retrieves the rule migrations stats for all migrations stored in the system + */ + getAllStatsRuleMigration(kibanaSpace: string = 'default') { + return supertest + .get(routeWithNamespace('/internal/siem_migrations/rules/stats', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Get the criticality record for a specific asset. */ @@ -909,11 +926,31 @@ finalize it. .query(props.query); }, /** - * Retrieves the rule migrations stored in the system + * Retrieves the rule documents stored in the system given the rule migration id */ - getRuleMigration(kibanaSpace: string = 'default') { + getRuleMigration(props: GetRuleMigrationProps, kibanaSpace: string = 'default') { return supertest - .get(routeWithNamespace('/internal/siem_migrations/rules', kibanaSpace)) + .get( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, + /** + * Retrieves the stats of a SIEM rules migration using the migration id provided + */ + getRuleMigrationStats(props: GetRuleMigrationStatsProps, kibanaSpace: string = 'default') { + return supertest + .get( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}/stats', props.params), + kibanaSpace + ) + ) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); @@ -1260,6 +1297,22 @@ detection engine rules. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Starts a SIEM rules migration using the migration id provided + */ + startRuleMigration(props: StartRuleMigrationProps, kibanaSpace: string = 'default') { + return supertest + .put( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}/start', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, stopEntityEngine(props: StopEntityEngineProps, kibanaSpace: string = 'default') { return supertest .post( @@ -1272,6 +1325,21 @@ detection engine rules. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Stops a running SIEM rules migration using the migration id provided + */ + stopRuleMigration(props: StopRuleMigrationProps, kibanaSpace: string = 'default') { + return supertest + .put( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}/stop', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Suggests user profiles. */ @@ -1490,6 +1558,12 @@ export interface GetRuleExecutionResultsProps { query: GetRuleExecutionResultsRequestQueryInput; params: GetRuleExecutionResultsRequestParamsInput; } +export interface GetRuleMigrationProps { + params: GetRuleMigrationRequestParamsInput; +} +export interface GetRuleMigrationStatsProps { + params: GetRuleMigrationStatsRequestParamsInput; +} export interface GetTimelineProps { query: GetTimelineRequestQueryInput; } @@ -1562,9 +1636,16 @@ export interface SetAlertTagsProps { export interface StartEntityEngineProps { params: StartEntityEngineRequestParamsInput; } +export interface StartRuleMigrationProps { + params: StartRuleMigrationRequestParamsInput; + body: StartRuleMigrationRequestBodyInput; +} export interface StopEntityEngineProps { params: StopEntityEngineRequestParamsInput; } +export interface StopRuleMigrationProps { + params: StopRuleMigrationRequestParamsInput; +} export interface SuggestUserProfilesProps { query: SuggestUserProfilesRequestQueryInput; } From 3b2a5729316fa1bbd33b0149c7f54d01da496b61 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:41:39 +1100 Subject: [PATCH 10/47] [8.x] [Obs AI Assistant] Add uuid to knowledge base entries to avoid overwriting accidentally (#191043) (#199263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Obs AI Assistant] Add uuid to knowledge base entries to avoid overwriting accidentally (#191043)](https://github.com/elastic/kibana/pull/191043) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Søren Louv-Jansen --- .../common/types.ts | 6 +- .../common/utils/short_id_table.test.ts | 10 + .../server/analytics/recall_ranking.ts | 4 +- .../server/functions/summarize.ts | 25 +- .../server/routes/chat/route.ts | 2 +- .../server/routes/functions/route.ts | 15 +- .../server/routes/knowledge_base/route.ts | 129 ++-- .../server/service/client/index.ts | 64 +- .../server/service/index.ts | 102 +-- .../server/service/kb_component_template.ts | 11 +- .../service/knowledge_base_service/index.ts | 292 +++------ .../knowledge_base_service/kb_docs/lens.ts | 596 ------------------ .../server/service/types.ts | 5 +- ...t_system_message_from_instructions.test.ts | 6 +- .../get_system_message_from_instructions.ts | 9 +- .../server/service/util/split_kb_text.ts | 36 -- .../server/utils/recall/recall_and_score.ts | 22 +- .../utils/recall/retrieve_suggestions.ts | 24 - .../server/utils/recall/score_suggestions.ts | 60 +- .../server/utils/recall/types.ts | 10 - .../public/helpers/categorize_entries.ts | 29 +- .../hooks/use_create_knowledge_base_entry.ts | 12 +- ..._create_knowledge_base_user_instruction.ts | 4 +- .../use_import_knowledge_base_entries.ts | 2 +- .../knowledge_base_bulk_import_flyout.tsx | 6 +- .../knowledge_base_category_flyout.tsx | 4 +- ...nowledge_base_edit_manual_entry_flyout.tsx | 61 +- ...edge_base_edit_user_instruction_flyout.tsx | 6 +- .../components/knowledge_base_tab.test.tsx | 19 +- .../routes/components/knowledge_base_tab.tsx | 23 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../common/config.ts | 20 +- .../common/users/users.ts | 14 +- .../tests/complete/complete.spec.ts | 12 +- .../tests/complete/functions/helpers.ts | 2 +- .../complete/functions/summarize.spec.ts | 14 +- .../tests/connectors/connectors.spec.ts | 6 +- .../tests/conversations/conversations.spec.ts | 24 +- .../knowledge_base/knowledge_base.spec.ts | 189 +++--- .../knowledge_base_setup.spec.ts | 4 +- .../knowledge_base_status.spec.ts | 6 +- .../knowledge_base_user_instructions.spec.ts | 73 +-- .../public_complete/public_complete.spec.ts | 2 +- .../common/config.ts | 12 +- .../common/ui/index.ts | 5 + .../tests/conversations/index.spec.ts | 16 +- .../knowledge_base_management/index.spec.ts | 124 ++++ .../check_registered_task_types.ts | 1 - .../knowledge_base/knowledge_base.spec.ts | 4 + 51 files changed, 687 insertions(+), 1438 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/kb_docs/lens.ts delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/split_kb_text.ts delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/retrieve_suggestions.ts delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/types.ts create mode 100644 x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts index 51ae37b39d90f..210eb08b31e1a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts @@ -82,8 +82,8 @@ export type ConversationUpdateRequest = ConversationRequestBase & { export interface KnowledgeBaseEntry { '@timestamp': string; id: string; + title?: string; text: string; - doc_id: string; confidence: 'low' | 'medium' | 'high'; is_correction: boolean; type?: 'user_instruction' | 'contextual'; @@ -96,12 +96,12 @@ export interface KnowledgeBaseEntry { } export interface Instruction { - doc_id: string; + id: string; text: string; } export interface AdHocInstruction { - doc_id?: string; + id?: string; text: string; instruction_type: 'user_instruction' | 'application_instruction'; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/short_id_table.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/short_id_table.test.ts index 784cf67530652..03d1cb177826e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/short_id_table.test.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/short_id_table.test.ts @@ -7,6 +7,16 @@ import { ShortIdTable } from './short_id_table'; describe('shortIdTable', () => { + it('generates a short id from a uuid', () => { + const table = new ShortIdTable(); + + const uuid = 'd877f65c-4036-42c4-b105-19e2f1a1c045'; + const shortId = table.take(uuid); + + expect(shortId.length).toBe(4); + expect(table.lookup(shortId)).toBe(uuid); + }); + it('generates at least 10k unique ids consistently', () => { const ids = new Set(); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/analytics/recall_ranking.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/analytics/recall_ranking.ts index 4c82f79fcba8d..4371310811edf 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/analytics/recall_ranking.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/analytics/recall_ranking.ts @@ -52,9 +52,9 @@ const schema: RootSchema = { }, }; -export const RecallRankingEventType = 'observability_ai_assistant_recall_ranking'; +export const recallRankingEventType = 'observability_ai_assistant_recall_ranking'; export const recallRankingEvent: EventTypeOpts = { - eventType: RecallRankingEventType, + eventType: recallRankingEventType, schema, }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/summarize.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/summarize.ts index 8865861d81f45..1f4afdbdd56bb 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/summarize.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/summarize.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KnowledgeBaseType } from '../../common/types'; +import { v4 } from 'uuid'; import type { FunctionRegistrationParameters } from '.'; import { KnowledgeBaseEntryRole } from '../../common'; @@ -14,6 +14,7 @@ export const SUMMARIZE_FUNCTION_NAME = 'summarize'; export function registerSummarizationFunction({ client, functions, + resources, }: FunctionRegistrationParameters) { functions.registerFunction( { @@ -28,10 +29,10 @@ export function registerSummarizationFunction({ parameters: { type: 'object', properties: { - id: { + title: { type: 'string', description: - 'An id for the document. This should be a short human-readable keyword field with only alphabetic characters and underscores, that allow you to update it later.', + 'A human readable title that can be used to identify the document later. This should be no longer than 255 characters', }, text: { type: 'string', @@ -54,7 +55,7 @@ export function registerSummarizationFunction({ }, }, required: [ - 'id' as const, + 'title' as const, 'text' as const, 'is_correction' as const, 'confidence' as const, @@ -62,21 +63,23 @@ export function registerSummarizationFunction({ ], }, }, - ( - { arguments: { id, text, is_correction: isCorrection, confidence, public: isPublic } }, + async ( + { arguments: { title, text, is_correction: isCorrection, confidence, public: isPublic } }, signal ) => { + const id = v4(); + resources.logger.debug(`Creating new knowledge base entry with id: ${id}`); + return client .addKnowledgeBaseEntry({ entry: { - doc_id: id, - role: KnowledgeBaseEntryRole.AssistantSummarization, id, + title, text, - is_correction: isCorrection, - type: KnowledgeBaseType.Contextual, - confidence, public: isPublic, + role: KnowledgeBaseEntryRole.AssistantSummarization, + confidence, + is_correction: isCorrection, labels: {}, }, // signal, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts index 8bc88cca10b01..a6fe57cb58adc 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts @@ -42,7 +42,7 @@ const chatCompleteBaseRt = t.type({ ]), instructions: t.array( t.intersection([ - t.partial({ doc_id: t.string }), + t.partial({ id: t.string }), t.type({ text: t.string, instruction_type: t.union([ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts index c402a0506736f..1571487765c09 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts @@ -7,6 +7,7 @@ import { notImplemented } from '@hapi/boom'; import { nonEmptyStringRt, toBooleanRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; +import { v4 } from 'uuid'; import { FunctionDefinition } from '../../../common/functions/types'; import { KnowledgeBaseEntryRole } from '../../../common/types'; import type { RecalledEntry } from '../../service/knowledge_base_service'; @@ -114,7 +115,8 @@ const functionRecallRoute = createObservabilityAIAssistantServerRoute({ throw notImplemented(); } - return client.recall({ queries, categories }); + const entries = await client.recall({ queries, categories }); + return { entries }; }, }); @@ -122,11 +124,10 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/functions/summarize', params: t.type({ body: t.type({ - id: t.string, + title: t.string, text: nonEmptyStringRt, confidence: t.union([t.literal('low'), t.literal('medium'), t.literal('high')]), is_correction: toBooleanRt, - type: t.union([t.literal('user_instruction'), t.literal('contextual')]), public: toBooleanRt, labels: t.record(t.string, t.string), }), @@ -142,10 +143,9 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ } const { + title, confidence, - id, is_correction: isCorrection, - type, text, public: isPublic, labels, @@ -153,11 +153,10 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ return client.addKnowledgeBaseEntry({ entry: { + title, confidence, - id, - doc_id: id, + id: v4(), is_correction: isCorrection, - type, text, public: isPublic, labels, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts index 1eb1650545781..0f1852c0e396c 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts @@ -9,16 +9,12 @@ import type { MlDeploymentAllocationState, MlDeploymentState, } from '@elastic/elasticsearch/lib/api/types'; +import pLimit from 'p-limit'; import { notImplemented } from '@hapi/boom'; import { nonEmptyStringRt, toBooleanRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; -import { - Instruction, - KnowledgeBaseEntry, - KnowledgeBaseEntryRole, - KnowledgeBaseType, -} from '../../../common/types'; +import { Instruction, KnowledgeBaseEntry, KnowledgeBaseEntryRole } from '../../../common/types'; const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({ endpoint: 'GET /internal/observability_ai_assistant/kb/status', @@ -108,18 +104,8 @@ const saveKnowledgeBaseUserInstruction = createObservabilityAIAssistantServerRou } const { id, text, public: isPublic } = resources.params.body; - return client.addKnowledgeBaseEntry({ - entry: { - id, - doc_id: id, - text, - public: isPublic, - confidence: 'high', - is_correction: false, - type: KnowledgeBaseType.UserInstruction, - labels: {}, - role: KnowledgeBaseEntryRole.UserEntry, - }, + return client.addUserInstruction({ + entry: { id, text, public: isPublic }, }); }, }); @@ -153,26 +139,29 @@ const getKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({ }, }); +const knowledgeBaseEntryRt = t.intersection([ + t.type({ + id: t.string, + title: t.string, + text: nonEmptyStringRt, + }), + t.partial({ + confidence: t.union([t.literal('low'), t.literal('medium'), t.literal('high')]), + is_correction: toBooleanRt, + public: toBooleanRt, + labels: t.record(t.string, t.string), + role: t.union([ + t.literal(KnowledgeBaseEntryRole.AssistantSummarization), + t.literal(KnowledgeBaseEntryRole.UserEntry), + t.literal(KnowledgeBaseEntryRole.Elastic), + ]), + }), +]); + const saveKnowledgeBaseEntry = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/kb/entries/save', params: t.type({ - body: t.intersection([ - t.type({ - id: t.string, - text: nonEmptyStringRt, - }), - t.partial({ - confidence: t.union([t.literal('low'), t.literal('medium'), t.literal('high')]), - is_correction: toBooleanRt, - public: toBooleanRt, - labels: t.record(t.string, t.string), - role: t.union([ - t.literal('assistant_summarization'), - t.literal('user_entry'), - t.literal('elastic'), - ]), - }), - ]), + body: knowledgeBaseEntryRt, }), options: { tags: ['access:ai_assistant'], @@ -184,27 +173,15 @@ const saveKnowledgeBaseEntry = createObservabilityAIAssistantServerRoute({ throw notImplemented(); } - const { - id, - text, - public: isPublic, - confidence, - is_correction: isCorrection, - labels, - role, - } = resources.params.body; - + const entry = resources.params.body; return client.addKnowledgeBaseEntry({ entry: { - id, - text, - doc_id: id, - confidence: confidence ?? 'high', - is_correction: isCorrection ?? false, - type: 'contextual', - public: isPublic ?? true, - labels: labels ?? {}, - role: (role as KnowledgeBaseEntryRole) ?? KnowledgeBaseEntryRole.UserEntry, + confidence: 'high', + is_correction: false, + public: true, + labels: {}, + role: KnowledgeBaseEntryRole.UserEntry, + ...entry, }, }); }, @@ -235,12 +212,7 @@ const importKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import', params: t.type({ body: t.type({ - entries: t.array( - t.type({ - id: t.string, - text: nonEmptyStringRt, - }) - ), + entries: t.array(knowledgeBaseEntryRt), }), }), options: { @@ -253,18 +225,29 @@ const importKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({ throw notImplemented(); } - const entries = resources.params.body.entries.map((entry) => ({ - doc_id: entry.id, - confidence: 'high' as KnowledgeBaseEntry['confidence'], - is_correction: false, - type: 'contextual' as const, - public: true, - labels: {}, - role: KnowledgeBaseEntryRole.UserEntry, - ...entry, - })); - - return await client.importKnowledgeBaseEntries({ entries }); + const status = await client.getKnowledgeBaseStatus(); + if (!status.ready) { + throw new Error('Knowledge base is not ready'); + } + + const limiter = pLimit(5); + + const promises = resources.params.body.entries.map(async (entry) => { + return limiter(async () => { + return client.addKnowledgeBaseEntry({ + entry: { + confidence: 'high', + is_correction: false, + public: true, + labels: {}, + role: KnowledgeBaseEntryRole.UserEntry, + ...entry, + }, + }); + }); + }); + + await Promise.all(promises); }, }); @@ -273,8 +256,8 @@ export const knowledgeBaseRoutes = { ...getKnowledgeBaseStatus, ...getKnowledgeBaseEntries, ...saveKnowledgeBaseUserInstruction, - ...getKnowledgeBaseUserInstructions, ...importKnowledgeBaseEntries, + ...getKnowledgeBaseUserInstructions, ...saveKnowledgeBaseEntry, ...deleteKnowledgeBaseEntry, }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts index 162220ec7a7f1..048bbd2d362c2 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts @@ -47,21 +47,19 @@ import { } from '../../../common/conversation_complete'; import { CompatibleJSONSchema } from '../../../common/functions/types'; import { - AdHocInstruction, + type AdHocInstruction, type Conversation, type ConversationCreateRequest, type ConversationUpdateRequest, type KnowledgeBaseEntry, type Message, + KnowledgeBaseType, + KnowledgeBaseEntryRole, } from '../../../common/types'; import { withoutTokenCountEvents } from '../../../common/utils/without_token_count_events'; import { CONTEXT_FUNCTION_NAME } from '../../functions/context'; import type { ChatFunctionClient } from '../chat_function_client'; -import { - KnowledgeBaseEntryOperationType, - KnowledgeBaseService, - RecalledEntry, -} from '../knowledge_base_service'; +import { KnowledgeBaseService, RecalledEntry } from '../knowledge_base_service'; import { getAccessQuery } from '../util/get_access_query'; import { getSystemMessageFromInstructions } from '../util/get_system_message_from_instructions'; import { replaceSystemMessage } from '../util/replace_system_message'; @@ -709,7 +707,7 @@ export class ObservabilityAIAssistantClient { }: { queries: Array<{ text: string; boost?: number }>; categories?: string[]; - }): Promise<{ entries: RecalledEntry[] }> => { + }): Promise => { return ( this.dependencies.knowledgeBaseService?.recall({ namespace: this.dependencies.namespace, @@ -718,7 +716,7 @@ export class ObservabilityAIAssistantClient { categories, esClient: this.dependencies.esClient, uiSettingsClient: this.dependencies.uiSettingsClient, - }) || { entries: [] } + }) || [] ); }; @@ -730,29 +728,55 @@ export class ObservabilityAIAssistantClient { return this.dependencies.knowledgeBaseService.setup(); }; - addKnowledgeBaseEntry = async ({ + addUserInstruction = async ({ entry, }: { - entry: Omit; + entry: Omit< + KnowledgeBaseEntry, + '@timestamp' | 'confidence' | 'is_correction' | 'type' | 'role' + >; }): Promise => { + // for now we want to limit the number of user instructions to 1 per user + // if a user instruction already exists for the user, we get the id and update it + this.dependencies.logger.debug('Adding user instruction entry'); + const existingId = await this.dependencies.knowledgeBaseService.getPersonalUserInstructionId({ + isPublic: entry.public, + namespace: this.dependencies.namespace, + user: this.dependencies.user, + }); + + if (existingId) { + entry.id = existingId; + this.dependencies.logger.debug(`Updating user instruction with id "${existingId}"`); + } + return this.dependencies.knowledgeBaseService.addEntry({ namespace: this.dependencies.namespace, user: this.dependencies.user, - entry, + entry: { + ...entry, + confidence: 'high', + is_correction: false, + type: KnowledgeBaseType.UserInstruction, + labels: {}, + role: KnowledgeBaseEntryRole.UserEntry, + }, }); }; - importKnowledgeBaseEntries = async ({ - entries, + addKnowledgeBaseEntry = async ({ + entry, }: { - entries: Array>; + entry: Omit; }): Promise => { - const operations = entries.map((entry) => ({ - type: KnowledgeBaseEntryOperationType.Index, - document: { ...entry, '@timestamp': new Date().toISOString() }, - })); - - await this.dependencies.knowledgeBaseService.addEntries({ operations }); + return this.dependencies.knowledgeBaseService.addEntry({ + namespace: this.dependencies.namespace, + user: this.dependencies.user, + entry: { + ...entry, + type: KnowledgeBaseType.Contextual, + }, + }); }; getKnowledgeBaseEntries = async ({ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts index d1aba4f232b0d..eb7eab19340ce 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts @@ -13,18 +13,14 @@ import { getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { once } from 'lodash'; import type { AssistantScope } from '@kbn/ai-assistant-common'; -import { - KnowledgeBaseEntryRole, - ObservabilityAIAssistantScreenContextRequest, -} from '../../common/types'; +import { ObservabilityAIAssistantScreenContextRequest } from '../../common/types'; import type { ObservabilityAIAssistantPluginStartDependencies } from '../types'; import { ChatFunctionClient } from './chat_function_client'; import { ObservabilityAIAssistantClient } from './client'; import { conversationComponentTemplate } from './conversation_component_template'; import { kbComponentTemplate } from './kb_component_template'; -import { KnowledgeBaseEntryOperationType, KnowledgeBaseService } from './knowledge_base_service'; +import { KnowledgeBaseService } from './knowledge_base_service'; import type { RegistrationCallback, RespondFunctionResources } from './types'; -import { splitKbText } from './util/split_kb_text'; function getResourceName(resource: string) { return `.kibana-observability-ai-assistant-${resource}`; @@ -52,24 +48,11 @@ export const resourceNames = { }, }; -export const INDEX_QUEUED_DOCUMENTS_TASK_ID = 'observabilityAIAssistant:indexQueuedDocumentsTask'; - -export const INDEX_QUEUED_DOCUMENTS_TASK_TYPE = INDEX_QUEUED_DOCUMENTS_TASK_ID + 'Type'; - -type KnowledgeBaseEntryRequest = { id: string; labels?: Record } & ( - | { - text: string; - } - | { - texts: string[]; - } -); - export class ObservabilityAIAssistantService { private readonly core: CoreSetup; private readonly logger: Logger; private readonly getModelId: () => Promise; - private kbService?: KnowledgeBaseService; + public kbService?: KnowledgeBaseService; private enableKnowledgeBase: boolean; private readonly registrations: RegistrationCallback[] = []; @@ -93,26 +76,6 @@ export class ObservabilityAIAssistantService { this.enableKnowledgeBase = enableKnowledgeBase; this.allowInit(); - if (enableKnowledgeBase) { - taskManager.registerTaskDefinitions({ - [INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: { - title: 'Index queued KB articles', - description: - 'Indexes previously registered entries into the knowledge base when it is ready', - timeout: '30m', - maxAttempts: 2, - createTaskRunner: (context) => { - return { - run: async () => { - if (this.kbService) { - await this.kbService.processQueue(); - } - }, - }; - }, - }, - }); - } } getKnowledgeBaseStatus() { @@ -336,65 +299,6 @@ export class ObservabilityAIAssistantService { return fnClient; } - addToKnowledgeBaseQueue(entries: KnowledgeBaseEntryRequest[]): void { - if (this.enableKnowledgeBase) { - this.init() - .then(() => { - this.kbService!.queue( - entries.flatMap((entry) => { - const entryWithSystemProperties = { - ...entry, - '@timestamp': new Date().toISOString(), - doc_id: entry.id, - public: true, - confidence: 'high' as const, - type: 'contextual' as const, - is_correction: false, - labels: { - ...entry.labels, - }, - role: KnowledgeBaseEntryRole.Elastic, - }; - - const operations = - 'texts' in entryWithSystemProperties - ? splitKbText(entryWithSystemProperties) - : [ - { - type: KnowledgeBaseEntryOperationType.Index, - document: entryWithSystemProperties, - }, - ]; - - return operations; - }) - ); - }) - .catch((error) => { - this.logger.error( - `Could not index ${entries.length} entries because of an initialisation error` - ); - this.logger.error(error); - }); - } - } - - addCategoryToKnowledgeBase(categoryId: string, entries: KnowledgeBaseEntryRequest[]) { - if (this.enableKnowledgeBase) { - this.addToKnowledgeBaseQueue( - entries.map((entry) => { - return { - ...entry, - labels: { - ...entry.labels, - category: categoryId, - }, - }; - }) - ); - } - } - register(cb: RegistrationCallback) { this.registrations.push(cb); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/kb_component_template.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/kb_component_template.ts index a4c6dc25d2e57..b1b2d3293a234 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/kb_component_template.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/kb_component_template.ts @@ -31,7 +31,16 @@ export const kbComponentTemplate: ClusterComponentTemplate['component_template'] properties: { '@timestamp': date, id: keyword, - doc_id: { type: 'text', fielddata: true }, + doc_id: { type: 'text', fielddata: true }, // deprecated but kept for backwards compatibility + title: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, user: { properties: { id: keyword, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts index 7306a0df7c572..92ce3a4a7e03b 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts @@ -9,16 +9,11 @@ import { serverUnavailable, gatewayTimeout, badRequest } from '@hapi/boom'; import type { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; -import pLimit from 'p-limit'; import pRetry from 'p-retry'; -import { map, orderBy } from 'lodash'; +import { orderBy } from 'lodash'; import { encode } from 'gpt-tokenizer'; import { MlTrainedModelDeploymentNodesStats } from '@elastic/elasticsearch/lib/api/types'; -import { - INDEX_QUEUED_DOCUMENTS_TASK_ID, - INDEX_QUEUED_DOCUMENTS_TASK_TYPE, - resourceNames, -} from '..'; +import { resourceNames } from '..'; import { Instruction, KnowledgeBaseEntry, @@ -63,36 +58,11 @@ function throwKnowledgeBaseNotReady(body: any) { throw serverUnavailable(`Knowledge base is not ready yet`, body); } -export enum KnowledgeBaseEntryOperationType { - Index = 'index', - Delete = 'delete', -} - -interface KnowledgeBaseDeleteOperation { - type: KnowledgeBaseEntryOperationType.Delete; - doc_id?: string; - labels?: Record; -} - -interface KnowledgeBaseIndexOperation { - type: KnowledgeBaseEntryOperationType.Index; - document: KnowledgeBaseEntry; -} - -export type KnowledgeBaseEntryOperation = - | KnowledgeBaseDeleteOperation - | KnowledgeBaseIndexOperation; - export class KnowledgeBaseService { - private hasSetup: boolean = false; - - private _queue: KnowledgeBaseEntryOperation[] = []; - - constructor(private readonly dependencies: Dependencies) { - this.ensureTaskScheduled(); - } + constructor(private readonly dependencies: Dependencies) {} setup = async () => { + this.dependencies.logger.debug('Setting up knowledge base'); if (!this.dependencies.enabled) { return; } @@ -192,7 +162,7 @@ export class KnowledgeBaseService { ); if (isReady) { - return Promise.resolve(); + return; } this.dependencies.logger.debug(`${elserModelId} model is not allocated yet`); @@ -202,116 +172,10 @@ export class KnowledgeBaseService { }, retryOptions); this.dependencies.logger.info(`${elserModelId} model is ready`); - this.ensureTaskScheduled(); }; - private ensureTaskScheduled() { - if (!this.dependencies.enabled) { - return; - } - this.dependencies.taskManagerStart - .ensureScheduled({ - taskType: INDEX_QUEUED_DOCUMENTS_TASK_TYPE, - id: INDEX_QUEUED_DOCUMENTS_TASK_ID, - state: {}, - params: {}, - schedule: { - interval: '1h', - }, - }) - .then(() => { - this.dependencies.logger.debug('Scheduled queue task'); - return this.dependencies.taskManagerStart.runSoon(INDEX_QUEUED_DOCUMENTS_TASK_ID); - }) - .then(() => { - this.dependencies.logger.debug('Queue task ran'); - }) - .catch((err) => { - this.dependencies.logger.error(`Failed to schedule queue task`); - this.dependencies.logger.error(err); - }); - } - - private async processOperation(operation: KnowledgeBaseEntryOperation) { - if (operation.type === KnowledgeBaseEntryOperationType.Delete) { - await this.dependencies.esClient.asInternalUser.deleteByQuery({ - index: resourceNames.aliases.kb, - query: { - bool: { - filter: [ - ...(operation.doc_id ? [{ term: { _id: operation.doc_id } }] : []), - ...(operation.labels - ? map(operation.labels, (value, key) => { - return { term: { [key]: value } }; - }) - : []), - ], - }, - }, - }); - return; - } - - await this.addEntry({ - entry: operation.document, - }); - } - - async processQueue() { - if (!this._queue.length || !this.dependencies.enabled) { - return; - } - - if (!(await this.status()).ready) { - this.dependencies.logger.debug(`Bailing on queue task: KB is not ready yet`); - return; - } - - this.dependencies.logger.debug(`Processing queue`); - - this.hasSetup = true; - - this.dependencies.logger.info(`Processing ${this._queue.length} queue operations`); - - const limiter = pLimit(5); - - const operations = this._queue.concat(); - - await Promise.all( - operations.map((operation) => - limiter(async () => { - this._queue.splice(operations.indexOf(operation), 1); - await this.processOperation(operation); - }) - ) - ); - - this.dependencies.logger.info('Processed all queued operations'); - } - - queue(operations: KnowledgeBaseEntryOperation[]): void { - if (!operations.length) { - return; - } - - if (!this.hasSetup) { - this._queue.push(...operations); - return; - } - - const limiter = pLimit(5); - - const limitedFunctions = this._queue.map((operation) => - limiter(() => this.processOperation(operation)) - ); - - Promise.all(limitedFunctions).catch((err) => { - this.dependencies.logger.error(`Failed to process all queued operations`); - this.dependencies.logger.error(err); - }); - } - status = async () => { + this.dependencies.logger.debug('Checking model status'); if (!this.dependencies.enabled) { return { ready: false, enabled: false }; } @@ -324,15 +188,24 @@ export class KnowledgeBaseService { const elserModelStats = modelStats.trained_model_stats[0]; const deploymentState = elserModelStats.deployment_stats?.state; const allocationState = elserModelStats.deployment_stats?.allocation_status.state; + const ready = deploymentState === 'started' && allocationState === 'fully_allocated'; + + this.dependencies.logger.debug( + `Model deployment state: ${deploymentState}, allocation state: ${allocationState}, ready: ${ready}` + ); return { - ready: deploymentState === 'started' && allocationState === 'fully_allocated', + ready, deployment_state: deploymentState, allocation_state: allocationState, model_name: elserModelId, enabled: true, }; } catch (error) { + this.dependencies.logger.debug( + `Failed to get status for model "${elserModelId}" due to ${error.message}` + ); + return { error: error instanceof errors.ResponseError ? error.body.error : String(error), ready: false, @@ -380,18 +253,21 @@ export class KnowledgeBaseService { }; const response = await this.dependencies.esClient.asInternalUser.search< - Pick + Pick & { doc_id?: string } >({ index: [resourceNames.aliases.kb], query: esQuery, size: 20, _source: { - includes: ['text', 'is_correction', 'labels'], + includes: ['text', 'is_correction', 'labels', 'doc_id', 'title'], }, }); return response.hits.hits.map((hit) => ({ - ...hit._source!, + text: hit._source?.text!, + is_correction: hit._source?.is_correction, + labels: hit._source?.labels, + title: hit._source?.title ?? hit._source?.doc_id, // use `doc_id` as fallback title for backwards compatibility score: hit._score!, id: hit._id!, })); @@ -411,12 +287,11 @@ export class KnowledgeBaseService { namespace: string; esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; uiSettingsClient: IUiSettingsClient; - }): Promise<{ - entries: RecalledEntry[]; - }> => { + }): Promise => { if (!this.dependencies.enabled) { - return { entries: [] }; + return []; } + this.dependencies.logger.debug( () => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"` ); @@ -480,9 +355,7 @@ export class KnowledgeBaseService { this.dependencies.logger.info(`Dropped ${droppedEntries} entries because of token limit`); } - return { - entries: returnedEntries, - }; + return returnedEntries; }; getUserInstructions = async ( @@ -508,11 +381,11 @@ export class KnowledgeBaseService { }, }, size: 500, - _source: ['doc_id', 'text', 'public'], + _source: ['id', 'text', 'public'], }); return response.hits.hits.map((hit) => ({ - doc_id: hit._source?.doc_id ?? '', + id: hit._id!, text: hit._source?.text ?? '', public: hit._source?.public, })); @@ -536,13 +409,17 @@ export class KnowledgeBaseService { return { entries: [] }; } try { - const response = await this.dependencies.esClient.asInternalUser.search({ + const response = await this.dependencies.esClient.asInternalUser.search< + KnowledgeBaseEntry & { doc_id?: string } + >({ index: resourceNames.aliases.kb, query: { bool: { filter: [ - // filter title by query - ...(query ? [{ wildcard: { doc_id: { value: `${query}*` } } }] : []), + // filter by search query + ...(query + ? [{ query_string: { query: `${query}*`, fields: ['doc_id', 'title'] } }] + : []), { // exclude user instructions bool: { must_not: { term: { type: KnowledgeBaseType.UserInstruction } } }, @@ -550,16 +427,17 @@ export class KnowledgeBaseService { ], }, }, - sort: [ - { - [String(sortBy)]: { - order: sortDirection, - }, - }, - ], + sort: + sortBy === 'title' + ? [ + { ['title.keyword']: { order: sortDirection } }, + { doc_id: { order: sortDirection } }, // sort by doc_id for backwards compatibility + ] + : [{ [String(sortBy)]: { order: sortDirection } }], size: 500, _source: { includes: [ + 'title', 'doc_id', 'text', 'is_correction', @@ -577,6 +455,7 @@ export class KnowledgeBaseService { return { entries: response.hits.hits.map((hit) => ({ ...hit._source!, + title: hit._source!.title ?? hit._source!.doc_id, // use `doc_id` as fallback title for backwards compatibility role: hit._source!.role ?? KnowledgeBaseEntryRole.UserEntry, score: hit._score, id: hit._id!, @@ -590,7 +469,7 @@ export class KnowledgeBaseService { } }; - getExistingUserInstructionId = async ({ + getPersonalUserInstructionId = async ({ isPublic, user, namespace, @@ -602,9 +481,7 @@ export class KnowledgeBaseService { if (!this.dependencies.enabled) { return null; } - const res = await this.dependencies.esClient.asInternalUser.search< - Pick - >({ + const res = await this.dependencies.esClient.asInternalUser.search({ index: resourceNames.aliases.kb, query: { bool: { @@ -616,14 +493,47 @@ export class KnowledgeBaseService { }, }, size: 1, - _source: ['doc_id'], + _source: false, }); - return res.hits.hits[0]?._source?.doc_id; + return res.hits.hits[0]?._id; + }; + + getUuidFromDocId = async ({ + docId, + user, + namespace, + }: { + docId: string; + user?: { name: string; id?: string }; + namespace?: string; + }) => { + const query = { + bool: { + filter: [ + { term: { doc_id: docId } }, + + // exclude user instructions + { bool: { must_not: { term: { type: KnowledgeBaseType.UserInstruction } } } }, + + // restrict access to user's own entries + ...getAccessQuery({ user, namespace }), + ], + }, + }; + + const response = await this.dependencies.esClient.asInternalUser.search({ + size: 1, + index: resourceNames.aliases.kb, + query, + _source: false, + }); + + return response.hits.hits[0]?._id; }; addEntry = async ({ - entry: { id, ...document }, + entry: { id, ...doc }, user, namespace, }: { @@ -634,19 +544,6 @@ export class KnowledgeBaseService { if (!this.dependencies.enabled) { return; } - // for now we want to limit the number of user instructions to 1 per user - if (document.type === KnowledgeBaseType.UserInstruction) { - const existingId = await this.getExistingUserInstructionId({ - isPublic: document.public, - user, - namespace, - }); - - if (existingId) { - id = existingId; - document.doc_id = existingId; - } - } try { await this.dependencies.esClient.asInternalUser.index({ @@ -654,7 +551,7 @@ export class KnowledgeBaseService { id, document: { '@timestamp': new Date().toISOString(), - ...document, + ...doc, user, namespace, }, @@ -669,29 +566,6 @@ export class KnowledgeBaseService { } }; - addEntries = async ({ - operations, - }: { - operations: KnowledgeBaseEntryOperation[]; - }): Promise => { - if (!this.dependencies.enabled) { - return; - } - this.dependencies.logger.info(`Starting import of ${operations.length} entries`); - - const limiter = pLimit(5); - - await Promise.all( - operations.map((operation) => - limiter(async () => { - await this.processOperation(operation); - }) - ) - ); - - this.dependencies.logger.info(`Completed import of ${operations.length} entries`); - }; - deleteEntry = async ({ id }: { id: string }): Promise => { try { await this.dependencies.esClient.asInternalUser.delete({ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/kb_docs/lens.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/kb_docs/lens.ts deleted file mode 100644 index 9baf75f6ff552..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/kb_docs/lens.ts +++ /dev/null @@ -1,596 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import dedent from 'dedent'; -import type { Logger } from '@kbn/logging'; -import type { ObservabilityAIAssistantService } from '../..'; - -export function addLensDocsToKb({ - service, -}: { - service: ObservabilityAIAssistantService; - logger: Logger; -}) { - service.addCategoryToKnowledgeBase('lens', [ - { - id: 'lens_formulas_how_it_works', - texts: [ - `Lens formulas let you do math using a combination of Elasticsearch aggregations and - math functions. There are three main types of functions: - - * Elasticsearch metrics, like \`sum(bytes)\` - * Time series functions use Elasticsearch metrics as input, like \`cumulative_sum()\` - * Math functions like \`round()\` - - An example formula that uses all of these: - - \`\`\` - round(100 * moving_average( - average(cpu.load.pct), - window=10, - kql='datacenter.name: east*' - )) - \`\`\` - `, - `Elasticsearch functions take a field name, which can be in quotes. \`sum(bytes)\` is the same - as \`sum('bytes')\`. - - Some functions take named arguments, like \`moving_average(count(), window=5)\`. - - Elasticsearch metrics can be filtered using KQL or Lucene syntax. To add a filter, use the named - parameter \`kql='field: value'\` or \`lucene=''\`. Always use single quotes when writing KQL or Lucene - queries. If your search has a single quote in it, use a backslash to escape, like: \`kql='Women's'\' - - Math functions can take positional arguments, like pow(count(), 3) is the same as count() * count() * count() - - Use the symbols +, -, /, and * to perform basic math.`, - ], - }, - { - id: 'lens_common_formulas', - texts: [ - `The most common formulas are dividing two values to produce a percent. To display accurately, set - "value format" to "percent"`, - `### Filter ratio: - - Use \`kql=''\` to filter one set of documents and compare it to other documents within the same grouping. - For example, to see how the error rate changes over time: - - \`\`\` - count(kql='response.status_code > 400') / count() - \`\`\``, - `### Week over week: - - Use \`shift='1w'\` to get the value of each grouping from - the previous week. Time shift should not be used with the *Top values* function. - - \`\`\` - percentile(system.network.in.bytes, percentile=99) / - percentile(system.network.in.bytes, percentile=99, shift='1w') - \`\`\``, - - `### Percent of total - - Formulas can calculate \`overall_sum\` for all the groupings, - which lets you convert each grouping into a percent of total: - - \`\`\` - sum(products.base_price) / overall_sum(sum(products.base_price)) - \`\`\``, - - `### Recent change - - Use \`reducedTimeRange='30m'\` to add an additional filter on the - time range of a metric aligned with the end of the global time range. - This can be used to calculate how much a value changed recently. - - \`\`\` - max(system.network.in.bytes, reducedTimeRange="30m") - - min(system.network.in.bytes, reducedTimeRange="30m") - \`\`\` - `, - ], - }, - { - id: 'lens_formulas_elasticsearch_functions', - texts: [ - `## Elasticsearch functions - - These functions will be executed on the raw documents for each row of the - resulting table, aggregating all documents matching the break down - dimensions into a single value.`, - - `#### average(field: string) - Returns the average of a field. This function only works for number fields. - - Example: Get the average of price: \`average(price)\` - - Example: Get the average of price for orders from the UK: \`average(price, - kql='location:UK')\``, - - `#### count([field: string]) - The total number of documents. When you provide a field, the total number of - field values is counted. When you use the Count function for fields that have - multiple values in a single document, all values are counted. - - To calculate the total number of documents, use \`count().\` - - To calculate the number of products in all orders, use \`count(products.id)\`. - - To calculate the number of documents that match a specific filter, use - \`count(kql='price > 500')\`.`, - - `#### last_value(field: string) - Returns the value of a field from the last document, ordered by the default - time field of the data view. - - This function is usefull the retrieve the latest state of an entity. - - Example: Get the current status of server A: \`last_value(server.status, - kql='server.name="A"')\``, - - `#### max(field: string) - Returns the max of a field. This function only works for number fields. - - Example: Get the max of price: \`max(price)\` - - Example: Get the max of price for orders from the UK: \`max(price, - kql='location:UK')\``, - - `#### median(field: string) - Returns the median of a field. This function only works for number fields. - - Example: Get the median of price: \`median(price)\` - - Example: Get the median of price for orders from the UK: \`median(price, - kql='location:UK')\``, - - `#### min(field: string) - Returns the min of a field. This function only works for number fields. - - Example: Get the min of price: \`min(price)\` - - Example: Get the min of price for orders from the UK: \`min(price, - kql='location:UK')\``, - - `#### percentile(field: string, [percentile]: number) - Returns the specified percentile of the values of a field. This is the value n - percent of the values occuring in documents are smaller. - - Example: Get the number of bytes larger than 95 % of values: - \`percentile(bytes, percentile=95)\``, - - `#### percentile_rank(field: string, [value]: number) - Returns the percentage of values which are below a certain value. For example, - if a value is greater than or equal to 95% of the observed values it is said to - be at the 95th percentile rank - - Example: Get the percentage of values which are below of 100: - \`percentile_rank(bytes, value=100)\``, - - `#### standard_deviation(field: string) - Returns the amount of variation or dispersion of the field. The function works - only for number fields. - - Example: To get the standard deviation of price, use - \`standard_deviation(price).\` - - Example: To get the variance of price for orders from the UK, use - \`square(standard_deviation(price, kql='location:UK'))\`.`, - - `#### sum(field: string) - Returns the sum of a field. This function only works for number fields. - - Example: Get the sum of price: sum(price) - - Example: Get the sum of price for orders from the UK: \`sum(price, - kql='location:UK')\``, - - `#### unique_count(field: string) - Calculates the number of unique values of a specified field. Works for number, - string, date and boolean values. - - Example: Calculate the number of different products: - \`unique_count(product.name)\` - - Example: Calculate the number of different products from the "clothes" group: - \`unique_count(product.name, kql='product.group=clothes')\` - - `, - ], - }, - { - id: 'lens_formulas_column_functions', - texts: [ - `## Column calculations - These functions are executed for each row, but are provided with the whole - column as context. This is also known as a window function.`, - - `#### counter_rate(metric: number) - Calculates the rate of an ever increasing counter. This function will only - yield helpful results on counter metric fields which contain a measurement of - some kind monotonically growing over time. If the value does get smaller, it - will interpret this as a counter reset. To get most precise results, - counter_rate should be calculated on the max of a field. - - This calculation will be done separately for separate series defined by filters - or top values dimensions. It uses the current interval when used in Formula. - - Example: Visualize the rate of bytes received over time by a memcached server: - counter_rate(max(memcached.stats.read.bytes))`, - - `cumulative_sum(metric: number) - Calculates the cumulative sum of a metric over time, adding all previous values - of a series to each value. To use this function, you need to configure a date - histogram dimension as well. - - This calculation will be done separately for separate series defined by filters - or top values dimensions. - - Example: Visualize the received bytes accumulated over time: - cumulative_sum(sum(bytes))`, - - `differences(metric: number) - Calculates the difference to the last value of a metric over time. To use this - function, you need to configure a date histogram dimension as well. Differences - requires the data to be sequential. If your data is empty when using - differences, try increasing the date histogram interval. - - This calculation will be done separately for separate series defined by filters - or top values dimensions. - - Example: Visualize the change in bytes received over time: - differences(sum(bytes))`, - - `moving_average(metric: number, [window]: number) - Calculates the moving average of a metric over time, averaging the last n-th - values to calculate the current value. To use this function, you need to - configure a date histogram dimension as well. The default window value is 5. - - This calculation will be done separately for separate series defined by filters - or top values dimensions. - - Takes a named parameter window which specifies how many last values to include - in the average calculation for the current value. - - Example: Smooth a line of measurements: moving_average(sum(bytes), window=5)`, - - `normalize_by_unit(metric: number, unit: s|m|h|d|w|M|y) - This advanced function is useful for normalizing counts and sums to a specific - time interval. It allows for integration with metrics that are stored already - normalized to a specific time interval. - - This function can only be used if there's a date histogram function used in the - current chart. - - Example: A ratio comparing an already normalized metric to another metric that - needs to be normalized. - normalize_by_unit(counter_rate(max(system.diskio.write.bytes)), unit='s') / - last_value(apache.status.bytes_per_second)`, - - `overall_average(metric: number) - Calculates the average of a metric for all data points of a series in the - current chart. A series is defined by a dimension using a date histogram or - interval function. Other dimensions breaking down the data like top values or - filter are treated as separate series. - - If no date histograms or interval functions are used in the current chart, - overall_average is calculating the average over all dimensions no matter the - used function - - Example: Divergence from the mean: sum(bytes) - overall_average(sum(bytes))`, - - `overall_max(metric: number) - Calculates the maximum of a metric for all data points of a series in the - current chart. A series is defined by a dimension using a date histogram or - interval function. Other dimensions breaking down the data like top values or - filter are treated as separate series. - - If no date histograms or interval functions are used in the current chart, - overall_max is calculating the maximum over all dimensions no matter the used - function - - Example: Percentage of range (sum(bytes) - overall_min(sum(bytes))) / - (overall_max(sum(bytes)) - overall_min(sum(bytes)))`, - - `overall_min(metric: number) - Calculates the minimum of a metric for all data points of a series in the - current chart. A series is defined by a dimension using a date histogram or - interval function. Other dimensions breaking down the data like top values or - filter are treated as separate series. - - If no date histograms or interval functions are used in the current chart, - overall_min is calculating the minimum over all dimensions no matter the used - function - - Example: Percentage of range (sum(bytes) - overall_min(sum(bytes)) / - (overall_max(sum(bytes)) - overall_min(sum(bytes)))`, - - `overall_sum(metric: number) - Calculates the sum of a metric of all data points of a series in the current - chart. A series is defined by a dimension using a date histogram or interval - function. Other dimensions breaking down the data like top values or filter are - treated as separate series. - - If no date histograms or interval functions are used in the current chart, - overall_sum is calculating the sum over all dimensions no matter the used - function. - - Example: Percentage of total sum(bytes) / overall_sum(sum(bytes))`, - ], - }, - { - id: 'lens_formulas_math_functions', - texts: [ - `Math - These functions will be executed for reach row of the resulting table using single values from the same row calculated using other functions.`, - - `abs([value]: number) - Calculates absolute value. A negative value is multiplied by -1, a positive value stays the same. - - Example: Calculate average distance to sea level abs(average(altitude))`, - - `add([left]: number, [right]: number) - Adds up two numbers. - - Also works with + symbol. - - Example: Calculate the sum of two fields - - sum(price) + sum(tax) - - Example: Offset count by a static value - - add(count(), 5)`, - - `cbrt([value]: number) - Cube root of value. - - Example: Calculate side length from volume - - cbrt(last_value(volume)) - - ceil([value]: number) - Ceiling of value, rounds up. - - Example: Round up price to the next dollar - - ceil(sum(price))`, - - `clamp([value]: number, [min]: number, [max]: number) - Limits the value from a minimum to maximum. - - Example: Make sure to catch outliers - - clamp( - average(bytes), - percentile(bytes, percentile=5), - percentile(bytes, percentile=95) - )`, - `cube([value]: number) - Calculates the cube of a number. - - Example: Calculate volume from side length - - cube(last_value(length))`, - - `defaults([value]: number, [default]: number) - Returns a default numeric value when value is null. - - Example: Return -1 when a field has no data - - defaults(average(bytes), -1)`, - - `divide([left]: number, [right]: number) - Divides the first number by the second number. - - Also works with / symbol - - Example: Calculate profit margin - - sum(profit) / sum(revenue) - - Example: divide(sum(bytes), 2)`, - - `exp([value]: number) - Raises e to the nth power. - - Example: Calculate the natural exponential function - - exp(last_value(duration))`, - - `fix([value]: number) - For positive values, takes the floor. For negative values, takes the ceiling. - - Example: Rounding towards zero - - fix(sum(profit))`, - - `floor([value]: number) - Round down to nearest integer value - - Example: Round down a price - - floor(sum(price))`, - - `log([value]: number, [base]?: number) - Logarithm with optional base. The natural base e is used as default. - - Example: Calculate number of bits required to store values - - log(sum(bytes)) - log(sum(bytes), 2)`, - `mod([value]: number, [base]: number) - Remainder after dividing the function by a number - - Example: Calculate last three digits of a value - - mod(sum(price), 1000)`, - - `multiply([left]: number, [right]: number) - Multiplies two numbers. - - Also works with * symbol. - - Example: Calculate price after current tax rate - - sum(bytes) * last_value(tax_rate) - - Example: Calculate price after constant tax rate - - multiply(sum(price), 1.2)`, - - `pick_max([left]: number, [right]: number) - Finds the maximum value between two numbers. - - Example: Find the maximum between two fields averages - - pick_max(average(bytes), average(memory))`, - - `pick_min([left]: number, [right]: number) - Finds the minimum value between two numbers. - - Example: Find the minimum between two fields averages - - pick_min(average(bytes), average(memory))`, - - `pow([value]: number, [base]: number) - Raises the value to a certain power. The second argument is required - - Example: Calculate volume based on side length - - pow(last_value(length), 3)`, - - `round([value]: number, [decimals]?: number) - Rounds to a specific number of decimal places, default of 0 - - Examples: Round to the cent - - round(sum(bytes)) - round(sum(bytes), 2)`, - `sqrt([value]: number) - Square root of a positive value only - - Example: Calculate side length based on area - - sqrt(last_value(area))`, - - `square([value]: number) - Raise the value to the 2nd power - - Example: Calculate area based on side length - - square(last_value(length))`, - - `subtract([left]: number, [right]: number) - Subtracts the first number from the second number. - - Also works with - symbol. - - Example: Calculate the range of a field - - subtract(max(bytes), min(bytes))`, - ], - }, - { - id: 'lens_formulas_comparison_functions', - texts: [ - `Comparison - These functions are used to perform value comparison.`, - - `eq([left]: number, [right]: number) - Performs an equality comparison between two values. - - To be used as condition for ifelse comparison function. - - Also works with == symbol. - - Example: Returns true if the average of bytes is exactly the same amount of average memory - - average(bytes) == average(memory) - - Example: eq(sum(bytes), 1000000)`, - - `gt([left]: number, [right]: number) - Performs a greater than comparison between two values. - - To be used as condition for ifelse comparison function. - - Also works with > symbol. - - Example: Returns true if the average of bytes is greater than the average amount of memory - - average(bytes) > average(memory) - - Example: gt(average(bytes), 1000)`, - - `gte([left]: number, [right]: number) - Performs a greater than comparison between two values. - - To be used as condition for ifelse comparison function. - - Also works with >= symbol. - - Example: Returns true if the average of bytes is greater than or equal to the average amount of memory - - average(bytes) >= average(memory) - - Example: gte(average(bytes), 1000)`, - - `ifelse([condition]: boolean, [left]: number, [right]: number) - Returns a value depending on whether the element of condition is true or false. - - Example: Average revenue per customer but in some cases customer id is not provided which counts as additional customer - - sum(total)/(unique_count(customer_id) + ifelse( count() > count(kql='customer_id:*'), 1, 0))`, - - `lt([left]: number, [right]: number) - Performs a lower than comparison between two values. - - To be used as condition for ifelse comparison function. - - Also works with < symbol. - - Example: Returns true if the average of bytes is lower than the average amount of memory - - average(bytes) <= average(memory) - - Example: lt(average(bytes), 1000)`, - - `lte([left]: number, [right]: number) - Performs a lower than or equal comparison between two values. - - To be used as condition for ifelse comparison function. - - Also works with <= symbol. - - Example: Returns true if the average of bytes is lower than or equal to the average amount of memory - - average(bytes) <= average(memory) - - Example: lte(average(bytes), 1000)`, - ], - }, - { - id: 'lens_formulas_kibana_context', - text: dedent(`Kibana context - - These functions are used to retrieve Kibana context variables, which are the - date histogram \`interval\`, the current \`now\` and the selected \`time_range\` - and help you to compute date math operations. - - interval() - The specified minimum interval for the date histogram, in milliseconds (ms). - - now() - The current now moment used in Kibana expressed in milliseconds (ms). - - time_range() - The specified time range, in milliseconds (ms).`), - }, - ]); -} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/types.ts index 2df3f36163972..2e24cf25902e0 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/types.ts @@ -17,7 +17,6 @@ import type { import type { Message, ObservabilityAIAssistantScreenContextRequest, - InstructionOrPlainText, AdHocInstruction, } from '../../common/types'; import type { ObservabilityAIAssistantRouteHandlerResources } from '../routes/types'; @@ -67,13 +66,13 @@ export interface FunctionHandler { respond: RespondFunction; } -export type InstructionOrCallback = InstructionOrPlainText | RegisterInstructionCallback; +export type InstructionOrCallback = string | RegisterInstructionCallback; export type RegisterInstructionCallback = ({ availableFunctionNames, }: { availableFunctionNames: string[]; -}) => InstructionOrPlainText | InstructionOrPlainText[] | undefined; +}) => string | string[] | undefined; export type RegisterInstruction = (...instruction: InstructionOrCallback[]) => void; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_system_message_from_instructions.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_system_message_from_instructions.test.ts index 8e4075bed7b9d..82f22e08d1fc7 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_system_message_from_instructions.test.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_system_message_from_instructions.test.ts @@ -41,10 +41,10 @@ describe('getSystemMessageFromInstructions', () => { expect( getSystemMessageFromInstructions({ applicationInstructions: ['first'], - userInstructions: [{ doc_id: 'second', text: 'second from kb' }], + userInstructions: [{ id: 'second', text: 'second from kb' }], adHocInstructions: [ { - doc_id: 'second', + id: 'second', text: 'second from adhoc instruction', instruction_type: 'user_instruction', }, @@ -58,7 +58,7 @@ describe('getSystemMessageFromInstructions', () => { expect( getSystemMessageFromInstructions({ applicationInstructions: ['first'], - userInstructions: [{ doc_id: 'second', text: 'second_kb' }], + userInstructions: [{ id: 'second', text: 'second_kb' }], adHocInstructions: [], availableFunctionNames: [], }) diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_system_message_from_instructions.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_system_message_from_instructions.ts index 570449673084b..7b59b4ce59219 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_system_message_from_instructions.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_system_message_from_instructions.ts @@ -45,7 +45,7 @@ export function getSystemMessageFromInstructions({ const adHocInstructionsWithId = adHocInstructions.map((adHocInstruction) => ({ ...adHocInstruction, - doc_id: adHocInstruction?.doc_id ?? v4(), + id: adHocInstruction?.id ?? v4(), })); // split ad hoc instructions into user instructions and application instructions @@ -55,15 +55,16 @@ export function getSystemMessageFromInstructions({ ); // all adhoc instructions and KB instructions. - // adhoc instructions will be prioritized over Knowledge Base instructions if the doc_id is the same + // adhoc instructions will be prioritized over Knowledge Base instructions if the id is the same const allUserInstructions = withTokenBudget( - uniqBy([...adHocUserInstructions, ...kbUserInstructions], (i) => i.doc_id), + uniqBy([...adHocUserInstructions, ...kbUserInstructions], (i) => i.id), 1000 ); return [ // application instructions - ...allApplicationInstructions.concat(adHocApplicationInstructions), + ...allApplicationInstructions, + ...adHocApplicationInstructions, // user instructions ...(allUserInstructions.length ? [USER_INSTRUCTIONS_HEADER, ...allUserInstructions] : []), diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/split_kb_text.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/split_kb_text.ts deleted file mode 100644 index 9a2f047b60f9b..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/split_kb_text.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { merge } from 'lodash'; -import type { KnowledgeBaseEntry } from '../../../common/types'; -import { - type KnowledgeBaseEntryOperation, - KnowledgeBaseEntryOperationType, -} from '../knowledge_base_service'; - -export function splitKbText({ - id, - texts, - ...rest -}: Omit & { texts: string[] }): KnowledgeBaseEntryOperation[] { - return [ - { - type: KnowledgeBaseEntryOperationType.Delete, - doc_id: id, - labels: {}, - }, - ...texts.map((text, index) => ({ - type: KnowledgeBaseEntryOperationType.Index, - document: merge({}, rest, { - id: [id, index].join('_'), - doc_id: id, - labels: {}, - text, - }), - })), - ]; -} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/recall_and_score.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/recall_and_score.ts index 8885ff7e1d7a2..fefe324805e59 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/recall_and_score.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/recall_and_score.ts @@ -7,13 +7,14 @@ import type { Logger } from '@kbn/logging'; import { AnalyticsServiceStart } from '@kbn/core/server'; +import { scoreSuggestions } from './score_suggestions'; import type { Message } from '../../../common'; import type { ObservabilityAIAssistantClient } from '../../service/client'; import type { FunctionCallChatFunction } from '../../service/types'; -import { retrieveSuggestions } from './retrieve_suggestions'; -import { scoreSuggestions } from './score_suggestions'; -import type { RetrievedSuggestion } from './types'; -import { RecallRanking, RecallRankingEventType } from '../../analytics/recall_ranking'; +import { RecallRanking, recallRankingEventType } from '../../analytics/recall_ranking'; +import { RecalledEntry } from '../../service/knowledge_base_service'; + +export type RecalledSuggestion = Pick; export async function recallAndScore({ recall, @@ -34,19 +35,18 @@ export async function recallAndScore({ logger: Logger; signal: AbortSignal; }): Promise<{ - relevantDocuments?: RetrievedSuggestion[]; + relevantDocuments?: RecalledSuggestion[]; scores?: Array<{ id: string; score: number }>; - suggestions: RetrievedSuggestion[]; + suggestions: RecalledSuggestion[]; }> { const queries = [ { text: userPrompt, boost: 3 }, { text: context, boost: 1 }, ].filter((query) => query.text.trim()); - const suggestions = await retrieveSuggestions({ - recall, - queries, - }); + const suggestions: RecalledSuggestion[] = (await recall({ queries })).map( + ({ id, text, score }) => ({ id, text, score }) + ); if (!suggestions.length) { return { @@ -67,7 +67,7 @@ export async function recallAndScore({ chat, }); - analytics.reportEvent(RecallRankingEventType, { + analytics.reportEvent(recallRankingEventType, { prompt: queries.map((query) => query.text).join('\n\n'), scoredDocuments: suggestions.map((suggestion) => { const llmScore = scores.find((score) => score.id === suggestion.id); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/retrieve_suggestions.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/retrieve_suggestions.ts deleted file mode 100644 index 3c680229cd5d2..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/retrieve_suggestions.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { omit } from 'lodash'; -import { ObservabilityAIAssistantClient } from '../../service/client'; -import { RetrievedSuggestion } from './types'; - -export async function retrieveSuggestions({ - queries, - recall, -}: { - queries: Array<{ text: string; boost?: number }>; - recall: ObservabilityAIAssistantClient['recall']; -}): Promise { - const recallResponse = await recall({ - queries, - }); - - return recallResponse.entries.map((entry) => omit(entry, 'labels', 'is_correction')); -} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts index 009b91a7a8c2c..7d1a19463cceb 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts @@ -5,15 +5,15 @@ * 2.0. */ import * as t from 'io-ts'; -import { omit } from 'lodash'; import { Logger } from '@kbn/logging'; import dedent from 'dedent'; import { lastValueFrom } from 'rxjs'; import { decodeOrThrow, jsonRt } from '@kbn/io-ts-utils'; +import { omit } from 'lodash'; import { concatenateChatCompletionChunks, Message, MessageRole } from '../../../common'; import type { FunctionCallChatFunction } from '../../service/types'; -import type { RetrievedSuggestion } from './types'; import { parseSuggestionScores } from './parse_suggestion_scores'; +import { RecalledSuggestion } from './recall_and_score'; import { ShortIdTable } from '../../../common/utils/short_id_table'; const scoreFunctionRequestRt = t.type({ @@ -38,7 +38,7 @@ export async function scoreSuggestions({ signal, logger, }: { - suggestions: RetrievedSuggestion[]; + suggestions: RecalledSuggestion[]; messages: Message[]; userPrompt: string; context: string; @@ -46,28 +46,21 @@ export async function scoreSuggestions({ signal: AbortSignal; logger: Logger; }): Promise<{ - relevantDocuments: RetrievedSuggestion[]; + relevantDocuments: RecalledSuggestion[]; scores: Array<{ id: string; score: number }>; }> { const shortIdTable = new ShortIdTable(); - const suggestionsWithShortId = suggestions.map((suggestion) => ({ - ...omit(suggestion, 'score', 'id'), // To not bias the LLM - originalId: suggestion.id, - shortId: shortIdTable.take(suggestion.id), - })); - const newUserMessageContent = - dedent(`Given the following question, score the documents that are relevant to the question. on a scale from 0 to 7, - 0 being completely irrelevant, and 7 being extremely relevant. Information is relevant to the question if it helps in - answering the question. Judge it according to the following criteria: - - - The document is relevant to the question, and the rest of the conversation - - The document has information relevant to the question that is not mentioned, - or more detailed than what is available in the conversation - - The document has a high amount of information relevant to the question compared to other documents - - The document contains new information not mentioned before in the conversation - + dedent(`Given the following prompt, score the documents that are relevant to the prompt on a scale from 0 to 7, + 0 being completely irrelevant, and 7 being extremely relevant. Information is relevant to the prompt if it helps in + answering the prompt. Judge the document according to the following criteria: + + - The document is relevant to the prompt, and the rest of the conversation + - The document has information relevant to the prompt that is not mentioned, or more detailed than what is available in the conversation + - The document has a high amount of information relevant to the prompt compared to other documents + - The document contains new information not mentioned before in the conversation or provides a correction to previously stated information. + User prompt: ${userPrompt} @@ -76,9 +69,9 @@ export async function scoreSuggestions({ Documents: ${JSON.stringify( - suggestionsWithShortId.map((suggestion) => ({ - id: suggestion.shortId, - content: suggestion.text, + suggestions.map((suggestion) => ({ + ...omit(suggestion, 'score'), // Omit score to not bias the LLM + id: shortIdTable.take(suggestion.id), // Shorten id to save tokens })), null, 2 @@ -127,15 +120,9 @@ export async function scoreSuggestions({ scoreFunctionRequest.message.function_call.arguments ); - const scores = parseSuggestionScores(scoresAsString).map(({ id, score }) => { - const originalSuggestion = suggestionsWithShortId.find( - (suggestion) => suggestion.shortId === id - ); - return { - originalId: originalSuggestion?.originalId, - score, - }; - }); + const scores = parseSuggestionScores(scoresAsString) + // Restore original IDs + .map(({ id, score }) => ({ id: shortIdTable.lookup(id)!, score })); if (scores.length === 0) { // seemingly invalid or no scores, return all @@ -144,12 +131,13 @@ export async function scoreSuggestions({ const suggestionIds = suggestions.map((document) => document.id); + // get top 5 documents ids with scores > 4 const relevantDocumentIds = scores - .filter((document) => suggestionIds.includes(document.originalId ?? '')) // Remove hallucinated documents - .filter((document) => document.score > 4) + .filter(({ score }) => score > 4) .sort((a, b) => b.score - a.score) .slice(0, 5) - .map((document) => document.originalId); + .filter(({ id }) => suggestionIds.includes(id ?? '')) // Remove hallucinated documents + .map(({ id }) => id); const relevantDocuments = suggestions.filter((suggestion) => relevantDocumentIds.includes(suggestion.id) @@ -159,6 +147,6 @@ export async function scoreSuggestions({ return { relevantDocuments, - scores: scores.map((score) => ({ id: score.originalId!, score: score.score })), + scores: scores.map((score) => ({ id: score.id, score: score.score })), }; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/types.ts deleted file mode 100644 index 3774df64c1ee1..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/utils/recall/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RecalledEntry } from '../../service/knowledge_base_service'; - -export type RetrievedSuggestion = Omit; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/categorize_entries.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/categorize_entries.ts index 8d32ea4664f9f..7ea25eab8c661 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/categorize_entries.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/helpers/categorize_entries.ts @@ -9,21 +9,30 @@ import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/ export interface KnowledgeBaseEntryCategory { '@timestamp': string; - categoryName: string; + categoryKey: string; + title: string; entries: KnowledgeBaseEntry[]; } -export function categorizeEntries({ entries }: { entries: KnowledgeBaseEntry[] }) { +export function categorizeEntries({ + entries, +}: { + entries: KnowledgeBaseEntry[]; +}): KnowledgeBaseEntryCategory[] { return entries.reduce((acc, entry) => { - const categoryName = entry.labels?.category ?? entry.id; - - const index = acc.findIndex((item) => item.categoryName === categoryName); + const categoryKey = entry.labels?.category ?? entry.id; - if (index > -1) { - acc[index].entries.push(entry); + const existingEntry = acc.find((item) => item.categoryKey === categoryKey); + if (existingEntry) { + existingEntry.entries.push(entry); return acc; - } else { - return acc.concat({ categoryName, entries: [entry], '@timestamp': entry['@timestamp'] }); } - }, [] as Array<{ categoryName: string; entries: KnowledgeBaseEntry[]; '@timestamp': string }>); + + return acc.concat({ + categoryKey, + title: entry.labels?.category ?? entry.title ?? 'No title', + entries: [entry], + '@timestamp': entry['@timestamp'], + }); + }, [] as Array<{ categoryKey: string; title: string; entries: KnowledgeBaseEntry[]; '@timestamp': string }>); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_create_knowledge_base_entry.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_create_knowledge_base_entry.ts index 459de7be2d528..4cc6c8e2b9bf1 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_create_knowledge_base_entry.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_create_knowledge_base_entry.ts @@ -28,10 +28,9 @@ export function useCreateKnowledgeBaseEntry() { void, ServerError, { - entry: Omit< - KnowledgeBaseEntry, - '@timestamp' | 'confidence' | 'is_correction' | 'role' | 'doc_id' - >; + entry: Omit & { + title: string; + }; } >( [REACT_QUERY_KEYS.CREATE_KB_ENTRIES], @@ -41,10 +40,7 @@ export function useCreateKnowledgeBaseEntry() { { signal: null, params: { - body: { - ...entry, - role: 'user_entry', - }, + body: entry, }, } ); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_create_knowledge_base_user_instruction.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_create_knowledge_base_user_instruction.ts index b51e45a3bdd6b..8adf5a7d8cfd0 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_create_knowledge_base_user_instruction.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_create_knowledge_base_user_instruction.ts @@ -32,7 +32,7 @@ export function useCreateKnowledgeBaseUserInstruction() { signal: null, params: { body: { - id: entry.doc_id, + id: entry.id, text: entry.text, public: entry.public, }, @@ -62,7 +62,7 @@ export function useCreateKnowledgeBaseUserInstruction() { 'xpack.observabilityAiAssistantManagement.kb.addUserInstruction.errorNotification', { defaultMessage: 'Something went wrong while creating {name}', - values: { name: entry.doc_id }, + values: { name: entry.id }, } ), }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_import_knowledge_base_entries.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_import_knowledge_base_entries.ts index 9ff0748793bc8..239e72d99109e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_import_knowledge_base_entries.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_import_knowledge_base_entries.ts @@ -30,7 +30,7 @@ export function useImportKnowledgeBaseEntries() { Omit< KnowledgeBaseEntry, '@timestamp' | 'confidence' | 'is_correction' | 'public' | 'labels' - > + > & { title: string } >; } >( diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_bulk_import_flyout.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_bulk_import_flyout.tsx index ac7632243a2a0..23d5ad00af12a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_bulk_import_flyout.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_bulk_import_flyout.tsx @@ -47,15 +47,13 @@ export function KnowledgeBaseBulkImportFlyout({ onClose }: { onClose: () => void }; const handleSubmitNewEntryClick = async () => { - let entries: Array> = []; + let entries: Array & { title: string }> = []; const text = await files[0].text(); const elements = text.split('\n').filter(Boolean); try { - entries = elements.map((el) => JSON.parse(el)) as Array< - Omit - >; + entries = elements.map((el) => JSON.parse(el)); } catch (_) { toasts.addError( new Error( diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_category_flyout.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_category_flyout.tsx index fab3b34809c19..8dcf76e4bc56e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_category_flyout.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_category_flyout.tsx @@ -104,13 +104,13 @@ export function KnowledgeBaseCategoryFlyout({ ]; const hasDescription = - CATEGORY_MAP[category.categoryName as unknown as keyof typeof CATEGORY_MAP]?.description; + CATEGORY_MAP[category.categoryKey as unknown as keyof typeof CATEGORY_MAP]?.description; return ( -

{capitalize(category.categoryName)}

+

{capitalize(category.categoryKey)}

diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_manual_entry_flyout.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_manual_entry_flyout.tsx index d809b6cd96d6d..20c7a75a401a8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_manual_entry_flyout.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_manual_entry_flyout.tsx @@ -26,7 +26,9 @@ import { EuiTitle, } from '@elastic/eui'; import moment from 'moment'; -import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types'; +import { KnowledgeBaseEntryRole } from '@kbn/observability-ai-assistant-plugin/public'; +import { type KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common'; +import { v4 } from 'uuid'; import { useCreateKnowledgeBaseEntry } from '../../hooks/use_create_knowledge_base_entry'; import { useDeleteKnowledgeBaseEntry } from '../../hooks/use_delete_knowledge_base_entry'; import { useKibana } from '../../hooks/use_kibana'; @@ -45,20 +47,24 @@ export function KnowledgeBaseEditManualEntryFlyout({ const { mutateAsync: deleteEntry, isLoading: isDeleting } = useDeleteKnowledgeBaseEntry(); const [isPublic, setIsPublic] = useState(entry?.public ?? false); - - const [newEntryId, setNewEntryId] = useState(entry?.id ?? ''); + const [newEntryTitle, setNewEntryTitle] = useState(entry?.title ?? ''); const [newEntryText, setNewEntryText] = useState(entry?.text ?? ''); - const isEntryIdInvalid = newEntryId.trim() === ''; + const isEntryTitleInvalid = newEntryTitle.trim() === ''; const isEntryTextInvalid = newEntryText.trim() === ''; - const isFormInvalid = isEntryIdInvalid || isEntryTextInvalid; + const isFormInvalid = isEntryTitleInvalid || isEntryTextInvalid; const handleSubmit = async () => { await createEntry({ entry: { - id: newEntryId, + id: entry?.id ?? v4(), + title: newEntryTitle, text: newEntryText, public: isPublic, + role: KnowledgeBaseEntryRole.UserEntry, + confidence: 'high', + is_correction: false, + labels: {}, }, }); @@ -85,8 +91,8 @@ export function KnowledgeBaseEditManualEntryFlyout({ : i18n.translate( 'xpack.observabilityAiAssistantManagement.knowledgeBaseNewEntryFlyout.h2.editEntryLabel', { - defaultMessage: 'Edit {id}', - values: { id: entry.id }, + defaultMessage: 'Edit {title}', + values: { title: entry?.title }, } )} @@ -94,23 +100,7 @@ export function KnowledgeBaseEditManualEntryFlyout({ - {!entry ? ( - - setNewEntryId(e.target.value)} - isInvalid={isEntryIdInvalid} - /> - - ) : ( + {entry ? ( @@ -136,7 +126,26 @@ export function KnowledgeBaseEditManualEntryFlyout({ - )} + ) : null} + + + + + setNewEntryTitle(e.target.value)} + isInvalid={isEntryTitleInvalid} + /> + + diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_user_instruction_flyout.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_user_instruction_flyout.tsx index e8152738e3807..1c05ca6d52b3f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_user_instruction_flyout.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_edit_user_instruction_flyout.tsx @@ -30,19 +30,19 @@ export function KnowledgeBaseEditUserInstructionFlyout({ onClose }: { onClose: ( const { userInstructions, isLoading: isFetching } = useGetUserInstructions(); const { mutateAsync: createEntry, isLoading: isSaving } = useCreateKnowledgeBaseUserInstruction(); const [newEntryText, setNewEntryText] = useState(''); - const [newEntryDocId, setNewEntryDocId] = useState(); + const [newEntryId, setNewEntryId] = useState(); const isSubmitDisabled = newEntryText.trim() === ''; useEffect(() => { const userInstruction = userInstructions?.find((entry) => !entry.public); - setNewEntryDocId(userInstruction?.doc_id); setNewEntryText(userInstruction?.text ?? ''); + setNewEntryId(userInstruction?.id); }, [userInstructions]); const handleSubmit = async () => { await createEntry({ entry: { - doc_id: newEntryDocId ?? uuidv4(), + id: newEntryId ?? uuidv4(), text: newEntryText, public: false, // limit user instructions to private (for now) }, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.test.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.test.tsx index d8e2897c6878c..50d0cb8ba47c8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.test.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.test.tsx @@ -81,7 +81,18 @@ describe('KnowledgeBaseTab', () => { getByTestId('knowledgeBaseEditManualEntryFlyoutSaveButton').click(); - expect(createMock).toHaveBeenCalledWith({ entry: { id: 'foo', public: false, text: 'bar' } }); + expect(createMock).toHaveBeenCalledWith({ + entry: { + id: expect.any(String), + title: 'foo', + public: false, + text: 'bar', + role: 'user_entry', + confidence: 'high', + is_correction: false, + labels: expect.any(Object), + }, + }); }); it('should require an id', () => { @@ -126,7 +137,7 @@ describe('KnowledgeBaseTab', () => { entries: [ { id: 'test', - doc_id: 'test', + title: 'test', text: 'test', '@timestamp': 1638340456, labels: {}, @@ -134,7 +145,7 @@ describe('KnowledgeBaseTab', () => { }, { id: 'test2', - doc_id: 'test2', + title: 'test2', text: 'test', '@timestamp': 1638340456, labels: { @@ -144,7 +155,7 @@ describe('KnowledgeBaseTab', () => { }, { id: 'test3', - doc_id: 'test3', + title: 'test3', text: 'test', '@timestamp': 1638340456, labels: { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.tsx index 6ba09101b6227..23fbf796290c8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/knowledge_base_tab.tsx @@ -57,10 +57,10 @@ export function KnowledgeBaseTab() { data-test-subj="pluginsColumnsButton" onClick={() => setSelectedCategory(category)} aria-label={ - category.categoryName === selectedCategory?.categoryName ? 'Collapse' : 'Expand' + category.categoryKey === selectedCategory?.categoryKey ? 'Collapse' : 'Expand' } iconType={ - category.categoryName === selectedCategory?.categoryName ? 'minimize' : 'expand' + category.categoryKey === selectedCategory?.categoryKey ? 'minimize' : 'expand' } /> ); @@ -85,7 +85,8 @@ export function KnowledgeBaseTab() { width: '40px', }, { - field: 'categoryName', + 'data-test-subj': 'knowledgeBaseTableTitleCell', + field: 'title', name: i18n.translate('xpack.observabilityAiAssistantManagement.kbTab.columns.name', { defaultMessage: 'Name', }), @@ -107,6 +108,7 @@ export function KnowledgeBaseTab() { }, }, { + 'data-test-subj': 'knowledgeBaseTableAuthorCell', name: i18n.translate('xpack.observabilityAiAssistantManagement.kbTab.columns.author', { defaultMessage: 'Author', }), @@ -183,7 +185,7 @@ export function KnowledgeBaseTab() { const [isNewEntryPopoverOpen, setIsNewEntryPopoverOpen] = useState(false); const [isEditUserInstructionFlyoutOpen, setIsEditUserInstructionFlyoutOpen] = useState(false); const [query, setQuery] = useState(''); - const [sortBy, setSortBy] = useState<'doc_id' | '@timestamp'>('doc_id'); + const [sortBy, setSortBy] = useState('title'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const { @@ -193,17 +195,10 @@ export function KnowledgeBaseTab() { } = useGetKnowledgeBaseEntries({ query, sortBy, sortDirection }); const categorizedEntries = categorizeEntries({ entries }); - const handleChangeSort = ({ - sort, - }: Criteria) => { + const handleChangeSort = ({ sort }: Criteria) => { if (sort) { const { field, direction } = sort; - if (field === '@timestamp') { - setSortBy(field); - } - if (field === 'categoryName') { - setSortBy('doc_id'); - } + setSortBy(field); setSortDirection(direction); } }; @@ -329,7 +324,7 @@ export function KnowledgeBaseTab() { loading={isLoading} sorting={{ sort: { - field: sortBy === 'doc_id' ? 'categoryName' : sortBy, + field: sortBy, direction: sortDirection, }, }} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 31982f6fddd72..7a41f7703eb2a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -32593,7 +32593,6 @@ "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiFormRow.contentsLabel": "Contenu", "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiFormRow.idLabel": "Nom", "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiMarkdownEditor.enterContentsLabel": "Entrer du contenu", - "xpack.observabilityAiAssistantManagement.knowledgeBaseNewEntryFlyout.h2.editEntryLabel": "Modifier {id}", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewEntryFlyout.h2.newEntryLabel": "Nouvelle entrée", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.cancelButtonEmptyLabel": "Annuler", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.euiMarkdownEditor.observabilityAiAssistantKnowledgeBaseViewMarkdownEditorLabel": "observabilityAiAssistantKnowledgeBaseViewMarkdownEditor", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 277fc3eb639bf..ccdc4069314e8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -32340,7 +32340,6 @@ "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiFormRow.contentsLabel": "目次", "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiFormRow.idLabel": "名前", "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiMarkdownEditor.enterContentsLabel": "コンテンツを入力", - "xpack.observabilityAiAssistantManagement.knowledgeBaseNewEntryFlyout.h2.editEntryLabel": "{id}を編集", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewEntryFlyout.h2.newEntryLabel": "新しいエントリー", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.cancelButtonEmptyLabel": "キャンセル", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.euiMarkdownEditor.observabilityAiAssistantKnowledgeBaseViewMarkdownEditorLabel": "observabilityAiAssistantKnowledgeBaseViewMarkdownEditor", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ec974a4b38349..341eec24125a2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -32383,7 +32383,6 @@ "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiFormRow.contentsLabel": "内容", "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiFormRow.idLabel": "名称", "xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiMarkdownEditor.enterContentsLabel": "输入内容", - "xpack.observabilityAiAssistantManagement.knowledgeBaseNewEntryFlyout.h2.editEntryLabel": "编辑 {id}", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewEntryFlyout.h2.newEntryLabel": "新条目", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.cancelButtonEmptyLabel": "取消", "xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.euiMarkdownEditor.observabilityAiAssistantKnowledgeBaseViewMarkdownEditorLabel": "observabilityAiAssistantKnowledgeBaseViewMarkdownEditor", diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/config.ts b/x-pack/test/observability_ai_assistant_api_integration/common/config.ts index 198fcefdc2bc8..427258a6e2910 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/common/config.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/common/config.ts @@ -11,7 +11,7 @@ import { ObservabilityAIAssistantFtrConfigName } from '../configs'; import { getApmSynthtraceEsClient } from './create_synthtrace_client'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; import { getScopedApiClient } from './observability_ai_assistant_api_client'; -import { editorUser, viewerUser } from './users/users'; +import { editor, secondaryEditor, viewer } from './users/users'; export interface ObservabilityAIAssistantFtrConfig { name: ObservabilityAIAssistantFtrConfigName; @@ -23,6 +23,10 @@ export type CreateTestConfig = ReturnType; export type CreateTest = ReturnType; +export type ObservabilityAIAssistantApiClients = Awaited< + ReturnType +>; + export type ObservabilityAIAssistantAPIClient = Awaited< ReturnType >; @@ -46,21 +50,23 @@ export function createObservabilityAIAssistantAPIConfig({ const apmSynthtraceKibanaClient = services.apmSynthtraceKibanaClient(); const allConfigs = config.getAll() as Record; + const getScopedApiClientForUsername = (username: string) => + getScopedApiClient(kibanaServer, username); + return { ...allConfigs, servers, services: { ...services, - getScopedApiClientForUsername: () => { - return (username: string) => getScopedApiClient(kibanaServer, username); - }, + getScopedApiClientForUsername: () => getScopedApiClientForUsername, apmSynthtraceEsClient: (context: InheritedFtrProviderContext) => getApmSynthtraceEsClient(context, apmSynthtraceKibanaClient), observabilityAIAssistantAPIClient: async () => { return { - adminUser: await getScopedApiClient(kibanaServer, 'elastic'), - viewerUser: await getScopedApiClient(kibanaServer, viewerUser.username), - editorUser: await getScopedApiClient(kibanaServer, editorUser.username), + admin: getScopedApiClientForUsername('elastic'), + viewer: getScopedApiClientForUsername(viewer.username), + editor: getScopedApiClientForUsername(editor.username), + secondaryEditor: getScopedApiClientForUsername(secondaryEditor.username), }; }, }, diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts b/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts index b6fa38e52e60b..898954a9bfb97 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts @@ -9,21 +9,27 @@ import { kbnTestConfig } from '@kbn/test'; const password = kbnTestConfig.getUrlParts().password!; export interface User { - username: 'elastic' | 'editor' | 'viewer'; + username: 'elastic' | 'editor' | 'viewer' | 'secondary_editor'; password: string; roles: string[]; } -export const editorUser: User = { +export const editor: User = { username: 'editor', password, roles: ['editor'], }; -export const viewerUser: User = { +export const secondaryEditor: User = { + username: 'secondary_editor', + password, + roles: ['editor'], +}; + +export const viewer: User = { username: 'viewer', password, roles: ['viewer'], }; -export const allUsers = [editorUser, viewerUser]; +export const allUsers = [editor, secondaryEditor, viewer]; diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts index a7606d21408c5..2eb7c6f986cfd 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts @@ -290,7 +290,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { )[0]?.conversation.id; await observabilityAIAssistantAPIClient - .adminUser({ + .admin({ endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -366,7 +366,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ).to.eql(0); const conversations = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/conversations', }) .expect(200); @@ -396,7 +396,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .completeAfterIntercept(); const createResponse = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/chat/complete', params: { body: { @@ -415,7 +415,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { conversationCreatedEvent = getConversationCreatedEvent(createResponse.body); const conversationId = conversationCreatedEvent.conversation.id; - const fullConversation = await observabilityAIAssistantAPIClient.editorUser({ + const fullConversation = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -429,7 +429,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .completeAfterIntercept(); const updatedResponse = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/chat/complete', params: { body: { @@ -460,7 +460,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/helpers.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/helpers.ts index b83221869baec..6ce506c502b5e 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/helpers.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/helpers.ts @@ -43,7 +43,7 @@ export async function invokeChatCompleteWithFunctionRequest({ scopes?: AssistantScope[]; }) { const { body } = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/chat/complete', params: { body: { diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/summarize.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/summarize.spec.ts index 923e8b0206070..34da4270f7721 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/summarize.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/summarize.spec.ts @@ -32,10 +32,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { let connectorId: string; before(async () => { - await clearKnowledgeBase(es); await createKnowledgeBaseModel(ml); await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', }) .expect(200); @@ -55,7 +54,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { name: 'summarize', trigger: MessageRole.User, arguments: JSON.stringify({ - id: 'my-id', + title: 'My Title', text: 'Hello world', is_correction: false, confidence: 'high', @@ -72,28 +71,29 @@ export default function ApiTest({ getService }: FtrProviderContext) { await deleteActionConnector({ supertest, connectorId, log }); await deleteKnowledgeBaseModel(ml); + await clearKnowledgeBase(es); }); it('persists entry in knowledge base', async () => { - const res = await observabilityAIAssistantAPIClient.editorUser({ + const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/entries', params: { query: { query: '', - sortBy: 'doc_id', + sortBy: 'title', sortDirection: 'asc', }, }, }); - const { role, public: isPublic, text, type, user, id } = res.body.entries[0]; + const { role, public: isPublic, text, type, user, title } = res.body.entries[0]; expect(role).to.eql('assistant_summarization'); expect(isPublic).to.eql(false); expect(text).to.eql('Hello world'); expect(type).to.eql('contextual'); expect(user?.name).to.eql('editor'); - expect(id).to.eql('my-id'); + expect(title).to.eql('My Title'); expect(res.body.entries).to.have.length(1); }); }); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/connectors/connectors.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/connectors/connectors.spec.ts index e8363ba41513b..41700b21555fa 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/connectors/connectors.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/connectors/connectors.spec.ts @@ -26,14 +26,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('Returns a 2xx for enterprise license', async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/connectors', }) .expect(200); }); it('returns an empty list of connectors', async () => { - const res = await observabilityAIAssistantAPIClient.editorUser({ + const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/connectors', }); @@ -43,7 +43,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it("returns the gen ai connector if it's been created", async () => { const connectorId = await createProxyActionConnector({ supertest, log, port: 1234 }); - const res = await observabilityAIAssistantAPIClient.editorUser({ + const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/connectors', }); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/conversations/conversations.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/conversations/conversations.spec.ts index 91a418b3000ee..71eb37d357696 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/conversations/conversations.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/conversations/conversations.spec.ts @@ -48,7 +48,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('without conversations', () => { it('returns no conversations when listing', async () => { const response = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/conversations', }) .expect(200); @@ -58,7 +58,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns a 404 for updating conversations', async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -74,7 +74,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns a 404 for retrieving a conversation', async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -92,7 +92,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { >; before(async () => { createResponse = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/conversation', params: { body: { @@ -105,7 +105,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -116,7 +116,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .expect(200); await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -148,7 +148,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns a 404 for updating a non-existing conversation', async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -164,7 +164,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns a 404 for retrieving a non-existing conversation', async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -177,7 +177,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the conversation that was created', async () => { const response = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -192,7 +192,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the created conversation when listing', async () => { const response = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/conversations', }) .expect(200); @@ -210,7 +210,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(async () => { updateResponse = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { @@ -234,7 +234,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the updated conversation after get', async () => { const updateAfterCreateResponse = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', params: { path: { diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts index c8881e82e43bb..27659f62ad579 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts @@ -6,67 +6,66 @@ */ import expect from '@kbn/expect'; +import { type KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { clearKnowledgeBase, createKnowledgeBaseModel, deleteKnowledgeBaseModel } from './helpers'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); const es = getService('es'); - const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); describe('Knowledge base', () => { before(async () => { await createKnowledgeBaseModel(ml); + + await observabilityAIAssistantAPIClient + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup' }) + .expect(200); }); after(async () => { await deleteKnowledgeBaseModel(ml); + await clearKnowledgeBase(es); }); - it('returns 200 on knowledge base setup', async () => { - const res = await observabilityAIAssistantAPIClient - .editorUser({ - endpoint: 'POST /internal/observability_ai_assistant/kb/setup', - }) - .expect(200); - expect(res.body).to.eql({}); - }); describe('when managing a single entry', () => { const knowledgeBaseEntry = { id: 'my-doc-id-1', + title: 'My title', text: 'My content', }; it('returns 200 on create', async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/entries/save', params: { body: knowledgeBaseEntry }, }) .expect(200); - const res = await observabilityAIAssistantAPIClient.editorUser({ + const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/entries', params: { query: { query: '', - sortBy: 'doc_id', + sortBy: 'title', sortDirection: 'asc', }, }, }); const entry = res.body.entries[0]; expect(entry.id).to.equal(knowledgeBaseEntry.id); + expect(entry.title).to.equal(knowledgeBaseEntry.title); expect(entry.text).to.equal(knowledgeBaseEntry.text); }); it('returns 200 on get entries and entry exists', async () => { const res = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/entries', params: { query: { query: '', - sortBy: 'doc_id', + sortBy: 'title', sortDirection: 'asc', }, }, @@ -74,13 +73,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { .expect(200); const entry = res.body.entries[0]; expect(entry.id).to.equal(knowledgeBaseEntry.id); + expect(entry.title).to.equal(knowledgeBaseEntry.title); expect(entry.text).to.equal(knowledgeBaseEntry.text); }); it('returns 200 on delete', async () => { const entryId = 'my-doc-id-1'; await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'DELETE /internal/observability_ai_assistant/kb/entries/{entryId}', params: { path: { entryId }, @@ -89,12 +89,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { .expect(200); const res = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/entries', params: { query: { query: '', - sortBy: 'doc_id', + sortBy: 'title', sortDirection: 'asc', }, }, @@ -108,7 +108,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns 500 on delete not found', async () => { const entryId = 'my-doc-id-1'; await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'DELETE /internal/observability_ai_assistant/kb/entries/{entryId}', params: { path: { entryId }, @@ -117,119 +117,88 @@ export default function ApiTest({ getService }: FtrProviderContext) { .expect(500); }); }); - describe('when managing multiple entries', () => { - before(async () => { - await clearKnowledgeBase(es); - }); - afterEach(async () => { - await clearKnowledgeBase(es); - }); - const knowledgeBaseEntries = [ - { - id: 'my_doc_a', - text: 'My content a', - }, - { - id: 'my_doc_b', - text: 'My content b', - }, - { - id: 'my_doc_c', - text: 'My content c', - }, - ]; - it('returns 200 on create', async () => { - await observabilityAIAssistantAPIClient - .editorUser({ - endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import', - params: { body: { entries: knowledgeBaseEntries } }, - }) - .expect(200); + describe('when managing multiple entries', () => { + async function getEntries({ + query = '', + sortBy = 'title', + sortDirection = 'asc', + }: { query?: string; sortBy?: string; sortDirection?: 'asc' | 'desc' } = {}) { const res = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/entries', params: { - query: { - query: '', - sortBy: 'doc_id', - sortDirection: 'asc', - }, + query: { query, sortBy, sortDirection }, }, }) .expect(200); - expect(res.body.entries.filter((entry) => entry.id.startsWith('my_doc')).length).to.eql(3); - }); - it('allows sorting', async () => { + return omitCategories(res.body.entries); + } + + beforeEach(async () => { + await clearKnowledgeBase(es); + await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import', - params: { body: { entries: knowledgeBaseEntries } }, - }) - .expect(200); - - const res = await observabilityAIAssistantAPIClient - .editorUser({ - endpoint: 'GET /internal/observability_ai_assistant/kb/entries', params: { - query: { - query: '', - sortBy: 'doc_id', - sortDirection: 'desc', + body: { + entries: [ + { + id: 'my_doc_a', + title: 'My title a', + text: 'My content a', + }, + { + id: 'my_doc_b', + title: 'My title b', + text: 'My content b', + }, + { + id: 'my_doc_c', + title: 'My title c', + text: 'My content c', + }, + ], }, }, }) .expect(200); + }); - const entries = res.body.entries.filter((entry) => entry.id.startsWith('my_doc')); - expect(entries[0].id).to.eql('my_doc_c'); - expect(entries[1].id).to.eql('my_doc_b'); - expect(entries[2].id).to.eql('my_doc_a'); - - // asc - const resAsc = await observabilityAIAssistantAPIClient - .editorUser({ - endpoint: 'GET /internal/observability_ai_assistant/kb/entries', - params: { - query: { - query: '', - sortBy: 'doc_id', - sortDirection: 'asc', - }, - }, - }) - .expect(200); + afterEach(async () => { + await clearKnowledgeBase(es); + }); - const entriesAsc = resAsc.body.entries.filter((entry) => entry.id.startsWith('my_doc')); - expect(entriesAsc[0].id).to.eql('my_doc_a'); - expect(entriesAsc[1].id).to.eql('my_doc_b'); - expect(entriesAsc[2].id).to.eql('my_doc_c'); + it('returns 200 on create', async () => { + const entries = await getEntries(); + expect(omitCategories(entries).length).to.eql(3); }); - it('allows searching', async () => { - await observabilityAIAssistantAPIClient - .editorUser({ - endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import', - params: { body: { entries: knowledgeBaseEntries } }, - }) - .expect(200); - const res = await observabilityAIAssistantAPIClient - .editorUser({ - endpoint: 'GET /internal/observability_ai_assistant/kb/entries', - params: { - query: { - query: 'my_doc_a', - sortBy: 'doc_id', - sortDirection: 'asc', - }, - }, - }) - .expect(200); + describe('when sorting ', () => { + const ascendingOrder = ['my_doc_a', 'my_doc_b', 'my_doc_c']; + + it('allows sorting ascending', async () => { + const entries = await getEntries({ sortBy: 'title', sortDirection: 'asc' }); + expect(entries.map(({ id }) => id)).to.eql(ascendingOrder); + }); + + it('allows sorting descending', async () => { + const entries = await getEntries({ sortBy: 'title', sortDirection: 'desc' }); + expect(entries.map(({ id }) => id)).to.eql([...ascendingOrder].reverse()); + }); + }); - expect(res.body.entries.length).to.eql(1); - expect(res.body.entries[0].id).to.eql('my_doc_a'); + it('allows searching by title', async () => { + const entries = await getEntries({ query: 'b' }); + expect(entries.length).to.eql(1); + expect(entries[0].title).to.eql('My title b'); }); }); }); } + +function omitCategories(entries: KnowledgeBaseEntry[]) { + return entries.filter((entry) => entry.labels?.category === undefined); +} diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts index 9099eff540d35..77f010d851f3c 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts @@ -17,7 +17,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns empty object when successful', async () => { await createKnowledgeBaseModel(ml); const res = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', }) .expect(200); @@ -27,7 +27,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns bad request if model cannot be installed', async () => { await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', }) .expect(400); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts index 4e9778630a535..6561c416f02cf 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts @@ -17,7 +17,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(async () => { await createKnowledgeBaseModel(ml); await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', }) .expect(200); @@ -29,7 +29,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns correct status after knowledge base is setup', async () => { const res = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/status', }) .expect(200); @@ -41,7 +41,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true); const res = await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/status', }) .expect(200); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts index a93c194c85daa..a9c1f245a1ac3 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { kbnTestConfig } from '@kbn/test'; import { sortBy } from 'lodash'; import { Message, MessageRole } from '@kbn/observability-ai-assistant-plugin/common'; import { CONTEXT_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/server/functions/context'; @@ -21,33 +20,27 @@ import { import { getConversationCreatedEvent } from '../conversations/helpers'; import { LlmProxy, createLlmProxy } from '../../common/create_llm_proxy'; import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors'; +import { User } from '../../common/users/users'; export default function ApiTest({ getService }: FtrProviderContext) { const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); - const getScopedApiClientForUsername = getService('getScopedApiClientForUsername'); - const security = getService('security'); const supertest = getService('supertest'); const es = getService('es'); const ml = getService('ml'); const log = getService('log'); + const getScopedApiClientForUsername = getService('getScopedApiClientForUsername'); describe('Knowledge base user instructions', () => { - const userJohn = 'john'; - before(async () => { - // create user - const password = kbnTestConfig.getUrlParts().password!; - await security.user.create(userJohn, { password, roles: ['editor'] }); await createKnowledgeBaseModel(ml); await observabilityAIAssistantAPIClient - .editorUser({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup' }) + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup' }) .expect(200); }); after(async () => { await deleteKnowledgeBaseModel(ml); - await security.user.delete(userJohn); await clearKnowledgeBase(es); await clearConversations(es); }); @@ -58,19 +51,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { const promises = [ { - username: 'editor', + username: 'editor' as const, isPublic: true, }, { - username: 'editor', + username: 'editor' as const, isPublic: false, }, { - username: userJohn, + username: 'secondary_editor' as const, isPublic: true, }, { - username: userJohn, + username: 'secondary_editor' as const, isPublic: false, }, ].map(async ({ username, isPublic }) => { @@ -92,61 +85,59 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('"editor" can retrieve their own private instructions and the public instruction', async () => { - const res = await observabilityAIAssistantAPIClient.editorUser({ + const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', }); const instructions = res.body.userInstructions; - const sortByDocId = (data: Array) => - sortBy(data, 'doc_id'); + const sortById = (data: Array) => sortBy(data, 'id'); - expect(sortByDocId(instructions)).to.eql( - sortByDocId([ + expect(sortById(instructions)).to.eql( + sortById([ { - doc_id: 'private-doc-from-editor', + id: 'private-doc-from-editor', public: false, text: 'Private user instruction from "editor"', }, { - doc_id: 'public-doc-from-editor', + id: 'public-doc-from-editor', public: true, text: 'Public user instruction from "editor"', }, { - doc_id: 'public-doc-from-john', + id: 'public-doc-from-secondary_editor', public: true, - text: 'Public user instruction from "john"', + text: 'Public user instruction from "secondary_editor"', }, ]) ); }); - it('"john" can retrieve their own private instructions and the public instruction', async () => { - const res = await getScopedApiClientForUsername(userJohn)({ + it('"secondaryEditor" can retrieve their own private instructions and the public instruction', async () => { + const res = await observabilityAIAssistantAPIClient.secondaryEditor({ endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', }); const instructions = res.body.userInstructions; - const sortByDocId = (data: Array) => - sortBy(data, 'doc_id'); + const sortById = (data: Array) => sortBy(data, 'id'); - expect(sortByDocId(instructions)).to.eql( - sortByDocId([ + expect(sortById(instructions)).to.eql( + sortById([ { - doc_id: 'public-doc-from-editor', + id: 'public-doc-from-editor', public: true, text: 'Public user instruction from "editor"', }, { - doc_id: 'public-doc-from-john', + id: 'public-doc-from-secondary_editor', public: true, - text: 'Public user instruction from "john"', + text: 'Public user instruction from "secondary_editor"', }, { - doc_id: 'private-doc-from-john', + id: 'private-doc-from-secondary_editor', public: false, - text: 'Private user instruction from "john"', + text: 'Private user instruction from "secondary_editor"', }, ]) ); @@ -158,7 +149,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { await clearKnowledgeBase(es); await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions', params: { body: { @@ -171,7 +162,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .expect(200); await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions', params: { body: { @@ -185,14 +176,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('updates the user instruction', async () => { - const res = await observabilityAIAssistantAPIClient.editorUser({ + const res = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', }); const instructions = res.body.userInstructions; expect(instructions).to.eql([ { - doc_id: 'doc-to-update', + id: 'doc-to-update', text: 'Updated text', public: false, }, @@ -207,12 +198,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const userInstructionText = 'Be polite and use language that is easy to understand. Never disagree with the user.'; - async function getConversationForUser(username: string) { + async function getConversationForUser(username: User['username']) { const apiClient = getScopedApiClientForUsername(username); // the user instruction is always created by "editor" user await observabilityAIAssistantAPIClient - .editorUser({ + .editor({ endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions', params: { body: { @@ -314,7 +305,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('does not add the instruction conversation for other users', async () => { - const conversation = await getConversationForUser('john'); + const conversation = await getConversationForUser('secondary_editor'); const systemMessage = conversation.messages.find( (message) => message.message.role === MessageRole.System )!; diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/public_complete/public_complete.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/public_complete/public_complete.spec.ts index a25a7cf5ef8eb..bb8984256f27c 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/public_complete/public_complete.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/public_complete/public_complete.spec.ts @@ -70,7 +70,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { (body) => !isFunctionTitleRequest(body) ); - const responsePromise = observabilityAIAssistantAPIClient.adminUser({ + const responsePromise = observabilityAIAssistantAPIClient.admin({ endpoint: 'POST /api/observability_ai_assistant/chat/complete 2023-10-31', params: { query: { format }, diff --git a/x-pack/test/observability_ai_assistant_functional/common/config.ts b/x-pack/test/observability_ai_assistant_functional/common/config.ts index c0f649d51d90d..99213e629e0e3 100644 --- a/x-pack/test/observability_ai_assistant_functional/common/config.ts +++ b/x-pack/test/observability_ai_assistant_functional/common/config.ts @@ -13,8 +13,9 @@ import { KibanaEBTUIProvider, } from '@kbn/test-suites-src/analytics/services/kibana_ebt'; import { - editorUser, - viewerUser, + secondaryEditor, + editor, + viewer, } from '../../observability_ai_assistant_api_integration/common/users/users'; import { ObservabilityAIAssistantFtrConfig, @@ -61,9 +62,10 @@ async function getTestConfig({ ObservabilityAIAssistantUIProvider(context), observabilityAIAssistantAPIClient: async (context: InheritedFtrProviderContext) => { return { - adminUser: await getScopedApiClient(kibanaServer, 'elastic'), - viewerUser: await getScopedApiClient(kibanaServer, viewerUser.username), - editorUser: await getScopedApiClient(kibanaServer, editorUser.username), + admin: getScopedApiClient(kibanaServer, 'elastic'), + viewer: getScopedApiClient(kibanaServer, viewer.username), + editor: getScopedApiClient(kibanaServer, editor.username), + secondaryEditor: getScopedApiClient(kibanaServer, secondaryEditor.username), }; }, kibana_ebt_server: KibanaEBTServerProvider, diff --git a/x-pack/test/observability_ai_assistant_functional/common/ui/index.ts b/x-pack/test/observability_ai_assistant_functional/common/ui/index.ts index 2c6852988cde5..d072cc3777a7d 100644 --- a/x-pack/test/observability_ai_assistant_functional/common/ui/index.ts +++ b/x-pack/test/observability_ai_assistant_functional/common/ui/index.ts @@ -27,6 +27,11 @@ export interface ObservabilityAIAssistantUIService { } const pages = { + kbManagementTab: { + table: 'knowledgeBaseTable', + tableTitleCell: 'knowledgeBaseTableTitleCell', + tableAuthorCell: 'knowledgeBaseTableAuthorCell', + }, conversations: { setupGenAiConnectorsButtonSelector: `observabilityAiAssistantInitialSetupPanelSetUpGenerativeAiConnectorButton`, chatInput: 'observabilityAiAssistantChatPromptEditorTextArea', diff --git a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts index 68b1d97d531dc..de780d2f46b0e 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts @@ -36,12 +36,12 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte const flyoutService = getService('flyout'); async function deleteConversations() { - const response = await observabilityAIAssistantAPIClient.editorUser({ + const response = await observabilityAIAssistantAPIClient.editor({ endpoint: 'POST /internal/observability_ai_assistant/conversations', }); for (const conversation of response.body.conversations) { - await observabilityAIAssistantAPIClient.editorUser({ + await observabilityAIAssistantAPIClient.editor({ endpoint: `DELETE /internal/observability_ai_assistant/conversation/{conversationId}`, params: { path: { @@ -53,7 +53,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte } async function deleteConnectors() { - const response = await observabilityAIAssistantAPIClient.editorUser({ + const response = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/connectors', }); @@ -66,7 +66,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte } async function createOldConversation() { - await observabilityAIAssistantAPIClient.editorUser({ + await observabilityAIAssistantAPIClient.editor({ endpoint: 'POST /internal/observability_ai_assistant/conversation', params: { body: { @@ -204,7 +204,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte }); it('creates a connector', async () => { - const response = await observabilityAIAssistantAPIClient.editorUser({ + const response = await observabilityAIAssistantAPIClient.editor({ endpoint: 'GET /internal/observability_ai_assistant/connectors', }); @@ -259,7 +259,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte }); it('creates a conversation and updates the URL', async () => { - const response = await observabilityAIAssistantAPIClient.editorUser({ + const response = await observabilityAIAssistantAPIClient.editor({ endpoint: 'POST /internal/observability_ai_assistant/conversations', }); @@ -325,7 +325,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte }); it('does not create another conversation', async () => { - const response = await observabilityAIAssistantAPIClient.editorUser({ + const response = await observabilityAIAssistantAPIClient.editor({ endpoint: 'POST /internal/observability_ai_assistant/conversations', }); @@ -333,7 +333,7 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte }); it('appends to the existing one', async () => { - const response = await observabilityAIAssistantAPIClient.editorUser({ + const response = await observabilityAIAssistantAPIClient.editor({ endpoint: 'POST /internal/observability_ai_assistant/conversations', }); diff --git a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts new file mode 100644 index 0000000000000..7a5a51ae58b6a --- /dev/null +++ b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { subj as testSubjSelector } from '@kbn/test-subj-selector'; +import { + clearKnowledgeBase, + createKnowledgeBaseModel, + deleteKnowledgeBaseModel, +} from '../../../observability_ai_assistant_api_integration/tests/knowledge_base/helpers'; +import { ObservabilityAIAssistantApiClient } from '../../../observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ApiTest({ getService, getPageObjects }: FtrProviderContext) { + const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); + const ui = getService('observabilityAIAssistantUI'); + const testSubjects = getService('testSubjects'); + const log = getService('log'); + const ml = getService('ml'); + const es = getService('es'); + const { common } = getPageObjects(['common']); + + async function saveKbEntry({ + apiClient, + text, + }: { + apiClient: ObservabilityAIAssistantApiClient; + text: string; + }) { + return apiClient({ + endpoint: 'POST /internal/observability_ai_assistant/functions/summarize', + params: { + body: { + title: 'Favourite color', + text, + confidence: 'high', + is_correction: false, + public: false, + labels: {}, + }, + }, + }).expect(200); + } + + describe('Knowledge management tab', () => { + before(async () => { + await clearKnowledgeBase(es); + + // create a knowledge base model + await createKnowledgeBaseModel(ml); + + await Promise.all([ + // setup the knowledge base + observabilityAIAssistantAPIClient + .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup' }) + .expect(200), + + // login as editor + ui.auth.login('editor'), + ]); + }); + + after(async () => { + await Promise.all([deleteKnowledgeBaseModel(ml), clearKnowledgeBase(es), ui.auth.logout()]); + }); + + describe('when the LLM calls the "summarize" function for two different users', () => { + async function getKnowledgeBaseEntries() { + await common.navigateToUrlWithBrowserHistory( + 'management', + '/kibana/observabilityAiAssistantManagement', + 'tab=knowledge_base' + ); + + const entryTitleCells = await testSubjects.findAll(ui.pages.kbManagementTab.tableTitleCell); + + const rows = await Promise.all( + entryTitleCells.map(async (cell) => { + const title = await cell.getVisibleText(); + const parentRow = await cell.findByXpath('ancestor::tr'); + + const authorElm = await parentRow.findByCssSelector( + testSubjSelector(ui.pages.kbManagementTab.tableAuthorCell) + ); + const author = await authorElm.getVisibleText(); + const rowText = (await parentRow.getVisibleText()).split('\n'); + + return { rowText, author, title }; + }) + ); + + log.debug(`Found ${rows.length} rows in the KB management table: ${JSON.stringify(rows)}`); + + return rows.filter(({ title }) => title === 'Favourite color'); + } + + before(async () => { + await saveKbEntry({ + apiClient: observabilityAIAssistantAPIClient.editor, + text: 'My favourite color is red', + }); + + await saveKbEntry({ + apiClient: observabilityAIAssistantAPIClient.secondaryEditor, + text: 'My favourite color is blue', + }); + }); + + it('shows two entries', async () => { + const entries = await getKnowledgeBaseEntries(); + expect(entries.length).to.eql(2); + }); + + it('shows two different authors', async () => { + const entries = await getKnowledgeBaseEntries(); + expect(entries.map(({ author }) => author)).to.eql(['secondary_editor', 'editor']); + }); + }); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 79bdd3ad7df09..091e0fe01e415 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -151,7 +151,6 @@ export default function ({ getService }: FtrProviderContext) { 'fleet:update_agent_tags:retry', 'fleet:upgrade_action:retry', 'logs-data-telemetry', - 'observabilityAIAssistant:indexQueuedDocumentsTaskType', 'osquery:telemetry-configs', 'osquery:telemetry-packs', 'osquery:telemetry-saved-queries', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base.spec.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base.spec.ts index b540ee5829e59..ce46939c365be 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base.spec.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base.spec.ts @@ -52,6 +52,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('when managing a single entry', () => { const knowledgeBaseEntry = { id: 'my-doc-id-1', + title: 'My title', text: 'My content', }; it('returns 200 on create', async () => { @@ -156,14 +157,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { const knowledgeBaseEntries = [ { id: 'my_doc_a', + title: 'My title a', text: 'My content a', }, { id: 'my_doc_b', + title: 'My title b', text: 'My content b', }, { id: 'my_doc_c', + title: 'My title c', text: 'My content c', }, ]; From a049cbc702b415bd7bce304edfb531d2b276c7e1 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:58:31 +1100 Subject: [PATCH 11/47] [8.x] [Observability Onboarding] fix docker integration double detection (#199237) (#199267) # Backport This will backport the following commits from `main` to `8.x`: - [[Observability Onboarding] fix docker integration double detection (#199237)](https://github.com/elastic/kibana/pull/199237) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Joe Reuter --- .../observability_onboarding/public/assets/auto_detect.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/assets/auto_detect.sh b/x-pack/plugins/observability_solution/observability_onboarding/public/assets/auto_detect.sh index c13b5cf031e0d..dd3077180d08e 100755 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/assets/auto_detect.sh +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/assets/auto_detect.sh @@ -329,6 +329,7 @@ read_open_log_file_list() { "^\/var\/log\/redis\/" "^\/var\/log\/rabbitmq\/" "^\/var\/log\/kafka\/" + "^\/var\/lib\/docker\/" "^\/var\/log\/mongodb\/" "^\/opt\/tomcat\/logs\/" "^\/var\/log\/prometheus\/" From ffaf529f29b9210e917f30296b95bcdf1e1a55f4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:22:27 +1100 Subject: [PATCH 12/47] [8.x] [ML] Migrate ColorRangeLegend from SCSS to emotion. (#199156) (#199274) # Backport This will backport the following commits from `main` to `8.x`: - [[ML] Migrate ColorRangeLegend from SCSS to emotion. (#199156)](https://github.com/elastic/kibana/pull/199156) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Walter Rafelsberger --- .../plugins/ml/public/application/_index.scss | 1 - .../_color_range_legend.scss | 18 ------------- .../components/color_range_legend/_index.scss | 1 - .../color_range_legend/color_range_legend.tsx | 27 +++++++++++++++++-- 4 files changed, 25 insertions(+), 22 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss delete mode 100644 x-pack/plugins/ml/public/application/components/color_range_legend/_index.scss diff --git a/x-pack/plugins/ml/public/application/_index.scss b/x-pack/plugins/ml/public/application/_index.scss index 3025b8f7d921b..95fbbf4cb112a 100644 --- a/x-pack/plugins/ml/public/application/_index.scss +++ b/x-pack/plugins/ml/public/application/_index.scss @@ -11,7 +11,6 @@ // Components @import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly @import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly - @import 'components/color_range_legend/index'; @import 'components/entity_cell/index'; @import 'components/influencers_list/index'; @import 'components/job_selector/index'; diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss b/x-pack/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss deleted file mode 100644 index b164e605a2488..0000000000000 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss +++ /dev/null @@ -1,18 +0,0 @@ -/* Overrides for d3/svg default styles */ -.mlColorRangeLegend { - text { - @include fontSize($euiFontSizeXS - 2px); - fill: $euiColorDarkShade; - } - - .axis path { - fill: none; - stroke: none; - } - - .axis line { - fill: none; - stroke: $euiColorMediumShade; - shape-rendering: crispEdges; - } -} diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/_index.scss b/x-pack/plugins/ml/public/application/components/color_range_legend/_index.scss deleted file mode 100644 index c7cd3faac0dcf..0000000000000 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'color_range_legend'; diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx b/x-pack/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx index f6a301f5eacce..9c121853cf6b4 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx @@ -7,12 +7,36 @@ import type { FC } from 'react'; import React, { useEffect, useRef } from 'react'; +import { css } from '@emotion/react'; import d3 from 'd3'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; + const COLOR_RANGE_RESOLUTION = 10; +// Overrides for d3/svg default styles +const cssOverride = css({ + // Override default font size and color for axis + text: { + fontSize: `calc(${euiThemeVars.euiFontSizeXS} - 2px)`, + fill: euiThemeVars.euiColorDarkShade, + }, + // Override default styles for axis lines + '.axis': { + path: { + fill: 'none', + stroke: 'none', + }, + line: { + fill: 'none', + stroke: euiThemeVars.euiColorMediumShade, + shapeRendering: 'crispEdges', + }, + }, +}); + interface ColorRangeLegendProps { colorRange: (d: number) => string; justifyTicks?: boolean; @@ -65,7 +89,6 @@ export const ColorRangeLegend: FC = ({ const wrapper = d3 .select(d3Container.current) - .classed('mlColorRangeLegend', true) .attr('width', wrapperWidth) .attr('height', wrapperHeight) .append('g') @@ -144,7 +167,7 @@ export const ColorRangeLegend: FC = ({ - + ); From 3265ee8e9ab7bf3b79c5eaad1e6b44fb68c91c34 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:54:59 +1100 Subject: [PATCH 13/47] [8.x] [Synthetics] Refactor bulk delete monitor and params routes !! (#195420) (#199277) # Backport This will backport the following commits from `main` to `8.x`: - [[Synthetics] Refactor bulk delete monitor and params routes !! (#195420)](https://github.com/elastic/kibana/pull/195420) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Shahzad --- .../monitors/delete-monitor-api.asciidoc | 10 +-- .../synthetics/params/delete-param.asciidoc | 44 +++++----- .../monitor_management/synthetics_params.ts | 2 + .../settings/global_params/delete_param.tsx | 73 +++------------- .../settings/global_params/params_list.tsx | 13 ++- .../synthetics/state/global_params/actions.ts | 5 ++ .../synthetics/state/global_params/api.ts | 17 ++-- .../synthetics/state/global_params/effects.ts | 32 ++++++- .../synthetics/state/global_params/index.ts | 19 ++++- .../apps/synthetics/state/monitor_list/api.ts | 9 +- .../apps/synthetics/state/root_effect.ts | 8 +- .../synthetics/server/routes/index.ts | 4 + .../bulk_cruds/add_monitor_bulk.ts | 6 +- .../bulk_cruds/delete_monitor_bulk.ts | 85 +++++++------------ .../routes/monitor_cruds/delete_monitor.ts | 36 +++++--- .../monitor_cruds/delete_monitor_project.ts | 7 +- .../services/delete_monitor_api.ts | 59 +++++++++++-- .../routes/settings/params/delete_param.ts | 41 +++++++-- .../settings/params/delete_params_bulk.ts | 39 +++++++++ .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../apis/synthetics/add_edit_params.ts | 40 +++++++++ .../apis/synthetics/delete_monitor.ts | 27 ++++++ .../synthetics_monitor_test_service.ts | 13 +++ .../apis/synthetics/sync_global_params.ts | 5 +- 26 files changed, 403 insertions(+), 197 deletions(-) create mode 100644 x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_params_bulk.ts diff --git a/docs/api/synthetics/monitors/delete-monitor-api.asciidoc b/docs/api/synthetics/monitors/delete-monitor-api.asciidoc index 70861fcd60a36..74798b40830b7 100644 --- a/docs/api/synthetics/monitors/delete-monitor-api.asciidoc +++ b/docs/api/synthetics/monitors/delete-monitor-api.asciidoc @@ -17,9 +17,6 @@ Deletes one or more monitors from the Synthetics app. You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the <>. -You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the -<>. - [[delete-monitor-api-path-params]] === {api-path-parms-title} @@ -27,7 +24,6 @@ You must have `all` privileges for the *Synthetics* feature in the *{observabili `config_id`:: (Required, string) The ID of the monitor that you want to delete. - Here is an example of a DELETE request to delete a monitor by ID: [source,sh] @@ -37,7 +33,7 @@ DELETE /api/synthetics/monitors/monitor1-id ==== Bulk Delete Monitors -You can delete multiple monitors by sending a list of config ids to a DELETE request to the `/api/synthetics/monitors` endpoint. +You can delete multiple monitors by sending a list of config ids to a POST request to the `/api/synthetics/monitors/_bulk_delete` endpoint. [[monitors-delete-request-body]] @@ -49,11 +45,11 @@ The request body should contain an array of monitors IDs that you want to delete (Required, array of strings) An array of monitor IDs to delete. -Here is an example of a DELETE request to delete a list of monitors by ID: +Here is an example of a POST request to delete a list of monitors by ID: [source,sh] -------------------------------------------------- -DELETE /api/synthetics/monitors +POST /api/synthetics/monitors/_bulk_delete { "ids": [ "monitor1-id", diff --git a/docs/api/synthetics/params/delete-param.asciidoc b/docs/api/synthetics/params/delete-param.asciidoc index 4c7d7911ec180..031a47501a8a8 100644 --- a/docs/api/synthetics/params/delete-param.asciidoc +++ b/docs/api/synthetics/params/delete-param.asciidoc @@ -8,9 +8,9 @@ Deletes one or more parameters from the Synthetics app. === {api-request-title} -`DELETE :/api/synthetics/params` +`DELETE :/api/synthetics/params/` -`DELETE :/s//api/synthetics/params` +`DELETE :/s//api/synthetics/params/` === {api-prereq-title} @@ -20,26 +20,19 @@ You must have `all` privileges for the *Synthetics* feature in the *{observabili You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the <>. -[[parameters-delete-request-body]] -==== Request Body +[[parameters-delete-path-param]] +==== Path Parameters The request body should contain an array of parameter IDs that you want to delete. -`ids`:: -(Required, array of strings) An array of parameter IDs to delete. +`param_id`:: +(Required, string) An id of parameter to delete. - -Here is an example of a DELETE request to delete a list of parameters by ID: +Here is an example of a DELETE request to delete a parameter by its ID: [source,sh] -------------------------------------------------- -DELETE /api/synthetics/params -{ - "ids": [ - "param1-id", - "param2-id" - ] -} +DELETE /api/synthetics/params/param_id1 -------------------------------------------------- [[parameters-delete-response-example]] @@ -58,10 +51,21 @@ Here's an example response for deleting multiple parameters: { "id": "param1-id", "deleted": true - }, - { - "id": "param2-id", - "deleted": true } ] --------------------------------------------------- \ No newline at end of file +-------------------------------------------------- + +==== Bulk delete parameters +To delete multiple parameters, you can send a POST request to `/api/synthetics/params/_bulk_delete` with an array of parameter IDs to delete via body. + +Here is an example of a POST request to delete multiple parameters: + +[source,sh] +-------------------------------------------------- +POST /api/synthetics/params/_bulk_delete +{ + "ids": ["param1-id", "param2-id"] +} +-------------------------------------------------- + + diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/synthetics_params.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/synthetics_params.ts index 5393fed135b7d..4107bc1efbb2a 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/synthetics_params.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/synthetics_params.ts @@ -19,6 +19,8 @@ export const SyntheticsParamsReadonlyCodec = t.intersection([ }), ]); +export const SyntheticsParamsReadonlyCodecList = t.array(SyntheticsParamsReadonlyCodec); + export type SyntheticsParamsReadonly = t.TypeOf; export const SyntheticsParamsCodec = t.intersection([ diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx index 814fb13a99ba9..2615a65ef289c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx @@ -5,16 +5,17 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { EuiConfirmModal } from '@elastic/eui'; -import { FETCH_STATUS, useFetcher } from '@kbn/observability-shared-plugin/public'; -import { toMountPoint } from '@kbn/react-kibana-mount'; import { i18n } from '@kbn/i18n'; -import { useDispatch } from 'react-redux'; -import { getGlobalParamAction, deleteGlobalParams } from '../../../state/global_params'; +import { useDispatch, useSelector } from 'react-redux'; +import { + getGlobalParamAction, + deleteGlobalParamsAction, + selectGlobalParamState, +} from '../../../state/global_params'; import { syncGlobalParamsAction } from '../../../state/settings'; -import { kibanaService } from '../../../../../utils/kibana_service'; import { NO_LABEL, YES_LABEL } from '../../monitors_page/management/monitor_list_table/labels'; import { ListParamItem } from './params_list'; @@ -25,19 +26,8 @@ export const DeleteParam = ({ items: ListParamItem[]; setIsDeleteModalVisible: React.Dispatch>; }) => { - const [isDeleting, setIsDeleting] = useState(false); - const dispatch = useDispatch(); - - const handleConfirmDelete = () => { - setIsDeleting(true); - }; - - const { status } = useFetcher(() => { - if (isDeleting) { - return deleteGlobalParams(items.map(({ id }) => id)); - } - }, [items, isDeleting]); + const { isDeleting, listOfParams } = useSelector(selectGlobalParamState); const name = items .map(({ key }) => key) @@ -45,51 +35,12 @@ export const DeleteParam = ({ .slice(0, 50); useEffect(() => { - if (!isDeleting) { - return; - } - const { coreStart, toasts } = kibanaService; - - if (status === FETCH_STATUS.FAILURE) { - toasts.addDanger( - { - title: toMountPoint( -

- {' '} - {i18n.translate('xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name', { - defaultMessage: 'Param {name} failed to delete.', - values: { name }, - })} -

, - coreStart - ), - }, - { toastLifeTimeMs: 3000 } - ); - } else if (status === FETCH_STATUS.SUCCESS) { - toasts.addSuccess( - { - title: toMountPoint( -

- {i18n.translate('xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name', { - defaultMessage: 'Param {name} deleted successfully.', - values: { name }, - })} -

, - coreStart - ), - }, - { toastLifeTimeMs: 3000 } - ); - dispatch(syncGlobalParamsAction.get()); - } - if (status === FETCH_STATUS.SUCCESS || status === FETCH_STATUS.FAILURE) { - setIsDeleting(false); + if (!isDeleting && (listOfParams ?? []).length === 0) { setIsDeleteModalVisible(false); dispatch(getGlobalParamAction.get()); dispatch(syncGlobalParamsAction.get()); } - }, [setIsDeleting, isDeleting, status, setIsDeleteModalVisible, name, dispatch]); + }, [isDeleting, setIsDeleteModalVisible, name, dispatch, listOfParams]); return ( setIsDeleteModalVisible(false)} - onConfirm={handleConfirmDelete} + onConfirm={() => { + dispatch(deleteGlobalParamsAction.get(items.map(({ id }) => id))); + }} cancelButtonText={NO_LABEL} confirmButtonText={YES_LABEL} buttonColor="danger" diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx index 2ff3ea547ae9f..b16dbcd686d91 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx @@ -83,7 +83,11 @@ export const ParamsList = () => { render: (val: string[]) => { const tags = val ?? []; if (tags.length === 0) { - return --; + return ( + + {i18n.translate('xpack.synthetics.columns.TextLabel', { defaultMessage: '--' })} + + ); } return ( @@ -105,7 +109,11 @@ export const ParamsList = () => { render: (val: string[]) => { const namespaces = val ?? []; if (namespaces.length === 0) { - return --; + return ( + + {i18n.translate('xpack.synthetics.columns.TextLabel', { defaultMessage: '--' })} + + ); } return ( @@ -184,6 +192,7 @@ export const ParamsList = () => { isEditingItem={isEditingItem} setIsEditingItem={setIsEditingItem} items={items} + key="add-param-flyout" />, ]; }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/actions.ts index b1388bc2674b9..0faef0079657a 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/actions.ts @@ -23,3 +23,8 @@ export const editGlobalParamAction = createAsyncAction< }, SyntheticsParams >('EDIT GLOBAL PARAM'); + +export const deleteGlobalParamsAction = createAsyncAction< + string[], + Array<{ id: string; deleted: boolean }> +>('DELETE GLOBAL PARAMS'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/api.ts index 33eb4622bf6c5..1badb74dff26f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/api.ts @@ -13,6 +13,7 @@ import { SyntheticsParams, SyntheticsParamsCodec, SyntheticsParamsReadonlyCodec, + SyntheticsParamsReadonlyCodecList, } from '../../../../../common/runtime_types'; import { apiService } from '../../../../utils/api_service/api_service'; @@ -20,14 +21,14 @@ export const getGlobalParams = async (): Promise => { return apiService.get( SYNTHETICS_API_URLS.PARAMS, { version: INITIAL_REST_VERSION }, - SyntheticsParamsReadonlyCodec + SyntheticsParamsReadonlyCodecList ); }; export const addGlobalParam = async ( paramRequest: SyntheticsParamRequest ): Promise => - apiService.post(SYNTHETICS_API_URLS.PARAMS, paramRequest, SyntheticsParamsCodec, { + apiService.post(SYNTHETICS_API_URLS.PARAMS, paramRequest, SyntheticsParamsReadonlyCodec, { version: INITIAL_REST_VERSION, }); @@ -53,11 +54,13 @@ export const editGlobalParam = async ({ ); }; -export const deleteGlobalParams = async (ids: string[]): Promise => - apiService.delete( - SYNTHETICS_API_URLS.PARAMS, - { version: INITIAL_REST_VERSION }, +export const deleteGlobalParams = async (ids: string[]): Promise => { + return await apiService.post( + SYNTHETICS_API_URLS.PARAMS + '/_bulk_delete', { ids, - } + }, + null, + { version: INITIAL_REST_VERSION } ); +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/effects.ts index d5249fcfc4519..f5f0c6e4ee951 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/effects.ts @@ -8,8 +8,13 @@ import { takeLeading } from 'redux-saga/effects'; import { i18n } from '@kbn/i18n'; import { fetchEffectFactory } from '../utils/fetch_effect'; -import { addGlobalParam, editGlobalParam, getGlobalParams } from './api'; -import { addNewGlobalParamAction, editGlobalParamAction, getGlobalParamAction } from './actions'; +import { addGlobalParam, deleteGlobalParams, editGlobalParam, getGlobalParams } from './api'; +import { + addNewGlobalParamAction, + deleteGlobalParamsAction, + editGlobalParamAction, + getGlobalParamAction, +} from './actions'; export function* getGlobalParamEffect() { yield takeLeading( @@ -69,3 +74,26 @@ const editSuccessMessage = i18n.translate('xpack.synthetics.settings.editParams. const editFailureMessage = i18n.translate('xpack.synthetics.settings.editParams.fail', { defaultMessage: 'Failed to edit global parameter.', }); + +// deleteGlobalParams + +export function* deleteGlobalParamsEffect() { + yield takeLeading( + deleteGlobalParamsAction.get, + fetchEffectFactory( + deleteGlobalParams, + deleteGlobalParamsAction.success, + deleteGlobalParamsAction.fail, + deleteSuccessMessage, + deleteFailureMessage + ) + ); +} + +const deleteSuccessMessage = i18n.translate('xpack.synthetics.settings.deleteParams.success', { + defaultMessage: 'Successfully deleted global parameters.', +}); + +const deleteFailureMessage = i18n.translate('xpack.synthetics.settings.deleteParams.fail', { + defaultMessage: 'Failed to delete global parameters.', +}); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/index.ts index 89b3a0b7e1904..a1e2e07ff955f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/index.ts @@ -8,7 +8,12 @@ import { createReducer } from '@reduxjs/toolkit'; import { SyntheticsParams } from '../../../../../common/runtime_types'; import { IHttpSerializedFetchError } from '..'; -import { addNewGlobalParamAction, editGlobalParamAction, getGlobalParamAction } from './actions'; +import { + addNewGlobalParamAction, + deleteGlobalParamsAction, + editGlobalParamAction, + getGlobalParamAction, +} from './actions'; export interface GlobalParamsState { isLoading?: boolean; @@ -16,6 +21,7 @@ export interface GlobalParamsState { addError: IHttpSerializedFetchError | null; editError: IHttpSerializedFetchError | null; isSaving?: boolean; + isDeleting?: boolean; savedData?: SyntheticsParams; } @@ -23,6 +29,7 @@ const initialState: GlobalParamsState = { isLoading: false, addError: null, isSaving: false, + isDeleting: false, editError: null, listOfParams: [], }; @@ -62,6 +69,16 @@ export const globalParamsReducer = createReducer(initialState, (builder) => { .addCase(editGlobalParamAction.fail, (state, action) => { state.isSaving = false; state.editError = action.payload; + }) + .addCase(deleteGlobalParamsAction.get, (state) => { + state.isDeleting = true; + }) + .addCase(deleteGlobalParamsAction.success, (state) => { + state.isDeleting = false; + state.listOfParams = []; + }) + .addCase(deleteGlobalParamsAction.fail, (state) => { + state.isDeleting = false; }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts index 344897dd0eb1d..bef569bf0da39 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts @@ -60,12 +60,13 @@ export const fetchDeleteMonitor = async ({ }): Promise => { const baseUrl = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS; - return await apiService.delete( - baseUrl, - { version: INITIAL_REST_VERSION, spaceId }, + return await apiService.post( + baseUrl + '/_bulk_delete', { ids: configIds, - } + }, + undefined, + { version: INITIAL_REST_VERSION, spaceId } ); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts index 1a565fe772aa6..e38a1b5ad918f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts @@ -7,7 +7,12 @@ import { all, fork } from 'redux-saga/effects'; import { getCertsListEffect } from './certs'; -import { addGlobalParamEffect, editGlobalParamEffect, getGlobalParamEffect } from './global_params'; +import { + addGlobalParamEffect, + deleteGlobalParamsEffect, + editGlobalParamEffect, + getGlobalParamEffect, +} from './global_params'; import { fetchManualTestRunsEffect } from './manual_test_runs/effects'; import { enableDefaultAlertingEffect, @@ -66,6 +71,7 @@ export const rootEffect = function* root(): Generator { fork(fetchManualTestRunsEffect), fork(addGlobalParamEffect), fork(editGlobalParamEffect), + fork(deleteGlobalParamsEffect), fork(getGlobalParamEffect), fork(getCertsListEffect), fork(getDefaultAlertingEffect), diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts index 48e0de1c7fba4..f9d178befeb46 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { deleteSyntheticsParamsBulkRoute } from './settings/params/delete_params_bulk'; +import { deleteSyntheticsMonitorBulkRoute } from './monitor_cruds/bulk_cruds/delete_monitor_bulk'; import { createGetDynamicSettingsRoute, createPostDynamicSettingsRoute, @@ -113,4 +115,6 @@ export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] = addSyntheticsMonitorRoute, editSyntheticsMonitorRoute, deleteSyntheticsMonitorRoute, + deleteSyntheticsMonitorBulkRoute, + deleteSyntheticsParamsBulkRoute, ]; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts index 2ecbbf83d471c..03c7ede49ceba 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts @@ -10,7 +10,6 @@ import { SavedObjectsBulkResponse } from '@kbn/core-saved-objects-api-server'; import { v4 as uuidV4 } from 'uuid'; import { NewPackagePolicy } from '@kbn/fleet-plugin/common'; import { SavedObjectError } from '@kbn/core-saved-objects-common'; -import { deleteMonitorBulk } from './delete_monitor_bulk'; import { SyntheticsServerSetup } from '../../../types'; import { RouteContext } from '../../types'; import { formatTelemetryEvent, sendTelemetryEvents } from '../../telemetry/monitor_upgrade_sender'; @@ -190,9 +189,10 @@ export const deleteMonitorIfCreated = async ({ newMonitorId ); if (encryptedMonitor) { - await deleteMonitorBulk({ + const deleteMonitorAPI = new DeleteMonitorAPI(routeContext); + + await deleteMonitorAPI.deleteMonitorBulk({ monitors: [encryptedMonitor], - routeContext, }); } } catch (e) { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts index 9a031b3e7111a..ba6426de740d3 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts @@ -4,63 +4,40 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { SavedObject } from '@kbn/core-saved-objects-server'; -import { - formatTelemetryDeleteEvent, - sendTelemetryEvents, -} from '../../telemetry/monitor_upgrade_sender'; -import { - ConfigKey, - MonitorFields, - SyntheticsMonitor, - EncryptedSyntheticsMonitorAttributes, - SyntheticsMonitorWithId, -} from '../../../../common/runtime_types'; -import { syntheticsMonitorType } from '../../../../common/types/saved_objects'; -import { RouteContext } from '../../types'; -export const deleteMonitorBulk = async ({ - monitors, - routeContext, -}: { - monitors: Array>; - routeContext: RouteContext; -}) => { - const { savedObjectsClient, server, spaceId, syntheticsMonitorClient } = routeContext; - const { logger, telemetry, stackVersion } = server; +import { schema } from '@kbn/config-schema'; +import { DeleteMonitorAPI } from '../services/delete_monitor_api'; +import { SYNTHETICS_API_URLS } from '../../../../common/constants'; +import { SyntheticsRestApiRouteFactory } from '../../types'; - try { - const deleteSyncPromise = syntheticsMonitorClient.deleteMonitors( - monitors.map((normalizedMonitor) => ({ - ...normalizedMonitor.attributes, - id: normalizedMonitor.attributes[ConfigKey.MONITOR_QUERY_ID], - })) as SyntheticsMonitorWithId[], - savedObjectsClient, - spaceId - ); +export const deleteSyntheticsMonitorBulkRoute: SyntheticsRestApiRouteFactory< + Array<{ id: string; deleted: boolean }>, + Record, + Record, + { ids: string[] } +> = () => ({ + method: 'POST', + path: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/_bulk_delete', + validate: {}, + validation: { + request: { + body: schema.object({ + ids: schema.arrayOf(schema.string(), { + minSize: 1, + }), + }), + }, + }, + handler: async (routeContext): Promise => { + const { request } = routeContext; - const deletePromises = savedObjectsClient.bulkDelete( - monitors.map((monitor) => ({ type: syntheticsMonitorType, id: monitor.id })) - ); + const { ids: idsToDelete } = request.body || {}; + const deleteMonitorAPI = new DeleteMonitorAPI(routeContext); - const [errors, result] = await Promise.all([deleteSyncPromise, deletePromises]); - - monitors.forEach((monitor) => { - sendTelemetryEvents( - logger, - telemetry, - formatTelemetryDeleteEvent( - monitor, - stackVersion, - new Date().toISOString(), - Boolean((monitor.attributes as MonitorFields)[ConfigKey.SOURCE_INLINE]), - errors - ) - ); + const { errors, result } = await deleteMonitorAPI.execute({ + monitorIds: idsToDelete, }); - return { errors, result }; - } catch (e) { - throw e; - } -}; + return { result, errors }; + }, +}); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor.ts index f40f06f66b1ff..b989d16e4f194 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { DeleteMonitorAPI } from './services/delete_monitor_api'; import { SyntheticsRestApiRouteFactory } from '../types'; @@ -41,30 +42,39 @@ export const deleteSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory< if (ids && queryId) { return response.badRequest({ - body: { message: 'id must be provided either via param or body.' }, + body: { + message: i18n.translate('xpack.synthetics.deleteMonitor.errorMultipleIdsProvided', { + defaultMessage: 'id must be provided either via param or body.', + }), + }, }); } const idsToDelete = [...(ids ?? []), ...(queryId ? [queryId] : [])]; if (idsToDelete.length === 0) { return response.badRequest({ - body: { message: 'id must be provided via param or body.' }, + body: { + message: i18n.translate('xpack.synthetics.deleteMonitor.errorMultipleIdsProvided', { + defaultMessage: 'id must be provided either via param or body.', + }), + }, }); } const deleteMonitorAPI = new DeleteMonitorAPI(routeContext); - try { - const { errors } = await deleteMonitorAPI.execute({ - monitorIds: idsToDelete, - }); + const { errors } = await deleteMonitorAPI.execute({ + monitorIds: idsToDelete, + }); - if (errors && errors.length > 0) { - return response.ok({ - body: { message: 'error pushing monitor to the service', attributes: { errors } }, - }); - } - } catch (getErr) { - throw getErr; + if (errors && errors.length > 0) { + return response.ok({ + body: { + message: i18n.translate('xpack.synthetics.deleteMonitor.errorPushingMonitorToService', { + defaultMessage: 'Error pushing monitor to the service', + }), + attributes: { errors }, + }, + }); } return deleteMonitorAPI.result; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor_project.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor_project.ts index 7b36780937694..a56f66842a703 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor_project.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor_project.ts @@ -6,12 +6,12 @@ */ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { DeleteMonitorAPI } from './services/delete_monitor_api'; import { SyntheticsRestApiRouteFactory } from '../types'; import { syntheticsMonitorType } from '../../../common/types/saved_objects'; import { ConfigKey } from '../../../common/runtime_types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { getMonitors, getSavedObjectKqlFilter } from '../common'; -import { deleteMonitorBulk } from './bulk_cruds/delete_monitor_bulk'; import { validateSpaceId } from './services/validate_space_id'; export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory = () => ({ @@ -58,9 +58,10 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory { fields: [] } ); - await deleteMonitorBulk({ + const deleteMonitorAPI = new DeleteMonitorAPI(routeContext); + + await deleteMonitorAPI.deleteMonitorBulk({ monitors, - routeContext, }); return { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts index bd162fc043592..4fc527f930832 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts @@ -7,16 +7,22 @@ import pMap from 'p-map'; import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; -import { deleteMonitorBulk } from '../bulk_cruds/delete_monitor_bulk'; import { validatePermissions } from '../edit_monitor'; import { + ConfigKey, EncryptedSyntheticsMonitorAttributes, + MonitorFields, SyntheticsMonitor, + SyntheticsMonitorWithId, SyntheticsMonitorWithSecretsAttributes, } from '../../../../common/runtime_types'; import { syntheticsMonitorType } from '../../../../common/types/saved_objects'; import { normalizeSecrets } from '../../../synthetics_service/utils'; -import { sendErrorTelemetryEvents } from '../../telemetry/monitor_upgrade_sender'; +import { + formatTelemetryDeleteEvent, + sendErrorTelemetryEvents, + sendTelemetryEvents, +} from '../../telemetry/monitor_upgrade_sender'; import { RouteContext } from '../../types'; export class DeleteMonitorAPI { @@ -100,9 +106,8 @@ export class DeleteMonitorAPI { } try { - const { errors, result } = await deleteMonitorBulk({ + const { errors, result } = await this.deleteMonitorBulk({ monitors, - routeContext: this.routeContext, }); result.statuses?.forEach((res) => { @@ -112,11 +117,55 @@ export class DeleteMonitorAPI { }); }); - return { errors }; + return { errors, result: this.result }; } catch (e) { server.logger.error(`Unable to delete Synthetics monitor with error ${e.message}`); server.logger.error(e); throw e; } } + + async deleteMonitorBulk({ + monitors, + }: { + monitors: Array>; + }) { + const { savedObjectsClient, server, spaceId, syntheticsMonitorClient } = this.routeContext; + const { logger, telemetry, stackVersion } = server; + + try { + const deleteSyncPromise = syntheticsMonitorClient.deleteMonitors( + monitors.map((normalizedMonitor) => ({ + ...normalizedMonitor.attributes, + id: normalizedMonitor.attributes[ConfigKey.MONITOR_QUERY_ID], + })) as SyntheticsMonitorWithId[], + savedObjectsClient, + spaceId + ); + + const deletePromises = savedObjectsClient.bulkDelete( + monitors.map((monitor) => ({ type: syntheticsMonitorType, id: monitor.id })) + ); + + const [errors, result] = await Promise.all([deleteSyncPromise, deletePromises]); + + monitors.forEach((monitor) => { + sendTelemetryEvents( + logger, + telemetry, + formatTelemetryDeleteEvent( + monitor, + stackVersion, + new Date().toISOString(), + Boolean((monitor.attributes as MonitorFields)[ConfigKey.SOURCE_INLINE]), + errors + ) + ); + }); + + return { errors, result }; + } catch (e) { + throw e; + } + } } diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_param.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_param.ts index 78d24d9452ae9..1a504b263861b 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_param.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_param.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { syntheticsParamType } from '../../../../common/types/saved_objects'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; @@ -13,25 +14,51 @@ import { DeleteParamsResponse } from '../../../../common/runtime_types'; export const deleteSyntheticsParamsRoute: SyntheticsRestApiRouteFactory< DeleteParamsResponse[], - unknown, + { id?: string }, unknown, { ids: string[] } > = () => ({ method: 'DELETE', - path: SYNTHETICS_API_URLS.PARAMS, + path: SYNTHETICS_API_URLS.PARAMS + '/{id?}', validate: {}, validation: { request: { - body: schema.object({ - ids: schema.arrayOf(schema.string()), + body: schema.nullable( + schema.object({ + ids: schema.arrayOf(schema.string(), { + minSize: 1, + }), + }) + ), + params: schema.object({ + id: schema.maybe(schema.string()), }), }, }, - handler: async ({ savedObjectsClient, request }) => { - const { ids } = request.body; + handler: async ({ savedObjectsClient, request, response }) => { + const { ids } = request.body ?? {}; + const { id: paramId } = request.params ?? {}; + + if (ids && paramId) { + return response.badRequest({ + body: i18n.translate('xpack.synthetics.deleteParam.errorMultipleIdsProvided', { + defaultMessage: `Both param id and body parameters cannot be provided`, + }), + }); + } + + const idsToDelete = ids ?? [paramId]; + + if (idsToDelete.length === 0) { + return response.badRequest({ + body: i18n.translate('xpack.synthetics.deleteParam.errorNoIdsProvided', { + defaultMessage: `No param ids provided`, + }), + }); + } const result = await savedObjectsClient.bulkDelete( - ids.map((id) => ({ type: syntheticsParamType, id })), + idsToDelete.map((id) => ({ type: syntheticsParamType, id })), { force: true } ); return result.statuses.map(({ id, success }) => ({ id, deleted: success })); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_params_bulk.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_params_bulk.ts new file mode 100644 index 0000000000000..2cafaf0a1af99 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_params_bulk.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { SyntheticsRestApiRouteFactory } from '../../types'; +import { syntheticsParamType } from '../../../../common/types/saved_objects'; +import { SYNTHETICS_API_URLS } from '../../../../common/constants'; +import { DeleteParamsResponse } from '../../../../common/runtime_types'; + +export const deleteSyntheticsParamsBulkRoute: SyntheticsRestApiRouteFactory< + DeleteParamsResponse[], + unknown, + unknown, + { ids: string[] } +> = () => ({ + method: 'POST', + path: SYNTHETICS_API_URLS.PARAMS + '/_bulk_delete', + validate: {}, + validation: { + request: { + body: schema.object({ + ids: schema.arrayOf(schema.string()), + }), + }, + }, + handler: async ({ savedObjectsClient, request }) => { + const { ids } = request.body; + + const result = await savedObjectsClient.bulkDelete( + ids.map((id) => ({ type: syntheticsParamType, id })), + { force: true } + ); + return result.statuses.map(({ id, success }) => ({ id, deleted: success })); + }, +}); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 7a41f7703eb2a..d7dfcc8ba4072 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -44373,8 +44373,6 @@ "xpack.synthetics.paramForm.namespaces": "Espaces de noms", "xpack.synthetics.paramForm.sharedAcrossSpacesLabel": "Partager entre les espaces", "xpack.synthetics.paramManagement.deleteParamNameLabel": "Supprimer le paramètre \"{name}\" ?", - "xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name": "Impossible de supprimer le paramètre {name}.", - "xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name": "Paramètre {name} supprimé avec succès.", "xpack.synthetics.params.description": "Définissez les variables et paramètres que vous pouvez utiliser dans la configuration du navigateur et des moniteurs légers, tels que des informations d'identification ou des URL. {learnMore}", "xpack.synthetics.params.unprivileged.unprivilegedDescription": "Vous devez disposer de privilèges supplémentaires pour voir les paramètres d'utilisation et de conservation des données des applications Synthetics. {docsLink}", "xpack.synthetics.pingList.collapseRow": "Réduire", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ccdc4069314e8..656e6f844f0c3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -44111,8 +44111,6 @@ "xpack.synthetics.paramForm.namespaces": "名前空間", "xpack.synthetics.paramForm.sharedAcrossSpacesLabel": "複数のスペース間で共有", "xpack.synthetics.paramManagement.deleteParamNameLabel": "\"{name}\"パラメーターを削除しますか?", - "xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name": "パラメーター{name}の削除に失敗しました。", - "xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name": "パラメーター\"{name}\"が正常に削除されました。", "xpack.synthetics.params.description": "ブラウザーや軽量モニターの設定に使用できる変数やパラメーター(認証情報やURLなど)を定義します。{learnMore}", "xpack.synthetics.params.unprivileged.unprivilegedDescription": "Syntheticsアプリデータの使用状況と保持設定を表示する追加の権限が必要です。{docsLink}", "xpack.synthetics.pingList.collapseRow": "縮小", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 341eec24125a2..d3341c98103cb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -44162,8 +44162,6 @@ "xpack.synthetics.paramForm.namespaces": "命名空间", "xpack.synthetics.paramForm.sharedAcrossSpacesLabel": "跨工作区共享", "xpack.synthetics.paramManagement.deleteParamNameLabel": "删除“{name}”参数?", - "xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name": "无法删除参数 {name}。", - "xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name": "已成功删除参数 {name}。", "xpack.synthetics.params.description": "定义可在浏览器和轻量级监测的配置中使用的变量和参数,如凭据或 URL。{learnMore}", "xpack.synthetics.params.unprivileged.unprivilegedDescription": "您需要其他权限才能查看 Synthetics 应用数据使用情况和保留设置。{docsLink}", "xpack.synthetics.pingList.collapseRow": "折叠", diff --git a/x-pack/test/api_integration/apis/synthetics/add_edit_params.ts b/x-pack/test/api_integration/apis/synthetics/add_edit_params.ts index 7b27aaa621f46..0aae85864bf16 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_edit_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_edit_params.ts @@ -353,5 +353,45 @@ export default function ({ getService }: FtrProviderContext) { expect(param.key).to.not.empty(); }); }); + + it('should handle bulk deleting params', async () => { + await kServer.savedObjects.clean({ types: [syntheticsParamType] }); + + const params = [ + { key: 'param1', value: 'value1' }, + { key: 'param2', value: 'value2' }, + { key: 'param3', value: 'value3' }, + ]; + + for (const param of params) { + await supertestAPI + .post(SYNTHETICS_API_URLS.PARAMS) + .set('kbn-xsrf', 'true') + .send(param) + .expect(200); + } + + const getResponse = await supertestAPI + .get(SYNTHETICS_API_URLS.PARAMS) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(getResponse.body.length).to.eql(3); + + const ids = getResponse.body.map((param: any) => param.id); + + await supertestAPI + .post(SYNTHETICS_API_URLS.PARAMS + '/_bulk_delete') + .set('kbn-xsrf', 'true') + .send({ ids }) + .expect(200); + + const getResponseAfterDelete = await supertestAPI + .get(SYNTHETICS_API_URLS.PARAMS) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(getResponseAfterDelete.body.length).to.eql(0); + }); }); } diff --git a/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts b/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts index c96175d2982b3..f8781295e8005 100644 --- a/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts @@ -121,6 +121,33 @@ export default function ({ getService }: FtrProviderContext) { await supertest.get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId).expect(404); }); + it('deletes multiple monitors by bulk delete', async () => { + const { id: monitorId } = await saveMonitor(httpMonitorJson as MonitorFields); + const { id: monitorId2 } = await saveMonitor({ + ...httpMonitorJson, + name: 'another -2', + } as MonitorFields); + + const deleteResponse = await monitorTestService.deleteMonitorBulk( + [monitorId2, monitorId], + 200 + ); + + expect( + deleteResponse.body.result.sort((a: { id: string }, b: { id: string }) => + a.id > b.id ? 1 : -1 + ) + ).eql( + [ + { id: monitorId2, deleted: true }, + { id: monitorId, deleted: true }, + ].sort((a, b) => (a.id > b.id ? 1 : -1)) + ); + + // Hit get endpoint and expect 404 as well + await supertest.get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId).expect(404); + }); + it('returns 404 if monitor id is not found', async () => { const invalidMonitorId = 'invalid-id'; const expected404Message = `Monitor id ${invalidMonitorId} not found!`; diff --git a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts index 1c7376e41c4d7..c0c15024b5401 100644 --- a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts +++ b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts @@ -230,4 +230,17 @@ export class SyntheticsMonitorTestService { expect(deleteResponse.status).to.eql(statusCode); return deleteResponse; } + + async deleteMonitorBulk(monitorIds: string[], statusCode = 200, spaceId?: string) { + const deleteResponse = await this.supertest + .post( + spaceId + ? `/s/${spaceId}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/_bulk_delete` + : SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/_bulk_delete' + ) + .send({ ids: monitorIds }) + .set('kbn-xsrf', 'true'); + expect(deleteResponse.status).to.eql(statusCode); + return deleteResponse; + } } diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts index 8e0aaeff21580..e0a79a8905ee8 100644 --- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts @@ -287,8 +287,9 @@ export default function ({ getService }: FtrProviderContext) { const deleteResponse = await supertestAPI .delete(SYNTHETICS_API_URLS.PARAMS) .set('kbn-xsrf', 'true') - .send({ ids }) - .expect(200); + .send({ ids }); + + expect(deleteResponse.status).eql(200); expect(deleteResponse.body).to.have.length(2); From ce0e63b2b9255dcbf11f321714e29ca3ff1dad1e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:56:01 +1100 Subject: [PATCH 14/47] [8.x] Fix getAlertSummary returning 400 (Bad Request) (#199116) (#199281) # Backport This will backport the following commits from `main` to `8.x`: - [Fix getAlertSummary returning 400 (Bad Request) (#199116)](https://github.com/elastic/kibana/pull/199116) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Maryam Saeidi --- .../src/hooks/use_alerts_history.ts | 4 +++- .../hooks/use_load_alert_summary.ts | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/x-pack/packages/observability/alert_details/src/hooks/use_alerts_history.ts b/x-pack/packages/observability/alert_details/src/hooks/use_alerts_history.ts index 7519fea5f99f8..193acb63f845b 100644 --- a/x-pack/packages/observability/alert_details/src/hooks/use_alerts_history.ts +++ b/x-pack/packages/observability/alert_details/src/hooks/use_alerts_history.ts @@ -55,6 +55,7 @@ export function useAlertsHistory({ http, instanceId, }: Props): UseAlertsHistory { + const enabled = !!featureIds.length; const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ queryKey: ['useAlertsHistory'], queryFn: async ({ signal }) => { @@ -71,10 +72,11 @@ export function useAlertsHistory({ }); }, refetchOnWindowFocus: false, + enabled, }); return { data: isInitialLoading ? EMPTY_ALERTS_HISTORY : data ?? EMPTY_ALERTS_HISTORY, - isLoading: isInitialLoading || isLoading || isRefetching, + isLoading: enabled && (isInitialLoading || isLoading || isRefetching), isSuccess, isError, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_alert_summary.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_alert_summary.ts index 50fcc6025938a..10ce201885098 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_alert_summary.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_alert_summary.ts @@ -102,16 +102,18 @@ async function fetchAlertSummary({ timeRange: AlertSummaryTimeRange; filter?: estypes.QueryDslQueryContainer; }): Promise { - const res = await http.post>(`${BASE_RAC_ALERTS_API_PATH}/_alert_summary`, { - signal, - body: JSON.stringify({ - fixed_interval: fixedInterval, - gte: utcFrom, - lte: utcTo, - featureIds, - filter: [filter], - }), - }); + const res = featureIds.length + ? await http.post>(`${BASE_RAC_ALERTS_API_PATH}/_alert_summary`, { + signal, + body: JSON.stringify({ + fixed_interval: fixedInterval, + gte: utcFrom, + lte: utcTo, + featureIds, + filter: [filter], + }), + }) + : {}; const activeAlertCount = res?.activeAlertCount ?? 0; const activeAlerts = res?.activeAlerts ?? []; From 27c3d9a6dccf26bc0fb7551373fecb9999a97b5b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 23:10:38 +1100 Subject: [PATCH 15/47] [8.x] [Entity Store] Aligning mappings with ECS (#199001) (#199283) # Backport This will backport the following commits from `main` to `8.x`: - [[Entity Store] Aligning mappings with ECS (#199001)](https://github.com/elastic/kibana/pull/199001) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Tiago Vila Verde --- .../entity_store/entities_list.test.tsx | 2 +- .../hooks/use_entities_list_columns.tsx | 2 +- .../united_entity_definitions/constants.ts | 4 +-- .../entity_types/host.ts | 12 +++++++- .../entity_types/user.ts | 12 +++++++- .../get_united_definition.test.ts | 28 ++++++++++++++++--- .../united_entity_definition.ts | 10 +++++++ 7 files changed, 60 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx index 91f0c42eab385..0f493304e1f87 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/entities_list.test.tsx @@ -109,7 +109,7 @@ describe('EntitiesList', () => { fireEvent.click(columnHeader); expect(mockUseEntitiesListQuery).toHaveBeenCalledWith( expect.objectContaining({ - sortField: 'entity.name.text', + sortField: 'entity.name', sortOrder: 'asc', }) ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx index 974a80454ee21..e603c95b6604a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entities_list_columns.tsx @@ -79,7 +79,7 @@ export const useEntitiesListColumns = (): EntitiesListColumns => { width: '5%', }, { - field: 'entity.name.text', + field: 'entity.name', name: ( { "entity.name": Object { "fields": Object { "text": Object { - "type": "keyword", + "type": "match_only_text", }, }, - "type": "text", + "type": "keyword", }, "entity.source": Object { "type": "keyword", @@ -59,9 +59,19 @@ describe('getUnitedEntityDefinition', () => { "type": "keyword", }, "host.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, "type": "keyword", }, "host.os.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, "type": "keyword", }, "host.os.type": Object { @@ -335,10 +345,10 @@ describe('getUnitedEntityDefinition', () => { "entity.name": Object { "fields": Object { "text": Object { - "type": "keyword", + "type": "match_only_text", }, }, - "type": "text", + "type": "keyword", }, "entity.source": Object { "type": "keyword", @@ -350,6 +360,11 @@ describe('getUnitedEntityDefinition', () => { "type": "keyword", }, "user.full_name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, "type": "keyword", }, "user.hash": Object { @@ -359,6 +374,11 @@ describe('getUnitedEntityDefinition', () => { "type": "keyword", }, "user.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, "type": "keyword", }, "user.risk.calculated_level": Object { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/united_entity_definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/united_entity_definition.ts index eced765c75193..fc7430ebb1806 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/united_entity_definition.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/united_entity_definition.ts @@ -94,6 +94,11 @@ export class UnitedEntityDefinition { ...BASE_ENTITY_INDEX_MAPPING, [identityField]: { type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, }, }; @@ -107,6 +112,11 @@ export class UnitedEntityDefinition { properties[identityField] = { type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, }; return { From 31e0899604fd707762ea454d65e0df7c048b123f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 23:26:08 +1100 Subject: [PATCH 16/47] [8.x] [Dataset quality] Extracting totalDocs form degradedDocs request (#198757) (#199177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Dataset quality] Extracting totalDocs form degradedDocs request (#198757)](https://github.com/elastic/kibana/pull/198757) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --------- Co-authored-by: Yngrid Coello --- .../dataset_quality/common/api_types.ts | 34 +- .../dataset_quality/common/constants.ts | 5 +- .../data_streams_stats/data_stream_stat.ts | 34 +- .../data_streams_stats/malformed_docs_stat.ts | 31 -- .../common/data_streams_stats/types.ts | 12 +- .../dataset_quality_indicator.tsx | 4 +- .../hooks/use_dataset_quality_filters.ts | 2 +- .../hooks/use_dataset_quality_table.tsx | 3 +- .../public/hooks/use_dataset_telemetry.ts | 2 +- .../public/hooks/use_summary_panel.ts | 2 +- .../data_streams_stats_client.ts | 46 ++- .../services/data_streams_stats/types.ts | 7 +- .../src/defaults.ts | 3 +- .../src/notifications.ts | 13 + .../src/state_machine.ts | 106 ++++-- .../dataset_quality_controller/src/types.ts | 17 +- .../public/utils/generate_datasets.test.ts | 335 +++++++++++++----- .../public/utils/generate_datasets.ts | 58 +-- ...et_dataset_aggregated_paginated_results.ts | 94 +++++ .../routes/data_streams/get_degraded_docs.ts | 168 ++------- .../server/routes/data_streams/routes.ts | 47 ++- .../dataset_quality/data_stream_total_docs.ts | 129 +++++++ .../observability/dataset_quality/index.ts | 1 + .../tests/data_streams/degraded_docs.spec.ts | 96 +---- .../tests/data_streams/total_docs.spec.ts | 41 +++ 25 files changed, 806 insertions(+), 484 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/malformed_docs_stat.ts create mode 100644 x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_dataset_aggregated_paginated_results.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/data_stream_total_docs.ts create mode 100644 x-pack/test/dataset_quality_api_integration/tests/data_streams/total_docs.spec.ts diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/api_types.ts b/x-pack/plugins/observability_solution/dataset_quality/common/api_types.ts index 903d7f0607663..51a1421aec918 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/api_types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/api_types.ts @@ -37,6 +37,25 @@ export const dataStreamStatRt = rt.intersection([ export type DataStreamStat = rt.TypeOf; +export const dataStreamDocsStatRt = rt.type({ + dataset: rt.string, + count: rt.number, +}); + +export type DataStreamDocsStat = rt.TypeOf; + +export const getDataStreamTotalDocsResponseRt = rt.type({ + totalDocs: rt.array(dataStreamDocsStatRt), +}); + +export type DataStreamTotalDocsResponse = rt.TypeOf; + +export const getDataStreamDegradedDocsResponseRt = rt.type({ + degradedDocs: rt.array(dataStreamDocsStatRt), +}); + +export type DataStreamDegradedDocsResponse = rt.TypeOf; + export const integrationDashboardRT = rt.type({ id: rt.string, title: rt.string, @@ -84,15 +103,6 @@ export const getIntegrationsResponseRt = rt.exact( export type IntegrationResponse = rt.TypeOf; -export const degradedDocsRt = rt.type({ - dataset: rt.string, - count: rt.number, - docsCount: rt.number, - percentage: rt.number, -}); - -export type DegradedDocs = rt.TypeOf; - export const degradedFieldRt = rt.type({ name: rt.string, count: rt.number, @@ -188,12 +198,6 @@ export const getDataStreamsStatsResponseRt = rt.exact( }) ); -export const getDataStreamsDegradedDocsStatsResponseRt = rt.exact( - rt.type({ - degradedDocs: rt.array(degradedDocsRt), - }) -); - export const getDataStreamsSettingsResponseRt = rt.exact(dataStreamSettingsRt); export const getDataStreamsDetailsResponseRt = rt.exact(dataStreamDetailsRt); diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/constants.ts b/x-pack/plugins/observability_solution/dataset_quality/common/constants.ts index 1b822c6c111d9..74809e0e19420 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/constants.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/constants.ts @@ -11,6 +11,7 @@ export const DATASET_QUALITY_APP_ID = 'dataset_quality'; export const DEFAULT_DATASET_TYPE: DataStreamType = 'logs'; export const DEFAULT_LOGS_DATA_VIEW = 'logs-*-*'; +export const DEFAULT_DATASET_QUALITY: QualityIndicators = 'good'; export const POOR_QUALITY_MINIMUM_PERCENTAGE = 3; export const DEGRADED_QUALITY_MINIMUM_PERCENTAGE = 0; @@ -26,10 +27,8 @@ export const DEFAULT_TIME_RANGE = { from: 'now-24h', to: 'now' }; export const DEFAULT_DATEPICKER_REFRESH = { value: 60000, pause: false }; export const DEFAULT_DEGRADED_DOCS = { - percentage: 0, count: 0, - docsCount: 0, - quality: 'good' as QualityIndicators, + percentage: 0, }; export const NUMBER_FORMAT = '0,0.[000]'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/data_stream_stat.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/data_stream_stat.ts index 164a43c625fb1..094d92ff3fea6 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/data_stream_stat.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/data_stream_stat.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { DEFAULT_DEGRADED_DOCS } from '../constants'; +import { DataStreamDocsStat } from '../api_types'; +import { DEFAULT_DATASET_QUALITY, DEFAULT_DEGRADED_DOCS } from '../constants'; import { DataStreamType, QualityIndicators } from '../types'; import { indexNameToDataStreamParts, mapPercentageToQuality } from '../utils'; import { Integration } from './integration'; -import { DegradedDocsStat } from './malformed_docs_stat'; import { DataStreamStatType } from './types'; export class DataStreamStat { @@ -24,11 +24,11 @@ export class DataStreamStat { userPrivileges?: DataStreamStatType['userPrivileges']; totalDocs?: DataStreamStatType['totalDocs']; // total datastream docs count integration?: Integration; + quality: QualityIndicators; + docsInTimeRange?: number; degradedDocs: { percentage: number; count: number; - docsCount: number; // docs count in the filtered time range - quality: QualityIndicators; }; private constructor(dataStreamStat: DataStreamStat) { @@ -43,12 +43,9 @@ export class DataStreamStat { this.userPrivileges = dataStreamStat.userPrivileges; this.totalDocs = dataStreamStat.totalDocs; this.integration = dataStreamStat.integration; - this.degradedDocs = { - percentage: dataStreamStat.degradedDocs.percentage, - count: dataStreamStat.degradedDocs.count, - docsCount: dataStreamStat.degradedDocs.docsCount, - quality: dataStreamStat.degradedDocs.quality, - }; + this.quality = dataStreamStat.quality; + this.docsInTimeRange = dataStreamStat.docsInTimeRange; + this.degradedDocs = dataStreamStat.degradedDocs; } public static create(dataStreamStat: DataStreamStatType) { @@ -65,6 +62,7 @@ export class DataStreamStat { lastActivity: dataStreamStat.lastActivity, userPrivileges: dataStreamStat.userPrivileges, totalDocs: dataStreamStat.totalDocs, + quality: DEFAULT_DATASET_QUALITY, degradedDocs: DEFAULT_DEGRADED_DOCS, }; @@ -74,9 +72,11 @@ export class DataStreamStat { public static fromDegradedDocStat({ degradedDocStat, datasetIntegrationMap, + totalDocs, }: { - degradedDocStat: DegradedDocsStat; + degradedDocStat: DataStreamDocsStat & { percentage: number }; datasetIntegrationMap: Record; + totalDocs: number; }) { const { type, dataset, namespace } = indexNameToDataStreamParts(degradedDocStat.dataset); @@ -87,19 +87,23 @@ export class DataStreamStat { title: datasetIntegrationMap[dataset]?.title || dataset, namespace, integration: datasetIntegrationMap[dataset]?.integration, + quality: mapPercentageToQuality(degradedDocStat.percentage), + docsInTimeRange: totalDocs, degradedDocs: { percentage: degradedDocStat.percentage, count: degradedDocStat.count, - docsCount: degradedDocStat.docsCount, - quality: mapPercentageToQuality(degradedDocStat.percentage), }, }; return new DataStreamStat(dataStreamStatProps); } - public static calculateFilteredSize({ sizeBytes, totalDocs, degradedDocs }: DataStreamStat) { + public static calculateFilteredSize({ sizeBytes, totalDocs, docsInTimeRange }: DataStreamStat) { const avgDocSize = sizeBytes && totalDocs ? sizeBytes / totalDocs : 0; - return avgDocSize * degradedDocs.docsCount; + return avgDocSize * (docsInTimeRange ?? 0); + } + + public static calculatePercentage({ totalDocs, count }: { totalDocs?: number; count?: number }) { + return totalDocs && count ? (count / totalDocs) * 100 : 0; } } diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/malformed_docs_stat.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/malformed_docs_stat.ts deleted file mode 100644 index c86b802ea42da..0000000000000 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/malformed_docs_stat.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 { QualityIndicators } from '../types'; -import { mapPercentageToQuality } from '../utils'; -import { DegradedDocsStatType } from './types'; - -export class DegradedDocsStat { - dataset: DegradedDocsStatType['dataset']; - percentage: DegradedDocsStatType['percentage']; - count: DegradedDocsStatType['count']; - docsCount: DegradedDocsStatType['docsCount']; - quality: QualityIndicators; - - private constructor(degradedDocsStat: DegradedDocsStat) { - this.dataset = degradedDocsStat.dataset; - this.percentage = degradedDocsStat.percentage; - this.count = degradedDocsStat.count; - this.docsCount = degradedDocsStat.docsCount; - this.quality = degradedDocsStat.quality; - } - - public static create(degradedDocsStat: DegradedDocsStatType) { - const quality = mapPercentageToQuality(degradedDocsStat.percentage); - return new DegradedDocsStat({ ...degradedDocsStat, quality }); - } -} diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/types.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/types.ts index 1e5adedc20f3a..bc0c12d234d26 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/types.ts @@ -18,10 +18,14 @@ export type DataStreamStatServiceResponse = GetDataStreamsStatsResponse; export type GetDataStreamsDegradedDocsStatsParams = APIClientRequestParamsOf<`GET /internal/dataset_quality/data_streams/degraded_docs`>['params']; export type GetDataStreamsDegradedDocsStatsQuery = GetDataStreamsDegradedDocsStatsParams['query']; -export type GetDataStreamsDegradedDocsStatsResponse = - APIReturnType<`GET /internal/dataset_quality/data_streams/degraded_docs`>; -export type DegradedDocsStatType = GetDataStreamsDegradedDocsStatsResponse['degradedDocs'][0]; -export type DataStreamDegradedDocsStatServiceResponse = DegradedDocsStatType[]; + +/* +Types for stats based in documents inside a DataStream +*/ + +export type GetDataStreamsTotalDocsParams = + APIClientRequestParamsOf<`GET /internal/dataset_quality/data_streams/total_docs`>['params']; +export type GetDataStreamsTotalDocsQuery = GetDataStreamsTotalDocsParams['query']; /* Types for Degraded Fields inside a DataStream diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/quality_indicator/dataset_quality_indicator.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/quality_indicator/dataset_quality_indicator.tsx index 419a13272dbc8..78c6d3bff9331 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/quality_indicator/dataset_quality_indicator.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/quality_indicator/dataset_quality_indicator.tsx @@ -19,9 +19,7 @@ export const DatasetQualityIndicator = ({ isLoading: boolean; dataStreamStat: DataStreamStat; }) => { - const { - degradedDocs: { quality }, - } = dataStreamStat; + const { quality } = dataStreamStat; const translatedQuality = i18n.translate('xpack.datasetQuality.datasetQualityIdicator', { defaultMessage: '{quality}', diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_filters.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_filters.ts index e370e7c22d469..056bba2304144 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_filters.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_filters.ts @@ -49,7 +49,7 @@ export const useDatasetQualityFilters = () => { datasets.reduce( (acc: Filters, dataset) => ({ namespaces: [...new Set([...acc.namespaces, dataset.namespace])], - qualities: [...new Set([...acc.qualities, dataset.degradedDocs.quality])], + qualities: [...new Set([...acc.qualities, dataset.quality])], filteredIntegrations: [ ...new Set([...acc.filteredIntegrations, dataset.integration?.name ?? 'none']), ], diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_table.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_table.tsx index 55265a250bb75..6529ae1841ee3 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_table.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_table.tsx @@ -132,8 +132,7 @@ export const useDatasetQualityTable = () => { const passesNamespaceFilter = namespaces.length === 0 || namespaces.includes(dataset.namespace); - const passesQualityFilter = - qualities.length === 0 || qualities.includes(dataset.degradedDocs.quality); + const passesQualityFilter = qualities.length === 0 || qualities.includes(dataset.quality); const passesQueryFilter = !query || dataset.rawName.includes(query); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_telemetry.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_telemetry.ts index 167ebd37fe81a..7d486f94f2607 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_telemetry.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_telemetry.ts @@ -77,7 +77,7 @@ function getDatasetEbtProps( namespace: dataset.namespace, type: dataset.type, }, - data_stream_health: dataset.degradedDocs.quality, + data_stream_health: dataset.quality, data_stream_aggregatable: nonAggregatableDatasets.some( (indexName) => indexName === dataset.rawName ), diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_summary_panel.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_summary_panel.ts index a85dc9c21d222..014d9f578eb60 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_summary_panel.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_summary_panel.ts @@ -84,7 +84,7 @@ const useSummaryPanel = () => { datasetsActivity, numberOfDatasets: filteredItems.length, - numberOfDocuments: filteredItems.reduce((acc, curr) => acc + curr.degradedDocs.docsCount, 0), + numberOfDocuments: filteredItems.reduce((acc, curr) => acc + curr.docsInTimeRange!, 0), }; }; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts index 8642a863726df..8e218819315b2 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts @@ -10,8 +10,11 @@ import { decodeOrThrow } from '@kbn/io-ts-utils'; import rison from '@kbn/rison'; import { KNOWN_TYPES } from '../../../common/constants'; import { - getDataStreamsDegradedDocsStatsResponseRt, + DataStreamDegradedDocsResponse, + DataStreamTotalDocsResponse, + getDataStreamDegradedDocsResponseRt, getDataStreamsStatsResponseRt, + getDataStreamTotalDocsResponseRt, getIntegrationsResponseRt, getNonAggregatableDatasetsRt, IntegrationResponse, @@ -20,9 +23,9 @@ import { import { DataStreamStatServiceResponse, GetDataStreamsDegradedDocsStatsQuery, - GetDataStreamsDegradedDocsStatsResponse, GetDataStreamsStatsQuery, GetDataStreamsStatsResponse, + GetDataStreamsTotalDocsQuery, GetNonAggregatableDataStreamsParams, } from '../../../common/data_streams_stats'; import { Integration } from '../../../common/data_streams_stats/integration'; @@ -56,16 +59,37 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { return { dataStreamsStats, datasetUserPrivileges }; } + public async getDataStreamsTotalDocs(params: GetDataStreamsTotalDocsQuery) { + const response = await this.http + .get('/internal/dataset_quality/data_streams/total_docs', { + query: { + ...params, + }, + }) + .catch((error) => { + throw new DatasetQualityError(`Failed to fetch data streams total docs: ${error}`, error); + }); + + const { totalDocs } = decodeOrThrow( + getDataStreamTotalDocsResponseRt, + (message: string) => + new DatasetQualityError( + `Failed to decode data streams total docs stats response: ${message}` + ) + )(response); + + return totalDocs; + } + public async getDataStreamsDegradedStats(params: GetDataStreamsDegradedDocsStatsQuery) { + const types = params.types.length === 0 ? KNOWN_TYPES : params.types; const response = await this.http - .get( - '/internal/dataset_quality/data_streams/degraded_docs', - { - query: { - ...params, - }, - } - ) + .get('/internal/dataset_quality/data_streams/degraded_docs', { + query: { + ...params, + types: rison.encodeArray(types), + }, + }) .catch((error) => { throw new DatasetQualityError( `Failed to fetch data streams degraded stats: ${error}`, @@ -74,7 +98,7 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { }); const { degradedDocs } = decodeOrThrow( - getDataStreamsDegradedDocsStatsResponseRt, + getDataStreamDegradedDocsResponseRt, (message: string) => new DatasetQualityError( `Failed to decode data streams degraded docs stats response: ${message}` diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/types.ts index dd057ee7f3062..240e5519cfc3d 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/types.ts @@ -7,14 +7,14 @@ import { HttpStart } from '@kbn/core/public'; import { - DataStreamDegradedDocsStatServiceResponse, DataStreamStatServiceResponse, GetDataStreamsDegradedDocsStatsQuery, GetDataStreamsStatsQuery, + GetDataStreamsTotalDocsQuery, GetNonAggregatableDataStreamsParams, } from '../../../common/data_streams_stats'; import { Integration } from '../../../common/data_streams_stats/integration'; -import { NonAggregatableDatasets } from '../../../common/api_types'; +import { DataStreamDocsStat, NonAggregatableDatasets } from '../../../common/api_types'; export type DataStreamsStatsServiceSetup = void; @@ -30,7 +30,8 @@ export interface IDataStreamsStatsClient { getDataStreamsStats(params?: GetDataStreamsStatsQuery): Promise; getDataStreamsDegradedStats( params?: GetDataStreamsDegradedDocsStatsQuery - ): Promise; + ): Promise; + getDataStreamsTotalDocs(params: GetDataStreamsTotalDocsQuery): Promise; getIntegrations(): Promise; getNonAggregatableDatasets( params: GetNonAggregatableDataStreamsParams diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/defaults.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/defaults.ts index 41cfa859ec977..7c77fe9d59422 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/defaults.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/defaults.ts @@ -37,7 +37,8 @@ export const DEFAULT_CONTEXT: DefaultDatasetQualityControllerState = { canViewIntegrations: true, }, dataStreamStats: [], - degradedDocStats: DEFAULT_DICTIONARY_TYPE, + degradedDocStats: [], + totalDocsStats: DEFAULT_DICTIONARY_TYPE, filters: { inactive: true, fullNames: false, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts index a21cc85aac449..0dea80104245f 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts @@ -7,6 +7,7 @@ import { IToasts } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { DataStreamType } from '../../../../common/types'; export const fetchDatasetStatsFailedNotifier = (toasts: IToasts, error: Error) => { toasts.addDanger({ @@ -26,6 +27,18 @@ export const fetchDegradedStatsFailedNotifier = (toasts: IToasts, error: Error) }); }; +export const fetchTotalDocsFailedNotifier = (toasts: IToasts, error: Error, meta: any) => { + const dataStreamType = meta._event.origin as DataStreamType; + + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.fetchTotalDocsFailed', { + defaultMessage: "We couldn't get total docs information for {dataStreamType}.", + values: { dataStreamType }, + }), + text: error.message, + }); +}; + export const fetchIntegrationsFailedNotifier = (toasts: IToasts, error: Error) => { toasts.addDanger({ title: i18n.translate('xpack.datasetQuality.fetchIntegrationsFailed', { diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts index a803d73448263..1217e52894ce7 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts @@ -8,12 +8,13 @@ import { IToasts } from '@kbn/core/public'; import { getDateISORange } from '@kbn/timerange'; import { assign, createMachine, DoneInvokeEvent, InterpreterFrom } from 'xstate'; -import { DataStreamStat, NonAggregatableDatasets } from '../../../../common/api_types'; -import { KNOWN_TYPES } from '../../../../common/constants'; import { - DataStreamDegradedDocsStatServiceResponse, - DataStreamStatServiceResponse, -} from '../../../../common/data_streams_stats'; + DataStreamDocsStat, + DataStreamStat, + NonAggregatableDatasets, +} from '../../../../common/api_types'; +import { KNOWN_TYPES } from '../../../../common/constants'; +import { DataStreamStatServiceResponse } from '../../../../common/data_streams_stats'; import { Integration } from '../../../../common/data_streams_stats/integration'; import { DataStreamType } from '../../../../common/types'; import { IDataStreamsStatsClient } from '../../../services/data_streams_stats'; @@ -24,6 +25,7 @@ import { fetchDatasetStatsFailedNotifier, fetchDegradedStatsFailedNotifier, fetchIntegrationsFailedNotifier, + fetchTotalDocsFailedNotifier, } from './notifications'; import { DatasetQualityControllerContext, @@ -92,34 +94,69 @@ export const createPureDatasetQualityControllerStateMachine = ( initial: 'fetching', states: { fetching: { - ...generateInvokePerType({ + invoke: { src: 'loadDegradedDocs', + onDone: { + target: 'loaded', + actions: ['storeDegradedDocStats', 'storeDatasets'], + }, + onError: [ + { + target: 'unauthorized', + cond: 'checkIfActionForbidden', + }, + { + target: 'loaded', + actions: ['notifyFetchDegradedStatsFailed'], + }, + ], + }, + }, + loaded: {}, + unauthorized: { type: 'final' }, + }, + on: { + UPDATE_TIME_RANGE: { + target: 'degradedDocs.fetching', + actions: ['storeTimeRange'], + }, + REFRESH_DATA: { + target: 'degradedDocs.fetching', + }, + }, + }, + docsStats: { + initial: 'fetching', + states: { + fetching: { + ...generateInvokePerType({ + src: 'loadDataStreamDocsStats', }), }, loaded: {}, unauthorized: { type: 'final' }, }, on: { - SAVE_DEGRADED_DOCS_STATS: { - target: 'degradedDocs.loaded', - actions: ['storeDegradedDocStats', 'storeDatasets'], + SAVE_TOTAL_DOCS_STATS: { + target: 'docsStats.loaded', + actions: ['storeTotalDocStats', 'storeDatasets'], }, - NOTIFY_DEGRADED_DOCS_STATS_FAILED: [ + NOTIFY_TOTAL_DOCS_STATS_FAILED: [ { - target: 'degradedDocs.unauthorized', + target: 'docsStats.unauthorized', cond: 'checkIfActionForbidden', }, { - target: 'degradedDocs.loaded', - actions: ['notifyFetchDegradedStatsFailed'], + target: 'docsStats.loaded', + actions: ['notifyFetchTotalDocsFailed'], }, ], UPDATE_TIME_RANGE: { - target: 'degradedDocs.fetching', + target: 'docsStats.fetching', actions: ['storeTimeRange'], }, REFRESH_DATA: { - target: 'degradedDocs.fetching', + target: 'docsStats.fetching', }, }, }, @@ -329,18 +366,21 @@ export const createPureDatasetQualityControllerStateMachine = ( }; } ), - storeDegradedDocStats: assign( - (context, event: DoneInvokeEvent, meta) => { + storeTotalDocStats: assign( + (context, event: DoneInvokeEvent, meta) => { const type = meta._event.origin as DataStreamType; return { - degradedDocStats: { - ...context.degradedDocStats, + totalDocsStats: { + ...context.totalDocsStats, [type]: event.data, }, }; } ), + storeDegradedDocStats: assign((_context, event: DoneInvokeEvent) => ({ + degradedDocStats: event.data, + })), storeNonAggregatableDatasets: assign( (_context, event: DoneInvokeEvent) => ({ nonAggregatableDatasets: event.data.datasets, @@ -364,7 +404,8 @@ export const createPureDatasetQualityControllerStateMachine = ( datasets: generateDatasets( context.dataStreamStats, context.degradedDocStats, - context.integrations + context.integrations, + context.totalDocsStats ), } : {}; @@ -404,6 +445,8 @@ export const createDatasetQualityControllerStateMachine = ({ fetchNonAggregatableDatasetsFailedNotifier(toasts, event.data), notifyFetchIntegrationsFailed: (_context, event: DoneInvokeEvent) => fetchIntegrationsFailedNotifier(toasts, event.data), + notifyFetchTotalDocsFailed: (_context, event: DoneInvokeEvent, meta) => + fetchTotalDocsFailedNotifier(toasts, event.data, meta), }, services: { loadDataStreamStats: (context, _event) => @@ -411,32 +454,41 @@ export const createDatasetQualityControllerStateMachine = ({ types: context.filters.types as DataStreamType[], datasetQuery: context.filters.query, }), - loadDegradedDocs: + loadDataStreamDocsStats: (context, _event, { data: { type } }) => async (send) => { try { const { startDate: start, endDate: end } = getDateISORange(context.filters.timeRange); - const degradedDocsStats = await (isTypeSelected(type, context) - ? dataStreamStatsClient.getDataStreamsDegradedStats({ + const totalDocsStats = await (isTypeSelected(type, context) + ? dataStreamStatsClient.getDataStreamsTotalDocs({ type, - datasetQuery: context.filters.query, start, end, }) : Promise.resolve([])); send({ - type: 'SAVE_DEGRADED_DOCS_STATS', - data: degradedDocsStats, + type: 'SAVE_TOTAL_DOCS_STATS', + data: totalDocsStats, }); } catch (e) { send({ - type: 'NOTIFY_DEGRADED_DOCS_STATS_FAILED', + type: 'NOTIFY_TOTAL_DOCS_STATS_FAILED', data: e, }); } }, + loadDegradedDocs: (context) => { + const { startDate: start, endDate: end } = getDateISORange(context.filters.timeRange); + + return dataStreamStatsClient.getDataStreamsDegradedStats({ + types: context.filters.types as DataStreamType[], + datasetQuery: context.filters.query, + start, + end, + }); + }, loadNonAggregatableDatasets: (context) => { const { startDate: start, endDate: end } = getDateISORange(context.filters.timeRange); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts index a5e03cfb480ff..de7fdbf9fbd77 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts @@ -6,16 +6,18 @@ */ import { DoneInvokeEvent } from 'xstate'; -import { DatasetUserPrivileges, NonAggregatableDatasets } from '../../../../common/api_types'; import { - DataStreamDegradedDocsStatServiceResponse, + DataStreamDocsStat, + DatasetUserPrivileges, + NonAggregatableDatasets, +} from '../../../../common/api_types'; +import { DataStreamDetails, DataStreamStat, DataStreamStatServiceResponse, DataStreamStatType, } from '../../../../common/data_streams_stats'; import { Integration } from '../../../../common/data_streams_stats/integration'; -import { DegradedDocsStat } from '../../../../common/data_streams_stats/malformed_docs_stat'; import { DataStreamType, QualityIndicators, @@ -50,8 +52,12 @@ export interface WithDataStreamStats { dataStreamStats: DataStreamStatType[]; } +export interface WithTotalDocs { + totalDocsStats: DictionaryType; +} + export interface WithDegradedDocs { - degradedDocStats: DictionaryType; + degradedDocStats: DataStreamDocsStat[]; } export interface WithNonAggregatableDatasets { @@ -68,6 +74,7 @@ export interface WithIntegrations { export type DefaultDatasetQualityControllerState = WithTableOptions & WithDataStreamStats & + WithTotalDocs & WithDegradedDocs & WithDatasets & WithFilters & @@ -146,7 +153,7 @@ export type DatasetQualityControllerEvent = type: 'UPDATE_TYPES'; types: DataStreamType[]; } - | DoneInvokeEvent + | DoneInvokeEvent | DoneInvokeEvent | DoneInvokeEvent | DoneInvokeEvent diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/utils/generate_datasets.test.ts b/x-pack/plugins/observability_solution/dataset_quality/public/utils/generate_datasets.test.ts index 6f2e46baacf8c..b75c74c2fd728 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/utils/generate_datasets.test.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/utils/generate_datasets.test.ts @@ -5,11 +5,10 @@ * 2.0. */ -import { indexNameToDataStreamParts } from '../../common/utils'; -import { Integration } from '../../common/data_streams_stats/integration'; -import { generateDatasets } from './generate_datasets'; import { DataStreamStatType } from '../../common/data_streams_stats'; +import { Integration } from '../../common/data_streams_stats/integration'; import { DEFAULT_DICTIONARY_TYPE } from '../state_machines/dataset_quality_controller'; +import { generateDatasets } from './generate_datasets'; describe('generateDatasets', () => { const integrations: Integration[] = [ @@ -41,6 +40,7 @@ describe('generateDatasets', () => { lastActivity: 1712911241117, size: '82.1kb', sizeBytes: 84160, + totalDocs: 100, integration: 'system', userPrivileges: { canMonitor: true, @@ -51,182 +51,337 @@ describe('generateDatasets', () => { lastActivity: 1712911241117, size: '62.5kb', sizeBytes: 64066, + totalDocs: 100, userPrivileges: { canMonitor: true, }, }, ]; - const degradedDocs = { + const totalDocs = { ...DEFAULT_DICTIONARY_TYPE, logs: [ { dataset: 'logs-system.application-default', - percentage: 0, - count: 0, - docsCount: 0, - quality: 'good' as const, + count: 100, }, { dataset: 'logs-synth-default', - percentage: 11.320754716981131, - count: 6, - docsCount: 0, - quality: 'poor' as const, + count: 100, }, ], }; - it('merges integrations information with dataStreamStats', () => { - const datasets = generateDatasets(dataStreamStats, DEFAULT_DICTIONARY_TYPE, integrations); + const degradedDocs = [ + { + dataset: 'logs-system.application-default', + count: 0, + }, + { + dataset: 'logs-synth-default', + count: 6, + }, + ]; + + it('merges integrations information with dataStreamStats and degradedDocs', () => { + const datasets = generateDatasets(dataStreamStats, degradedDocs, integrations, totalDocs); expect(datasets).toEqual([ { - ...dataStreamStats[0], - name: indexNameToDataStreamParts(dataStreamStats[0].name).dataset, - namespace: indexNameToDataStreamParts(dataStreamStats[0].name).namespace, - title: - integrations[0].datasets[indexNameToDataStreamParts(dataStreamStats[0].name).dataset], - type: indexNameToDataStreamParts(dataStreamStats[0].name).type, - rawName: dataStreamStats[0].name, + name: 'system.application', + type: 'logs', + namespace: 'default', + title: 'Windows Application Events', + rawName: 'logs-system.application-default', + lastActivity: 1712911241117, + size: '82.1kb', + sizeBytes: 84160, integration: integrations[0], + totalDocs: 100, + userPrivileges: { + canMonitor: true, + }, + docsInTimeRange: 100, + quality: 'good', + degradedDocs: { + percentage: 0, + count: 0, + }, + }, + { + name: 'synth', + type: 'logs', + namespace: 'default', + title: 'synth', + rawName: 'logs-synth-default', + lastActivity: 1712911241117, + size: '62.5kb', + sizeBytes: 64066, + integration: undefined, + totalDocs: 100, + userPrivileges: { + canMonitor: true, + }, + docsInTimeRange: 100, + quality: 'poor', degradedDocs: { - percentage: degradedDocs.logs[0].percentage, - count: degradedDocs.logs[0].count, - docsCount: degradedDocs.logs[0].docsCount, - quality: degradedDocs.logs[0].quality, + count: 6, + percentage: 6, }, }, + ]); + }); + + it('merges integrations information with dataStreamStats and degradedDocs when no docs in timerange', () => { + const datasets = generateDatasets( + dataStreamStats, + degradedDocs, + integrations, + DEFAULT_DICTIONARY_TYPE + ); + + expect(datasets).toEqual([ { - ...dataStreamStats[1], - name: indexNameToDataStreamParts(dataStreamStats[1].name).dataset, - namespace: indexNameToDataStreamParts(dataStreamStats[1].name).namespace, - title: indexNameToDataStreamParts(dataStreamStats[1].name).dataset, - type: indexNameToDataStreamParts(dataStreamStats[1].name).type, - rawName: dataStreamStats[1].name, + name: 'system.application', + type: 'logs', + namespace: 'default', + title: 'Windows Application Events', + rawName: 'logs-system.application-default', + lastActivity: 1712911241117, + size: '82.1kb', + sizeBytes: 84160, + integration: integrations[0], + totalDocs: 100, + userPrivileges: { + canMonitor: true, + }, + docsInTimeRange: 0, + quality: 'good', degradedDocs: { + percentage: 0, count: 0, + }, + }, + { + name: 'synth', + type: 'logs', + namespace: 'default', + title: 'synth', + rawName: 'logs-synth-default', + lastActivity: 1712911241117, + size: '62.5kb', + sizeBytes: 64066, + integration: undefined, + totalDocs: 100, + userPrivileges: { + canMonitor: true, + }, + docsInTimeRange: 0, + quality: 'good', + degradedDocs: { + count: 6, percentage: 0, - docsCount: 0, - quality: 'good', }, }, ]); }); it('merges integrations information with degradedDocs', () => { - const datasets = generateDatasets(undefined, degradedDocs, integrations); + const datasets = generateDatasets([], degradedDocs, integrations, totalDocs); expect(datasets).toEqual([ { - rawName: degradedDocs.logs[0].dataset, - name: indexNameToDataStreamParts(degradedDocs.logs[0].dataset).dataset, - type: indexNameToDataStreamParts(degradedDocs.logs[0].dataset).type, + name: 'system.application', + type: 'logs', + namespace: 'default', + title: 'Windows Application Events', + rawName: 'logs-system.application-default', + lastActivity: undefined, + size: undefined, + sizeBytes: undefined, + integration: integrations[0], + totalDocs: undefined, + userPrivileges: undefined, + docsInTimeRange: 100, + quality: 'good', + degradedDocs: { + percentage: 0, + count: 0, + }, + }, + { + name: 'synth', + type: 'logs', + namespace: 'default', + title: 'synth', + rawName: 'logs-synth-default', lastActivity: undefined, size: undefined, sizeBytes: undefined, + integration: undefined, + totalDocs: undefined, userPrivileges: undefined, - namespace: indexNameToDataStreamParts(degradedDocs.logs[0].dataset).namespace, - title: - integrations[0].datasets[ - indexNameToDataStreamParts(degradedDocs.logs[0].dataset).dataset - ], + docsInTimeRange: 100, + quality: 'poor', + degradedDocs: { + count: 6, + percentage: 6, + }, + }, + ]); + }); + + it('merges integrations information with degradedDocs and totalDocs', () => { + const datasets = generateDatasets([], degradedDocs, integrations, { + ...totalDocs, + logs: [...totalDocs.logs, { dataset: 'logs-another-default', count: 100 }], + }); + + expect(datasets).toEqual([ + { + name: 'system.application', + type: 'logs', + namespace: 'default', + title: 'Windows Application Events', + rawName: 'logs-system.application-default', + lastActivity: undefined, + size: undefined, + sizeBytes: undefined, integration: integrations[0], + totalDocs: undefined, + userPrivileges: undefined, + docsInTimeRange: 100, + quality: 'good', degradedDocs: { - percentage: degradedDocs.logs[0].percentage, - count: degradedDocs.logs[0].count, - docsCount: degradedDocs.logs[0].docsCount, - quality: degradedDocs.logs[0].quality, + percentage: 0, + count: 0, }, }, { - rawName: degradedDocs.logs[1].dataset, - name: indexNameToDataStreamParts(degradedDocs.logs[1].dataset).dataset, - type: indexNameToDataStreamParts(degradedDocs.logs[1].dataset).type, + name: 'synth', + type: 'logs', + namespace: 'default', + title: 'synth', + rawName: 'logs-synth-default', lastActivity: undefined, size: undefined, sizeBytes: undefined, + integration: undefined, + totalDocs: undefined, userPrivileges: undefined, - namespace: indexNameToDataStreamParts(degradedDocs.logs[1].dataset).namespace, - title: indexNameToDataStreamParts(degradedDocs.logs[1].dataset).dataset, + docsInTimeRange: 100, + quality: 'poor', + degradedDocs: { + count: 6, + percentage: 6, + }, + }, + { + name: 'another', + type: 'logs', + namespace: 'default', + title: 'another', + rawName: 'logs-another-default', + lastActivity: undefined, + size: undefined, + sizeBytes: undefined, integration: undefined, + totalDocs: undefined, + userPrivileges: undefined, + docsInTimeRange: 100, + quality: 'good', degradedDocs: { - percentage: degradedDocs.logs[1].percentage, - count: degradedDocs.logs[1].count, - docsCount: degradedDocs.logs[1].docsCount, - quality: degradedDocs.logs[1].quality, + percentage: 0, + count: 0, }, }, ]); }); - it('merges integrations information with dataStreamStats and degradedDocs', () => { - const datasets = generateDatasets(dataStreamStats, degradedDocs, integrations); + it('merges integrations information with dataStreamStats', () => { + const datasets = generateDatasets(dataStreamStats, [], integrations, totalDocs); expect(datasets).toEqual([ { - ...dataStreamStats[0], - name: indexNameToDataStreamParts(dataStreamStats[0].name).dataset, - namespace: indexNameToDataStreamParts(dataStreamStats[0].name).namespace, - title: - integrations[0].datasets[indexNameToDataStreamParts(dataStreamStats[0].name).dataset], - type: indexNameToDataStreamParts(dataStreamStats[0].name).type, - rawName: dataStreamStats[0].name, + name: 'system.application', + type: 'logs', + namespace: 'default', + title: 'Windows Application Events', + rawName: 'logs-system.application-default', + lastActivity: 1712911241117, + size: '82.1kb', + sizeBytes: 84160, integration: integrations[0], + totalDocs: 100, + userPrivileges: { + canMonitor: true, + }, + quality: 'good', + docsInTimeRange: 100, degradedDocs: { - percentage: degradedDocs.logs[0].percentage, - count: degradedDocs.logs[0].count, - docsCount: degradedDocs.logs[0].docsCount, - quality: degradedDocs.logs[0].quality, + count: 0, + percentage: 0, }, }, { - ...dataStreamStats[1], - name: indexNameToDataStreamParts(dataStreamStats[1].name).dataset, - namespace: indexNameToDataStreamParts(dataStreamStats[1].name).namespace, - title: indexNameToDataStreamParts(dataStreamStats[1].name).dataset, - type: indexNameToDataStreamParts(dataStreamStats[1].name).type, - rawName: dataStreamStats[1].name, + name: 'synth', + type: 'logs', + namespace: 'default', + title: 'synth', + rawName: 'logs-synth-default', + lastActivity: 1712911241117, + size: '62.5kb', + sizeBytes: 64066, + integration: undefined, + totalDocs: 100, + userPrivileges: { + canMonitor: true, + }, + quality: 'good', + docsInTimeRange: 100, degradedDocs: { - percentage: degradedDocs.logs[1].percentage, - count: degradedDocs.logs[1].count, - docsCount: degradedDocs.logs[1].docsCount, - quality: degradedDocs.logs[1].quality, + count: 0, + percentage: 0, }, }, ]); }); it('merges integration information with dataStreamStats when dataset is not an integration default one', () => { - const dataset = 'logs-system.custom-default'; - const nonDefaultDataset = { - name: dataset, + name: 'logs-system.custom-default', lastActivity: 1712911241117, size: '82.1kb', sizeBytes: 84160, + totalDocs: 100, integration: 'system', userPrivileges: { canMonitor: true, }, }; - const datasets = generateDatasets([nonDefaultDataset], DEFAULT_DICTIONARY_TYPE, integrations); + const datasets = generateDatasets([nonDefaultDataset], [], integrations, totalDocs); expect(datasets).toEqual([ { - ...nonDefaultDataset, - title: indexNameToDataStreamParts(dataset).dataset, - name: indexNameToDataStreamParts(dataset).dataset, - namespace: indexNameToDataStreamParts(dataset).namespace, - type: indexNameToDataStreamParts(dataset).type, - rawName: nonDefaultDataset.name, + name: 'system.custom', + type: 'logs', + namespace: 'default', + title: 'system.custom', + rawName: 'logs-system.custom-default', + lastActivity: 1712911241117, + size: '82.1kb', + sizeBytes: 84160, integration: integrations[0], + userPrivileges: { + canMonitor: true, + }, + quality: 'good', + totalDocs: 100, + docsInTimeRange: 0, degradedDocs: { count: 0, percentage: 0, - docsCount: 0, - quality: 'good', }, }, ]); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/utils/generate_datasets.ts b/x-pack/plugins/observability_solution/dataset_quality/public/utils/generate_datasets.ts index fb479198bbac3..8e9f2f3db7083 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/utils/generate_datasets.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/utils/generate_datasets.ts @@ -5,23 +5,20 @@ * 2.0. */ +import { DEFAULT_DEGRADED_DOCS } from '../../common/constants'; +import { DataStreamDocsStat } from '../../common/api_types'; import { DataStreamStatType } from '../../common/data_streams_stats/types'; import { mapPercentageToQuality } from '../../common/utils'; import { Integration } from '../../common/data_streams_stats/integration'; import { DataStreamStat } from '../../common/data_streams_stats/data_stream_stat'; -import { DegradedDocsStat } from '../../common/data_streams_stats/malformed_docs_stat'; import { DictionaryType } from '../state_machines/dataset_quality_controller/src/types'; import { flattenStats } from './flatten_stats'; - export function generateDatasets( dataStreamStats: DataStreamStatType[] = [], - degradedDocStats: DictionaryType, - integrations: Integration[] + degradedDocStats: DataStreamDocsStat[] = [], + integrations: Integration[], + totalDocsStats: DictionaryType ): DataStreamStat[] { - if (!dataStreamStats.length && !integrations.length) { - return []; - } - const { datasetIntegrationMap, integrationsMap, @@ -50,35 +47,42 @@ export function generateDatasets( { datasetIntegrationMap: {}, integrationsMap: {} } ); - const degradedDocs = flattenStats(degradedDocStats); - - if (!dataStreamStats.length) { - return degradedDocs.map((degradedDocStat) => - DataStreamStat.fromDegradedDocStat({ degradedDocStat, datasetIntegrationMap }) - ); - } + const totalDocs = flattenStats(totalDocsStats); + const totalDocsMap: Record = + Object.fromEntries(totalDocs.map(({ dataset, count }) => [dataset, count])); const degradedMap: Record< - DegradedDocsStat['dataset'], + DataStreamDocsStat['dataset'], { - percentage: DegradedDocsStat['percentage']; - count: DegradedDocsStat['count']; - docsCount: DegradedDocsStat['docsCount']; - quality: DegradedDocsStat['quality']; + percentage: number; + count: DataStreamDocsStat['count']; } - > = degradedDocs.reduce( - (degradedMapAcc, { dataset, percentage, count, docsCount }) => + > = degradedDocStats.reduce( + (degradedMapAcc, { dataset, count }) => Object.assign(degradedMapAcc, { [dataset]: { - percentage, count, - docsCount, - quality: mapPercentageToQuality(percentage), + percentage: DataStreamStat.calculatePercentage({ + totalDocs: totalDocsMap[dataset], + count, + }), }, }), {} ); + if (!dataStreamStats.length) { + // We want to pick up all datasets even when they don't have degraded docs + const dataStreams = [...new Set([...Object.keys(totalDocsMap), ...Object.keys(degradedMap)])]; + return dataStreams.map((dataset) => + DataStreamStat.fromDegradedDocStat({ + degradedDocStat: { dataset, ...(degradedMap[dataset] || DEFAULT_DEGRADED_DOCS) }, + datasetIntegrationMap, + totalDocs: totalDocsMap[dataset] ?? 0, + }) + ); + } + return dataStreamStats?.map((dataStream) => { const dataset = DataStreamStat.create(dataStream); @@ -89,6 +93,10 @@ export function generateDatasets( datasetIntegrationMap[dataset.name]?.integration ?? integrationsMap[dataStream.integration ?? ''], degradedDocs: degradedMap[dataset.rawName] || dataset.degradedDocs, + docsInTimeRange: totalDocsMap[dataset.rawName] ?? 0, + quality: mapPercentageToQuality( + (degradedMap[dataset.rawName] || dataset.degradedDocs).percentage + ), }; }); } diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_dataset_aggregated_paginated_results.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_dataset_aggregated_paginated_results.ts new file mode 100644 index 0000000000000..062dcd2f16cf7 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_dataset_aggregated_paginated_results.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from '@kbn/core/server'; +import { rangeQuery } from '@kbn/observability-plugin/server'; +import { QueryDslBoolQuery } from '@elastic/elasticsearch/lib/api/types'; +import { DataStreamDocsStat } from '../../../common/api_types'; +import { createDatasetQualityESClient } from '../../utils'; + +interface Dataset { + type: string; + dataset: string; + namespace: string; +} + +const SIZE_LIMIT = 10000; + +export async function getAggregatedDatasetPaginatedResults(options: { + esClient: ElasticsearchClient; + index: string; + start: number; + end: number; + query?: QueryDslBoolQuery; + after?: Dataset; + prevResults?: DataStreamDocsStat[]; +}): Promise { + const { esClient, index, query, start, end, after, prevResults = [] } = options; + + const datasetQualityESClient = createDatasetQualityESClient(esClient); + + const aggs = (afterKey?: Dataset) => ({ + datasets: { + composite: { + ...(afterKey ? { after: afterKey } : {}), + size: SIZE_LIMIT, + sources: [ + { type: { terms: { field: 'data_stream.type' } } }, + { dataset: { terms: { field: 'data_stream.dataset' } } }, + { namespace: { terms: { field: 'data_stream.namespace' } } }, + ], + }, + }, + }); + + const bool = { + ...query, + filter: [ + ...(query?.filter ? (Array.isArray(query.filter) ? query.filter : [query.filter]) : []), + ...[...rangeQuery(start, end)], + ], + }; + + const response = await datasetQualityESClient.search({ + index, + size: 0, + query: { + bool, + }, + aggs: aggs(after), + }); + + const currResults = + response.aggregations?.datasets.buckets.map((bucket) => ({ + dataset: `${bucket.key.type}-${bucket.key.dataset}-${bucket.key.namespace}`, + count: bucket.doc_count, + })) ?? []; + + const results = [...prevResults, ...currResults]; + + if ( + response.aggregations?.datasets.after_key && + response.aggregations?.datasets.buckets.length === SIZE_LIMIT + ) { + return getAggregatedDatasetPaginatedResults({ + esClient, + index, + start, + end, + after: + (response.aggregations?.datasets.after_key as { + type: string; + dataset: string; + namespace: string; + }) || after, + prevResults: results, + }); + } + + return results; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_docs.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_docs.ts index 454fdb7e1a8b8..48b50c4b8680d 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_docs.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_docs.ts @@ -6,161 +6,37 @@ */ import type { ElasticsearchClient } from '@kbn/core/server'; -import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; -import { DEFAULT_DATASET_TYPE } from '../../../common/constants'; +import { streamPartsToIndexPattern } from '../../../common/utils'; import { DataStreamType } from '../../../common/types'; -import { DegradedDocs } from '../../../common/api_types'; -import { - DATA_STREAM_DATASET, - DATA_STREAM_NAMESPACE, - DATA_STREAM_TYPE, - _IGNORED, -} from '../../../common/es_fields'; -import { createDatasetQualityESClient, wildcardQuery } from '../../utils'; - -interface ResultBucket { - dataset: string; - count: number; -} - -const SIZE_LIMIT = 10000; +import { DataStreamDocsStat } from '../../../common/api_types'; +import { _IGNORED } from '../../../common/es_fields'; +import { getAggregatedDatasetPaginatedResults } from './get_dataset_aggregated_paginated_results'; export async function getDegradedDocsPaginated(options: { esClient: ElasticsearchClient; - type?: DataStreamType; + types: DataStreamType[]; + datasetQuery?: string; start: number; end: number; - datasetQuery?: string; - after?: { - degradedDocs?: { dataset: string; namespace: string }; - docsCount?: { dataset: string; namespace: string }; - }; - prevResults?: { degradedDocs: ResultBucket[]; docsCount: ResultBucket[] }; -}): Promise { - const { +}): Promise { + const { esClient, types, datasetQuery, start, end } = options; + + const datasetNames = datasetQuery + ? [datasetQuery] + : types.map((type) => + streamPartsToIndexPattern({ + typePattern: type, + datasetPattern: '*-*', + }) + ); + + return await getAggregatedDatasetPaginatedResults({ esClient, - type = DEFAULT_DATASET_TYPE, - datasetQuery, start, end, - after, - prevResults = { degradedDocs: [], docsCount: [] }, - } = options; - - const datasetQualityESClient = createDatasetQualityESClient(esClient); - - const datasetFilter = { - ...(datasetQuery - ? { - should: [ - ...wildcardQuery(DATA_STREAM_DATASET, datasetQuery), - ...wildcardQuery(DATA_STREAM_NAMESPACE, datasetQuery), - ], - minimum_should_match: 1, - } - : {}), - }; - - const otherFilters = [...rangeQuery(start, end), ...termQuery(DATA_STREAM_TYPE, type)]; - - const aggs = (afterKey?: { dataset: string; namespace: string }) => ({ - datasets: { - composite: { - ...(afterKey ? { after: afterKey } : {}), - size: SIZE_LIMIT, - sources: [ - { dataset: { terms: { field: 'data_stream.dataset' } } }, - { namespace: { terms: { field: 'data_stream.namespace' } } }, - ], - }, + index: datasetNames.join(','), + query: { + must: { exists: { field: _IGNORED } }, }, }); - - const response = await datasetQualityESClient.msearch({ index: `${type}-*-*` }, [ - // degraded docs per dataset - { - size: 0, - query: { - bool: { - ...datasetFilter, - filter: otherFilters, - must: { exists: { field: _IGNORED } }, - }, - }, - aggs: aggs(after?.degradedDocs), - }, - // total docs per dataset - { - size: 0, - query: { - bool: { - ...datasetFilter, - filter: otherFilters, - }, - }, - aggs: aggs(after?.docsCount), - }, - ]); - const [degradedDocsResponse, totalDocsResponse] = response.responses; - - const currDegradedDocs = - degradedDocsResponse.aggregations?.datasets.buckets.map((bucket) => ({ - dataset: `${type}-${bucket.key.dataset}-${bucket.key.namespace}`, - count: bucket.doc_count, - })) ?? []; - - const degradedDocs = [...prevResults.degradedDocs, ...currDegradedDocs]; - - const currTotalDocs = - totalDocsResponse.aggregations?.datasets.buckets.map((bucket) => ({ - dataset: `${type}-${bucket.key.dataset}-${bucket.key.namespace}`, - count: bucket.doc_count, - })) ?? []; - - const docsCount = [...prevResults.docsCount, ...currTotalDocs]; - - if ( - totalDocsResponse.aggregations?.datasets.after_key && - totalDocsResponse.aggregations?.datasets.buckets.length === SIZE_LIMIT - ) { - return getDegradedDocsPaginated({ - esClient, - type, - start, - end, - datasetQuery, - after: { - degradedDocs: - (degradedDocsResponse.aggregations?.datasets.after_key as { - dataset: string; - namespace: string; - }) || after?.degradedDocs, - docsCount: - (totalDocsResponse.aggregations?.datasets.after_key as { - dataset: string; - namespace: string; - }) || after?.docsCount, - }, - prevResults: { degradedDocs, docsCount }, - }); - } - - const degradedDocsMap = degradedDocs.reduce( - (acc, curr) => ({ - ...acc, - [curr.dataset]: curr.count, - }), - {} - ); - - return docsCount.map((curr) => { - const degradedDocsCount = degradedDocsMap[curr.dataset as keyof typeof degradedDocsMap] || 0; - - return { - ...curr, - docsCount: curr.count, - count: degradedDocsCount, - percentage: (degradedDocsCount / curr.count) * 100, - }; - }); } diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/routes.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/routes.ts index 41ba3ee8c7299..3a60f0b9a8ef3 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/routes.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/routes.ts @@ -10,12 +10,12 @@ import { DataStreamDetails, DataStreamSettings, DataStreamStat, - DegradedDocs, NonAggregatableDatasets, DegradedFieldResponse, DatasetUserPrivileges, DegradedFieldValues, DegradedFieldAnalysis, + DataStreamDocsStat, UpdateFieldLimitResponse, DataStreamRolloverResponse, } from '../../../common/api_types'; @@ -31,6 +31,7 @@ import { getDegradedFields } from './get_degraded_fields'; import { getDegradedFieldValues } from './get_degraded_field_values'; import { analyzeDegradedField } from './get_degraded_field_analysis'; import { getDataStreamsMeteringStats } from './get_data_streams_metering_stats'; +import { getAggregatedDatasetPaginatedResults } from './get_dataset_aggregated_paginated_results'; import { updateFieldLimit } from './update_field_limit'; import { createDatasetQualityESClient } from '../../utils'; @@ -97,7 +98,7 @@ const degradedDocsRoute = createDatasetQualityServerRoute({ params: t.type({ query: t.intersection([ rangeRt, - typeRt, + t.type({ types: typesRt }), t.partial({ datasetQuery: t.string, }), @@ -107,19 +108,13 @@ const degradedDocsRoute = createDatasetQualityServerRoute({ tags: [], }, async handler(resources): Promise<{ - degradedDocs: DegradedDocs[]; + degradedDocs: DataStreamDocsStat[]; }> { const { context, params } = resources; const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asCurrentUser; - await datasetQualityPrivileges.throwIfCannotReadDataset( - esClient, - params.query.type, - params.query.datasetQuery - ); - const degradedDocs = await getDegradedDocsPaginated({ esClient, ...params.query, @@ -131,6 +126,39 @@ const degradedDocsRoute = createDatasetQualityServerRoute({ }, }); +const totalDocsRoute = createDatasetQualityServerRoute({ + endpoint: 'GET /internal/dataset_quality/data_streams/total_docs', + params: t.type({ + query: t.intersection([rangeRt, typeRt]), + }), + options: { + tags: [], + }, + async handler(resources): Promise<{ + totalDocs: DataStreamDocsStat[]; + }> { + const { context, params } = resources; + const coreContext = await context.core; + + const esClient = coreContext.elasticsearch.client.asCurrentUser; + + await datasetQualityPrivileges.throwIfCannotReadDataset(esClient, params.query.type); + + const { type, start, end } = params.query; + + const totalDocs = await getAggregatedDatasetPaginatedResults({ + esClient, + start, + end, + index: `${type}-*-*`, + }); + + return { + totalDocs, + }; + }, +}); + const nonAggregatableDatasetsRoute = createDatasetQualityServerRoute({ endpoint: 'GET /internal/dataset_quality/data_streams/non_aggregatable', params: t.type({ @@ -383,6 +411,7 @@ const rolloverDataStream = createDatasetQualityServerRoute({ export const dataStreamsRouteRepository = { ...statsRoute, ...degradedDocsRoute, + ...totalDocsRoute, ...nonAggregatableDatasetsRoute, ...nonAggregatableDatasetRoute, ...degradedFieldsRoute, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/data_stream_total_docs.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/data_stream_total_docs.ts new file mode 100644 index 0000000000000..c513f3519a30a --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/data_stream_total_docs.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { log, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; + +import { APIClientRequestParamsOf } from '@kbn/dataset-quality-plugin/common/rest'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { RoleCredentials, SupertestWithRoleScopeType } from '../../../services'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const samlAuth = getService('samlAuth'); + const roleScopedSupertest = getService('roleScopedSupertest'); + const synthtrace = getService('logsSynthtraceEsClient'); + const from = '2024-09-20T11:00:00.000Z'; + const to = '2024-09-20T11:01:00.000Z'; + const dataStreamType = 'logs'; + const dataset = 'synth'; + const syntheticsDataset = 'synthetics'; + const namespace = 'default'; + const serviceName = 'my-service'; + const hostName = 'synth-host'; + const dataStreamName = `${dataStreamType}-${dataset}-${namespace}`; + const syntheticsDataStreamName = `${dataStreamType}-${syntheticsDataset}-${namespace}`; + + const endpoint = 'GET /internal/dataset_quality/data_streams/total_docs'; + type ApiParams = APIClientRequestParamsOf['params']['query']; + + async function callApiAs({ + roleScopedSupertestWithCookieCredentials, + apiParams: { type, start, end }, + }: { + roleScopedSupertestWithCookieCredentials: SupertestWithRoleScopeType; + apiParams: ApiParams; + }) { + return roleScopedSupertestWithCookieCredentials + .get(`/internal/dataset_quality/data_streams/total_docs`) + .query({ + type, + start, + end, + }); + } + + describe('DataStream total docs', function () { + let adminRoleAuthc: RoleCredentials; + let supertestAdminWithCookieCredentials: SupertestWithRoleScopeType; + + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + supertestAdminWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'admin', + { + useCookieHeader: true, + withInternalHeaders: true, + } + ); + + await synthtrace.index([ + timerange(from, to) + .interval('1m') + .rate(1) + .generator((timestamp) => [ + log + .create() + .message('This is a log message') + .timestamp(timestamp) + .dataset(dataset) + .namespace(namespace) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'host.name': hostName, + }), + log + .create() + .message('This is a log message') + .timestamp(timestamp) + .dataset(syntheticsDataset) + .namespace(namespace) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'host.name': hostName, + }), + ]), + ]); + }); + + after(async () => { + await synthtrace.clean(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('returns number of documents per DataStream', async () => { + const resp = await callApiAs({ + roleScopedSupertestWithCookieCredentials: supertestAdminWithCookieCredentials, + apiParams: { + type: dataStreamType, + start: from, + end: to, + }, + }); + + expect(resp.body.totalDocs.length).to.be(2); + expect(resp.body.totalDocs[0].dataset).to.be(dataStreamName); + expect(resp.body.totalDocs[0].count).to.be(1); + expect(resp.body.totalDocs[1].dataset).to.be(syntheticsDataStreamName); + expect(resp.body.totalDocs[1].count).to.be(1); + }); + + it('returns empty when all documents are outside timeRange', async () => { + const resp = await callApiAs({ + roleScopedSupertestWithCookieCredentials: supertestAdminWithCookieCredentials, + apiParams: { + type: dataStreamType, + start: '2024-09-21T11:00:00.000Z', + end: '2024-09-21T11:01:00.000Z', + }, + }); + + expect(resp.body.totalDocs.length).to.be(0); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/index.ts index 7e555b7a310e1..28133d6c8e613 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('./data_stream_settings')); loadTestFile(require.resolve('./data_stream_rollover')); loadTestFile(require.resolve('./update_field_limit')); + loadTestFile(require.resolve('./data_stream_total_docs')); }); } diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_docs.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_docs.spec.ts index 92aa69610a66d..60aeef1af9c93 100644 --- a/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_docs.spec.ts +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_docs.spec.ts @@ -7,8 +7,7 @@ import { log, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; -import { DatasetQualityApiError } from '../../common/dataset_quality_api_supertest'; -import { expectToReject } from '../../utils'; +import rison from '@kbn/rison'; import { DatasetQualityApiClientKey } from '../../common/config'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -24,7 +23,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { endpoint: 'GET /internal/dataset_quality/data_streams/degraded_docs', params: { query: { - type: 'logs', + types: rison.encodeArray(['logs']), start, end, }, @@ -33,13 +32,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { } registry.when('Degraded docs', { config: 'basic' }, () => { - describe('authorization', () => { - it('should return a 403 when the user does not have sufficient privileges', async () => { - const err = await expectToReject(() => callApiAs('noAccessUser')); - expect(err.res.status).to.be(403); - }); - }); - describe('and there are log documents', () => { before(async () => { await synthtrace.index([ @@ -75,25 +67,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns stats correctly', async () => { const stats = await callApiAs('datasetQualityMonitorUser'); - expect(stats.body.degradedDocs.length).to.be(2); + expect(stats.body.degradedDocs.length).to.be(1); const degradedDocsStats = stats.body.degradedDocs.reduce( (acc, curr) => ({ ...acc, [curr.dataset]: { - percentage: curr.percentage, count: curr.count, }, }), - {} as Record + {} as Record ); - expect(degradedDocsStats['logs-synth.1-default']).to.eql({ - percentage: 0, - count: 0, - }); expect(degradedDocsStats['logs-synth.2-default']).to.eql({ - percentage: 100, count: 1, }); }); @@ -155,117 +141,45 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns counts and list of datasets correctly', async () => { const stats = await callApiAs('datasetQualityMonitorUser'); - expect(stats.body.degradedDocs.length).to.be(18); + expect(stats.body.degradedDocs.length).to.be(9); const expected = { degradedDocs: [ - { - dataset: 'logs-apache.access-default', - count: 0, - docsCount: 1, - percentage: 0, - }, - { - dataset: 'logs-apache.access-space1', - count: 0, - docsCount: 1, - percentage: 0, - }, - { - dataset: 'logs-apache.access-space2', - count: 0, - docsCount: 1, - percentage: 0, - }, { dataset: 'logs-apache.error-default', count: 1, - docsCount: 2, - percentage: 50, }, { dataset: 'logs-apache.error-space1', count: 1, - docsCount: 2, - percentage: 50, }, { dataset: 'logs-apache.error-space2', count: 1, - docsCount: 2, - percentage: 50, - }, - { - dataset: 'logs-mysql.access-default', - count: 0, - docsCount: 1, - percentage: 0, - }, - { - dataset: 'logs-mysql.access-space1', - count: 0, - docsCount: 1, - percentage: 0, - }, - { - dataset: 'logs-mysql.access-space2', - count: 0, - docsCount: 1, - percentage: 0, }, { dataset: 'logs-mysql.error-default', count: 1, - docsCount: 2, - percentage: 50, }, { dataset: 'logs-mysql.error-space1', count: 1, - docsCount: 2, - percentage: 50, }, { dataset: 'logs-mysql.error-space2', count: 1, - docsCount: 2, - percentage: 50, - }, - { - dataset: 'logs-nginx.access-default', - count: 0, - docsCount: 1, - percentage: 0, - }, - { - dataset: 'logs-nginx.access-space1', - count: 0, - docsCount: 1, - percentage: 0, - }, - { - dataset: 'logs-nginx.access-space2', - count: 0, - docsCount: 1, - percentage: 0, }, { dataset: 'logs-nginx.error-default', count: 1, - docsCount: 2, - percentage: 50, }, { dataset: 'logs-nginx.error-space1', count: 1, - docsCount: 2, - percentage: 50, }, { dataset: 'logs-nginx.error-space2', count: 1, - docsCount: 2, - percentage: 50, }, ], }; diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/total_docs.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/total_docs.spec.ts new file mode 100644 index 0000000000000..71442e1300a2b --- /dev/null +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/total_docs.spec.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 expect from '@kbn/expect'; +import { DatasetQualityApiClientKey } from '../../common/config'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { expectToReject } from '../../utils'; +import { DatasetQualityApiError } from '../../common/dataset_quality_api_supertest'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const datasetQualityApiClient = getService('datasetQualityApiClient'); + const start = '2023-12-11T18:00:00.000Z'; + const end = '2023-12-11T18:01:00.000Z'; + + async function callApiAs(user: DatasetQualityApiClientKey) { + return await datasetQualityApiClient[user]({ + endpoint: 'GET /internal/dataset_quality/data_streams/total_docs', + params: { + query: { + type: 'logs', + start, + end, + }, + }, + }); + } + + registry.when('Total docs', { config: 'basic' }, () => { + describe('authorization', () => { + it('should return a 403 when the user does not have sufficient privileges', async () => { + const err = await expectToReject(() => callApiAs('noAccessUser')); + expect(err.res.status).to.be(403); + }); + }); + }); +} From daa1b3c82941718557fc5dcf55b36428c7a31212 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 23:33:54 +1100 Subject: [PATCH 17/47] [8.x] [Response Ops][Connectors] Refactor Jira Connector to use latest API only (#197787) (#199289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Response Ops][Connectors] Refactor Jira Connector to use latest API only (#197787)](https://github.com/elastic/kibana/pull/197787) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Julian Gernun <17549662+jcger@users.noreply.github.com> --- .../connectors/action-types/jira.asciidoc | 4 +- .../actions/server/lib/axios_utils.test.ts | 8 + .../plugins/actions/server/lib/axios_utils.ts | 10 + .../connector_types/jira/service.test.ts | 667 +++++------------- .../server/connector_types/jira/service.ts | 130 +--- .../server/connector_types/jira/types.ts | 1 - .../server/jira_simulation.ts | 39 +- 7 files changed, 236 insertions(+), 623 deletions(-) diff --git a/docs/management/connectors/action-types/jira.asciidoc b/docs/management/connectors/action-types/jira.asciidoc index 906a2945d82de..2111de7a77ce6 100644 --- a/docs/management/connectors/action-types/jira.asciidoc +++ b/docs/management/connectors/action-types/jira.asciidoc @@ -14,7 +14,7 @@ The Jira connector uses the https://developer.atlassian.com/cloud/jira/platform/ [[jira-compatibility]] === Compatibility -Jira on-premise deployments (Server and Data Center) are not supported. +Jira Cloud and Jira Data Center are supported. Jira on-premise deployments are not supported. [float] [[define-jira-ui]] @@ -37,7 +37,7 @@ Name:: The name of the connector. URL:: Jira instance URL. Project key:: Jira project key. Email:: The account email for HTTP Basic authentication. -API token:: Jira API authentication token for HTTP Basic authentication. +API token:: Jira API authentication token for HTTP Basic authentication. For Jira Data Center, this value should be the password associated with the email owner. [float] [[jira-action-configuration]] diff --git a/x-pack/plugins/actions/server/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/lib/axios_utils.test.ts index bee09a90ed27b..b7bb7548b9052 100644 --- a/x-pack/plugins/actions/server/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/lib/axios_utils.test.ts @@ -577,4 +577,12 @@ describe('throwIfResponseIsNotValid', () => { }) ).not.toThrow(); }); + + test('it does NOT throw if HTTP status code is 204 even if the content type is not supported', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, status: 204, headers: { ['content-type']: 'text/html' } }, + }) + ).not.toThrow(); + }); }); diff --git a/x-pack/plugins/actions/server/lib/axios_utils.ts b/x-pack/plugins/actions/server/lib/axios_utils.ts index 254ad1a36f6e2..78abebf48022f 100644 --- a/x-pack/plugins/actions/server/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/lib/axios_utils.ts @@ -137,6 +137,16 @@ export const throwIfResponseIsNotValid = ({ const requiredContentType = 'application/json'; const contentType = res.headers['content-type'] ?? 'undefined'; const data = res.data; + const statusCode = res.status; + + /** + * Some external services may return a 204 + * status code but with unsupported content type like text/html. + * To avoid throwing on valid requests we return. + */ + if (statusCode === 204) { + return; + } /** * Check that the content-type of the response is application/json. diff --git a/x-pack/plugins/stack_connectors/server/connector_types/jira/service.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/jira/service.test.ts index 5e98bdc96c0ee..0001d7cf13284 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/jira/service.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/jira/service.test.ts @@ -36,67 +36,14 @@ const configurationUtilities = actionsConfigMock.create(); const issueTypesResponse = createAxiosResponse({ data: { - projects: [ + issueTypes: [ { - issuetypes: [ - { - id: '10006', - name: 'Task', - }, - { - id: '10007', - name: 'Bug', - }, - ], + id: '10006', + name: 'Task', }, - ], - }, -}); - -const fieldsResponse = createAxiosResponse({ - data: { - projects: [ { - issuetypes: [ - { - id: '10006', - name: 'Task', - fields: { - summary: { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - priority: { - required: false, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { - name: 'Highest', - id: '1', - }, - { - name: 'High', - id: '2', - }, - { - name: 'Medium', - id: '3', - }, - { - name: 'Low', - id: '4', - }, - { - name: 'Lowest', - id: '5', - }, - ], - defaultValue: { - name: 'Medium', - id: '3', - }, - }, - }, - }, - ], + id: '10007', + name: 'Bug', }, ], }, @@ -110,30 +57,6 @@ const issueResponse = { const issuesResponse = [issueResponse]; -const mockNewAPI = () => - requestMock.mockImplementationOnce(() => - createAxiosResponse({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://coolsite.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': 'https://coolsite.net/rest/capabilities/list-issuetype-fields', - }, - }, - }) - ); - -const mockOldAPI = () => - requestMock.mockImplementationOnce(() => - createAxiosResponse({ - data: { - capabilities: { - navigation: 'https://coolsite.net/rest/capabilities/navigation', - }, - }, - }) - ); - describe('Jira service', () => { let service: ExternalService; let connectorUsageCollector: ConnectorUsageCollector; @@ -347,23 +270,6 @@ describe('Jira service', () => { }); test('it creates the incident correctly without issue type', async () => { - /* The response from Jira when creating an issue contains only the key and the id. - The function makes the following calls when creating an issue: - 1. Get issueTypes to set a default ONLY when incident.issueType is missing - 2. Create the issue. - 3. Get the created issue with all the necessary fields. - */ - // getIssueType mocks - requestMock.mockImplementationOnce(() => - createAxiosResponse({ - data: { - capabilities: { - navigation: 'https://coolsite.net/rest/capabilities/navigation', - }, - }, - }) - ); - // getIssueType mocks requestMock.mockImplementationOnce(() => issueTypesResponse); @@ -419,16 +325,6 @@ describe('Jira service', () => { }); test('removes newline characters and trialing spaces from summary', async () => { - requestMock.mockImplementationOnce(() => - createAxiosResponse({ - data: { - capabilities: { - navigation: 'https://coolsite.net/rest/capabilities/navigation', - }, - }, - }) - ); - // getIssueType mocks requestMock.mockImplementationOnce(() => issueTypesResponse); @@ -800,28 +696,47 @@ describe('Jira service', () => { }); }); - describe('getCapabilities', () => { - test('it should return the capabilities', async () => { - mockOldAPI(); - const res = await service.getCapabilities(); - expect(res).toEqual({ - capabilities: { - navigation: 'https://coolsite.net/rest/capabilities/navigation', + describe('getIssueTypes', () => { + test('it should return the issue types', async () => { + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + issueTypes: issueTypesResponse.data.issueTypes, + }, + }) + ); + + const res = await service.getIssueTypes(); + + expect(res).toEqual([ + { + id: '10006', + name: 'Task', }, - }); + { + id: '10007', + name: 'Bug', + }, + ]); }); test('it should call request with correct arguments', async () => { - mockOldAPI(); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + issueTypes: issueTypesResponse.data.issueTypes, + }, + }) + ); - await service.getCapabilities(); + await service.getIssueTypes(); - expect(requestMock).toHaveBeenCalledWith({ + expect(requestMock).toHaveBeenLastCalledWith({ axios, logger, method: 'get', configurationUtilities, - url: 'https://coolsite.net/rest/capabilities', + url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes', connectorUsageCollector, }); }); @@ -829,25 +744,12 @@ describe('Jira service', () => { test('it should throw an error', async () => { requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); - error.response = { data: { errors: { capabilities: 'Could not get capabilities' } } }; - throw error; - }); - - await expect(service.getCapabilities()).rejects.toThrow( - '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities' - ); - }); - - test('it should return unknown if the error is a string', async () => { - requestMock.mockImplementation(() => { - const error = new Error('An error has occurred'); - // @ts-ignore - error.response = { data: 'Unauthorized' }; + error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; throw error; }); - await expect(service.getCapabilities()).rejects.toThrow( - '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: unknown: errorResponse.errors was null' + await expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' ); }); @@ -856,346 +758,178 @@ describe('Jira service', () => { createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) ); - await expect(service.getCapabilities()).rejects.toThrow( - '[Action][Jira]: Unable to get capabilities. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' - ); - }); - - test('it should throw if the required attributes are not there', async () => { - requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); - - await expect(service.getCapabilities()).rejects.toThrow( - '[Action][Jira]: Unable to get capabilities. Error: Response is missing at least one of the expected fields: capabilities. Reason: unknown: errorResponse was null' + await expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' ); }); - }); - - describe('getIssueTypes', () => { - describe('Old API', () => { - test('it should return the issue types', async () => { - mockOldAPI(); - - requestMock.mockImplementationOnce(() => issueTypesResponse); - const res = await service.getIssueTypes(); - - expect(res).toEqual([ - { - id: '10006', - name: 'Task', - }, - { - id: '10007', - name: 'Bug', + test('it should work with data center response - issueTypes returned in data.values', async () => { + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: issueTypesResponse.data.issueTypes, }, - ]); - }); - - test('it should call request with correct arguments', async () => { - mockOldAPI(); - - requestMock.mockImplementationOnce(() => issueTypesResponse); - - await service.getIssueTypes(); - - expect(requestMock).toHaveBeenLastCalledWith({ - axios, - logger, - method: 'get', - configurationUtilities, - url: 'https://coolsite.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields', - connectorUsageCollector, - }); - }); - - test('it should throw an error', async () => { - mockOldAPI(); - - requestMock.mockImplementation(() => { - const error: ResponseError = new Error('An error has occurred'); - error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; - throw error; - }); - - await expect(service.getIssueTypes()).rejects.toThrow( - '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' - ); - }); - - test('it should throw if the request is not a JSON', async () => { - mockOldAPI(); + }) + ); - requestMock.mockImplementation(() => - createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) - ); + await service.getIssueTypes(); - await expect(service.getIssueTypes()).rejects.toThrow( - '[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' - ); + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + configurationUtilities, + url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes', + connectorUsageCollector, }); }); - describe('New API', () => { - test('it should return the issue types', async () => { - mockNewAPI(); - - requestMock.mockImplementationOnce(() => - createAxiosResponse({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - }) - ); - - const res = await service.getIssueTypes(); + }); - expect(res).toEqual([ - { - id: '10006', - name: 'Task', - }, - { - id: '10007', - name: 'Bug', + describe('getFieldsByIssueType', () => { + test('it should return the fields', async () => { + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + fields: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { + required: false, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + ], }, - ]); - }); - - test('it should call request with correct arguments', async () => { - mockNewAPI(); - - requestMock.mockImplementationOnce(() => - createAxiosResponse({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - }) - ); - - await service.getIssueTypes(); - - expect(requestMock).toHaveBeenLastCalledWith({ - axios, - logger, - method: 'get', - configurationUtilities, - url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes', - connectorUsageCollector, - }); - }); - - test('it should throw an error', async () => { - mockNewAPI(); - - requestMock.mockImplementation(() => { - const error: ResponseError = new Error('An error has occurred'); - error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; - throw error; - }); - - await expect(service.getIssueTypes()).rejects.toThrow( - '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' - ); - }); - - test('it should throw if the request is not a JSON', async () => { - mockNewAPI(); + }) + ); - requestMock.mockImplementation(() => - createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) - ); + const res = await service.getFieldsByIssueType('10006'); - await expect(service.getIssueTypes()).rejects.toThrow( - '[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' - ); + expect(res).toEqual({ + priority: { + required: false, + schema: { type: 'string' }, + allowedValues: [{ id: '3', name: 'Medium' }], + defaultValue: { id: '3', name: 'Medium' }, + }, + summary: { + required: true, + schema: { type: 'string' }, + allowedValues: [], + defaultValue: {}, + }, }); }); - }); - describe('getFieldsByIssueType', () => { - describe('Old API', () => { - test('it should return the fields', async () => { - mockOldAPI(); - - requestMock.mockImplementationOnce(() => fieldsResponse); - - const res = await service.getFieldsByIssueType('10006'); - - expect(res).toEqual({ - priority: { - required: false, - schema: { type: 'string' }, - allowedValues: [ - { id: '1', name: 'Highest' }, - { id: '2', name: 'High' }, - { id: '3', name: 'Medium' }, - { id: '4', name: 'Low' }, - { id: '5', name: 'Lowest' }, + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + fields: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { + required: true, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, ], - defaultValue: { id: '3', name: 'Medium' }, - }, - summary: { - required: true, - schema: { type: 'string' }, - allowedValues: [], - defaultValue: {}, }, - }); - }); - - test('it should call request with correct arguments', async () => { - mockOldAPI(); - - requestMock.mockImplementationOnce(() => fieldsResponse); + }) + ); - await service.getFieldsByIssueType('10006'); + await service.getFieldsByIssueType('10006'); - expect(requestMock).toHaveBeenLastCalledWith({ - axios, - logger, - method: 'get', - configurationUtilities, - url: 'https://coolsite.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields', - connectorUsageCollector, - }); + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + configurationUtilities, + url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10006', }); + }); - test('it should throw an error', async () => { - mockOldAPI(); - - requestMock.mockImplementation(() => { - const error: ResponseError = new Error('An error has occurred'); - error.response = { data: { errors: { fields: 'Could not get fields' } } }; - throw error; - }); - - await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( - '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields' - ); + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; + throw error; }); - test('it should throw if the request is not a JSON', async () => { - mockOldAPI(); + await expect(service.getFieldsByIssueType('10006')).rejects.toThrowError( + '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types' + ); + }); - requestMock.mockImplementation(() => - createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) - ); + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); - await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( - '[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' - ); - }); + await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); }); - describe('New API', () => { - test('it should return the fields', async () => { - mockNewAPI(); - - requestMock.mockImplementationOnce(() => - createAxiosResponse({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { - required: false, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { - name: 'Medium', - id: '3', - }, - ], - defaultValue: { + test('it should work with data center response - issueTypes returned in data.values', async () => { + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { + required: false, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { name: 'Medium', id: '3', }, + ], + defaultValue: { + name: 'Medium', + id: '3', }, - ], - }, - }) - ); - - const res = await service.getFieldsByIssueType('10006'); - - expect(res).toEqual({ - priority: { - required: false, - schema: { type: 'string' }, - allowedValues: [{ id: '3', name: 'Medium' }], - defaultValue: { id: '3', name: 'Medium' }, - }, - summary: { - required: true, - schema: { type: 'string' }, - allowedValues: [], - defaultValue: {}, + }, + ], }, - }); - }); - - test('it should call request with correct arguments', async () => { - mockNewAPI(); - - requestMock.mockImplementationOnce(() => - createAxiosResponse({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { - required: true, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { - name: 'Medium', - id: '3', - }, - ], - defaultValue: { - name: 'Medium', - id: '3', - }, - }, - ], - }, - }) - ); - - await service.getFieldsByIssueType('10006'); - - expect(requestMock).toHaveBeenLastCalledWith({ - axios, - logger, - method: 'get', - configurationUtilities, - url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10006', - }); - }); - - test('it should throw an error', async () => { - mockNewAPI(); - - requestMock.mockImplementation(() => { - const error: ResponseError = new Error('An error has occurred'); - error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; - throw error; - }); - - await expect(service.getFieldsByIssueType('10006')).rejects.toThrowError( - '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types' - ); - }); - - test('it should throw if the request is not a JSON', async () => { - mockNewAPI(); + }) + ); - requestMock.mockImplementation(() => - createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) - ); + const res = await service.getFieldsByIssueType('10006'); - await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( - '[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' - ); + expect(res).toEqual({ + priority: { + required: false, + schema: { type: 'string' }, + allowedValues: [{ id: '3', name: 'Medium' }], + defaultValue: { id: '3', name: 'Medium' }, + }, + summary: { + required: true, + schema: { type: 'string' }, + allowedValues: [], + defaultValue: {}, + }, }); }); }); @@ -1403,50 +1137,14 @@ describe('Jira service', () => { .mockImplementationOnce(() => createAxiosResponse({ data: { - capabilities: { - 'list-project-issuetypes': - 'https://coolsite.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://coolsite.net/rest/capabilities/list-issuetype-fields', - }, - }, - }) - ) - .mockImplementationOnce(() => - createAxiosResponse({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - }) - ) - .mockImplementationOnce(() => - createAxiosResponse({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://coolsite.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://coolsite.net/rest/capabilities/list-issuetype-fields', - }, - }, - }) - ) - .mockImplementationOnce(() => - createAxiosResponse({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://coolsite.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://coolsite.net/rest/capabilities/list-issuetype-fields', - }, + issueTypes: issueTypesResponse.data.issueTypes, }, }) ) .mockImplementationOnce(() => createAxiosResponse({ data: { - values: [ + fields: [ { required: true, schema: { type: 'string' }, fieldId: 'summary' }, { required: true, schema: { type: 'string' }, fieldId: 'description' }, { @@ -1471,7 +1169,7 @@ describe('Jira service', () => { .mockImplementationOnce(() => createAxiosResponse({ data: { - values: [ + fields: [ { required: true, schema: { type: 'string' }, fieldId: 'summary' }, { required: true, schema: { type: 'string' }, fieldId: 'description' }, ], @@ -1488,10 +1186,7 @@ describe('Jira service', () => { callMocks(); await service.getFields(); const callUrls = [ - 'https://coolsite.net/rest/capabilities', 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes', - 'https://coolsite.net/rest/capabilities', - 'https://coolsite.net/rest/capabilities', 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10006', 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10007', ]; @@ -1525,7 +1220,7 @@ describe('Jira service', () => { throw error; }); await expect(service.getFields()).rejects.toThrow( - '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Required field' + '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Required field' ); }); }); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/jira/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/jira/service.ts index 064667558b37e..f8929ce67b68a 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/jira/service.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/jira/service.ts @@ -39,12 +39,9 @@ import * as i18n from './translations'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; -const CAPABILITIES_URL = `rest/capabilities`; const VIEW_INCIDENT_URL = `browse`; -const createMetaCapabilities = ['list-project-issuetypes', 'list-issuetype-fields']; - export const createExternalService = ( { config, secrets }: ExternalServiceCredentials, logger: Logger, @@ -60,10 +57,7 @@ export const createExternalService = ( const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/issue`; - const capabilitiesUrl = `${urlWithoutTrailingSlash}/${CAPABILITIES_URL}`; const commentUrl = `${incidentUrl}/{issueId}/comment`; - const getIssueTypesOldAPIURL = `${urlWithoutTrailingSlash}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; - const getIssueTypeFieldsOldAPIURL = `${urlWithoutTrailingSlash}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`; const getIssueTypesUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`; const getIssueTypeFieldsUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes/{issueTypeId}`; const searchUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/search`; @@ -144,9 +138,6 @@ export const createExternalService = ( }, ''); }; - const hasSupportForNewAPI = (capabilities: { capabilities?: {} }) => - createMetaCapabilities.every((c) => Object.keys(capabilities?.capabilities ?? {}).includes(c)); - const normalizeIssueTypes = (issueTypes: Array<{ id: string; name: string }>) => issueTypes.map((type) => ({ id: type.id, name: type.name })); @@ -356,12 +347,12 @@ export const createExternalService = ( } }; - const getCapabilities = async () => { + const getIssueTypes = async () => { try { const res = await request({ axios: axiosInstance, method: 'get', - url: capabilitiesUrl, + url: getIssueTypesUrl, logger, configurationUtilities, connectorUsageCollector, @@ -369,59 +360,11 @@ export const createExternalService = ( throwIfResponseIsNotValid({ res, - requiredAttributesToBeInTheResponse: ['capabilities'], }); - return { ...res.data }; - } catch (error) { - throw new Error( - getErrorMessage( - i18n.NAME, - `Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage( - error.response?.data - )}` - ) - ); - } - }; - - const getIssueTypes = async () => { - const capabilitiesResponse = await getCapabilities(); - const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); - try { - if (!supportsNewAPI) { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: getIssueTypesOldAPIURL, - logger, - configurationUtilities, - connectorUsageCollector, - }); - - throwIfResponseIsNotValid({ - res, - }); - - const issueTypes = res.data.projects[0]?.issuetypes ?? []; - return normalizeIssueTypes(issueTypes); - } else { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: getIssueTypesUrl, - logger, - configurationUtilities, - connectorUsageCollector, - }); - - throwIfResponseIsNotValid({ - res, - }); - - const issueTypes = res.data.values; - return normalizeIssueTypes(issueTypes); - } + // Cloud returns issueTypes and Data Center returns values + const { issueTypes, values } = res.data; + return normalizeIssueTypes(issueTypes || values); } catch (error) { throw new Error( getErrorMessage( @@ -435,47 +378,29 @@ export const createExternalService = ( }; const getFieldsByIssueType = async (issueTypeId: string) => { - const capabilitiesResponse = await getCapabilities(); - const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); try { - if (!supportsNewAPI) { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId), - logger, - configurationUtilities, - connectorUsageCollector, - }); - - throwIfResponseIsNotValid({ - res, - }); - - const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; - return normalizeFields(fields); - } else { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), - logger, - configurationUtilities, - }); - - throwIfResponseIsNotValid({ - res, - }); - - const fields = res.data.values.reduce( - (acc: { [x: string]: {} }, value: { fieldId: string }) => ({ - ...acc, - [value.fieldId]: { ...value }, - }), - {} - ); - return normalizeFields(fields); - } + const res = await request({ + axios: axiosInstance, + method: 'get', + url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), + logger, + configurationUtilities, + }); + + throwIfResponseIsNotValid({ + res, + }); + + // Cloud returns fields and Data Center returns values + const { fields: rawFields, values } = res.data; + const fields = (rawFields || values).reduce( + (acc: { [x: string]: {} }, value: { fieldId: string }) => ({ + ...acc, + [value.fieldId]: { ...value }, + }), + {} + ); + return normalizeFields(fields); } catch (error) { throw new Error( getErrorMessage( @@ -580,7 +505,6 @@ export const createExternalService = ( createIncident, updateIncident, createComment, - getCapabilities, getIssueTypes, getFieldsByIssueType, getIssues, diff --git a/x-pack/plugins/stack_connectors/server/connector_types/jira/types.ts b/x-pack/plugins/stack_connectors/server/connector_types/jira/types.ts index c975e23b1b783..755726137e412 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/jira/types.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/jira/types.ts @@ -101,7 +101,6 @@ export interface ExternalService { createComment: (params: CreateCommentParams) => Promise; createIncident: (params: CreateIncidentParams) => Promise; getFields: () => Promise; - getCapabilities: () => Promise; getFieldsByIssueType: (issueTypeId: string) => Promise; getIncident: (id: string) => Promise; getIssue: (id: string) => Promise; diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/jira_simulation.ts index 31ae7f1507ca8..ee559ca91b9ec 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/jira_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/jira_simulation.ts @@ -100,7 +100,7 @@ export function initPlugin(router: IRouter, path: string) { router.get( { - path: `${path}/rest/capabilities`, + path: `${path}/rest/api/2/issue/createmeta/{projectId}/issuetypes`, options: { authRequired: false, }, @@ -112,37 +112,14 @@ export function initPlugin(router: IRouter, path: string) { res: KibanaResponseFactory ): Promise> { return jsonResponse(res, 200, { - capabilities: {}, - }); - } - ); - - router.get( - { - path: `${path}/rest/api/2/issue/createmeta`, - options: { - authRequired: false, - }, - validate: {}, - }, - async function ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> { - return jsonResponse(res, 200, { - projects: [ + issueTypes: [ + { + id: '10006', + name: 'Task', + }, { - issuetypes: [ - { - id: '10006', - name: 'Task', - }, - { - id: '10007', - name: 'Sub-task', - }, - ], + id: '10007', + name: 'Sub-task', }, ], }); From c3bf0b4c9432b553c6bdaa7b82b869160ea4e1d9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Nov 2024 23:51:17 +1100 Subject: [PATCH 18/47] [8.x] Use getUrlPartsWithStrippedDefaultPort instead of getUrlParts (#199264) (#199288) # Backport This will backport the following commits from `main` to `8.x`: - [Use getUrlPartsWithStrippedDefaultPort instead of getUrlParts (#199264)](https://github.com/elastic/kibana/pull/199264) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Maryam Saeidi --- .../alerting/custom_threshold/custom_eq_avg_bytes_fired.ts | 2 +- .../alerting/custom_threshold/documents_count_fired.ts | 2 +- .../observability/alerting/custom_threshold/group_by_fired.ts | 2 +- .../observability/alerting/custom_threshold/p99_pct_fired.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/custom_eq_avg_bytes_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/custom_eq_avg_bytes_fired.ts index 4163c85c849f4..5eb9f4cc21738 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/custom_eq_avg_bytes_fired.ts @@ -249,7 +249,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { docCountTarget: 1, }); - const { protocol, hostname, port } = kbnTestConfig.getUrlParts(); + const { protocol, hostname, port } = kbnTestConfig.getUrlPartsWithStrippedDefaultPort(); expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.custom_threshold'); expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql( `${protocol}://${hostname}${port ? `:${port}` : ''}/app/observability/alerts/${alertId}` diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/documents_count_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/documents_count_fired.ts index f9d7067f0e0a4..97451b1930afc 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/documents_count_fired.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/documents_count_fired.ts @@ -247,7 +247,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { docCountTarget: 1, }); - const { protocol, hostname, port } = kbnTestConfig.getUrlParts(); + const { protocol, hostname, port } = kbnTestConfig.getUrlPartsWithStrippedDefaultPort(); expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.custom_threshold'); expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql( diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/group_by_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/group_by_fired.ts index 3a554a16c5cec..457b8bdceae44 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/group_by_fired.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/group_by_fired.ts @@ -272,7 +272,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const resp = await alertingApi.waitForDocumentInIndex({ indexName: ALERT_ACTION_INDEX, }); - const { protocol, hostname, port } = kbnTestConfig.getUrlParts(); + const { protocol, hostname, port } = kbnTestConfig.getUrlPartsWithStrippedDefaultPort(); expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.custom_threshold'); expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql( diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/p99_pct_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/p99_pct_fired.ts index 743bca2683895..5d6cfb49f86f0 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/p99_pct_fired.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/p99_pct_fired.ts @@ -244,7 +244,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { docCountTarget: 1, }); - const { protocol, hostname, port } = kbnTestConfig.getUrlParts(); + const { protocol, hostname, port } = kbnTestConfig.getUrlPartsWithStrippedDefaultPort(); expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.custom_threshold'); expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql( `${protocol}://${hostname}${port ? `:${port}` : ''}/app/observability/alerts/${alertId}` From ed448ddac642145e8dcf678a42418b76ba3c326b Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 7 Nov 2024 07:20:06 -0600 Subject: [PATCH 19/47] Remove codeowners --- .github/CODEOWNERS | 2022 -------------------------------------------- 1 file changed, 2022 deletions(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index dda83bdf40615..0000000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,2022 +0,0 @@ -#### -## Everything at the top of the codeowners file is auto generated based on the -## "owner" fields in the kibana.jsonc files at the root of each package. This -## file is automatically updated by CI or can be updated locally by running -## `node scripts/generate codeowners`. -#### - -x-pack/test/alerting_api_integration/common/plugins/aad @elastic/response-ops -x-pack/plugins/actions @elastic/response-ops -x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops -packages/kbn-actions-types @elastic/response-ops -src/plugins/advanced_settings @elastic/appex-sharedux @elastic/kibana-management -x-pack/packages/kbn-ai-assistant @elastic/search-kibana -x-pack/packages/kbn-ai-assistant-common @elastic/search-kibana -src/plugins/ai_assistant_management/selection @elastic/obs-knowledge-team -x-pack/packages/ml/aiops_change_point_detection @elastic/ml-ui -x-pack/packages/ml/aiops_common @elastic/ml-ui -x-pack/packages/ml/aiops_components @elastic/ml-ui -x-pack/packages/ml/aiops_log_pattern_analysis @elastic/ml-ui -x-pack/packages/ml/aiops_log_rate_analysis @elastic/ml-ui -x-pack/plugins/aiops @elastic/ml-ui -x-pack/packages/ml/aiops_test_utils @elastic/ml-ui -x-pack/test/alerting_api_integration/packages/helpers @elastic/response-ops -x-pack/test/alerting_api_integration/common/plugins/alerts @elastic/response-ops -x-pack/packages/kbn-alerting-comparators @elastic/response-ops -x-pack/examples/alerting_example @elastic/response-ops -x-pack/test/functional_with_es_ssl/plugins/alerts @elastic/response-ops -x-pack/plugins/alerting @elastic/response-ops -x-pack/packages/kbn-alerting-state-types @elastic/response-ops -packages/kbn-alerting-types @elastic/response-ops -packages/kbn-alerts-as-data-utils @elastic/response-ops -packages/kbn-alerts-grouping @elastic/response-ops -x-pack/test/alerting_api_integration/common/plugins/alerts_restricted @elastic/response-ops -packages/kbn-alerts-ui-shared @elastic/response-ops -packages/kbn-ambient-common-types @elastic/kibana-operations -packages/kbn-ambient-ftr-types @elastic/kibana-operations @elastic/appex-qa -packages/kbn-ambient-storybook-types @elastic/kibana-operations -packages/kbn-ambient-ui-types @elastic/kibana-operations -packages/kbn-analytics @elastic/kibana-core -packages/analytics/utils/analytics_collection_utils @elastic/kibana-core -test/analytics/plugins/analytics_ftr_helpers @elastic/kibana-core -test/analytics/plugins/analytics_plugin_a @elastic/kibana-core -packages/kbn-apm-config-loader @elastic/kibana-core @vigneshshanmugam -x-pack/plugins/observability_solution/apm_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team -packages/kbn-apm-data-view @elastic/obs-ux-infra_services-team -x-pack/plugins/observability_solution/apm/ftr_e2e @elastic/obs-ux-infra_services-team -x-pack/plugins/observability_solution/apm @elastic/obs-ux-infra_services-team -packages/kbn-apm-synthtrace @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team -packages/kbn-apm-synthtrace-client @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team -packages/kbn-apm-types @elastic/obs-ux-infra_services-team -packages/kbn-apm-utils @elastic/obs-ux-infra_services-team -test/plugin_functional/plugins/app_link_test @elastic/kibana-core -x-pack/test/usage_collection/plugins/application_usage_test @elastic/kibana-core -x-pack/test/security_api_integration/plugins/audit_log @elastic/kibana-security -packages/kbn-avc-banner @elastic/security-defend-workflows -packages/kbn-axe-config @elastic/kibana-qa -packages/kbn-babel-preset @elastic/kibana-operations -packages/kbn-babel-register @elastic/kibana-operations -packages/kbn-babel-transform @elastic/kibana-operations -x-pack/plugins/banners @elastic/appex-sharedux -packages/kbn-bazel-runner @elastic/kibana-operations -packages/kbn-bfetch-error @elastic/appex-sharedux -examples/bfetch_explorer @elastic/appex-sharedux -src/plugins/bfetch @elastic/appex-sharedux -packages/kbn-calculate-auto @elastic/obs-ux-management-team -packages/kbn-calculate-width-from-char-count @elastic/kibana-visualizations -x-pack/plugins/canvas @elastic/kibana-presentation -packages/kbn-capture-oas-snapshot-cli @elastic/kibana-core -x-pack/test/cases_api_integration/common/plugins/cases @elastic/response-ops -packages/kbn-cases-components @elastic/response-ops -x-pack/plugins/cases @elastic/response-ops -packages/kbn-cbor @elastic/kibana-operations -packages/kbn-cell-actions @elastic/security-threat-hunting-explore -src/plugins/chart_expressions/common @elastic/kibana-visualizations -packages/kbn-chart-icons @elastic/kibana-visualizations -src/plugins/charts @elastic/kibana-visualizations -packages/kbn-check-mappings-update-cli @elastic/kibana-core -packages/kbn-check-prod-native-modules-cli @elastic/kibana-operations -packages/kbn-ci-stats-core @elastic/kibana-operations -packages/kbn-ci-stats-performance-metrics @elastic/kibana-operations -packages/kbn-ci-stats-reporter @elastic/kibana-operations -packages/kbn-ci-stats-shipper-cli @elastic/kibana-operations -packages/kbn-cli-dev-mode @elastic/kibana-operations -packages/cloud @elastic/kibana-core -x-pack/plugins/cloud_integrations/cloud_chat @elastic/kibana-core -x-pack/plugins/cloud_integrations/cloud_data_migration @elastic/kibana-management -x-pack/plugins/cloud_defend @elastic/kibana-cloud-security-posture -x-pack/plugins/cloud_integrations/cloud_experiments @elastic/kibana-core -x-pack/plugins/cloud_integrations/cloud_full_story @elastic/kibana-core -x-pack/test/cloud_integration/plugins/saml_provider @elastic/kibana-core -x-pack/plugins/cloud_integrations/cloud_links @elastic/kibana-core -x-pack/plugins/cloud @elastic/kibana-core -x-pack/packages/kbn-cloud-security-posture/public @elastic/kibana-cloud-security-posture -x-pack/packages/kbn-cloud-security-posture/common @elastic/kibana-cloud-security-posture -x-pack/packages/kbn-cloud-security-posture/graph @elastic/kibana-cloud-security-posture -x-pack/plugins/cloud_security_posture @elastic/kibana-cloud-security-posture -packages/shared-ux/code_editor/impl @elastic/appex-sharedux -packages/shared-ux/code_editor/mocks @elastic/appex-sharedux -packages/kbn-code-owners @elastic/appex-qa -packages/kbn-coloring @elastic/kibana-visualizations -packages/kbn-config @elastic/kibana-core -packages/kbn-config-mocks @elastic/kibana-core -packages/kbn-config-schema @elastic/kibana-core -src/plugins/console @elastic/kibana-management -packages/content-management/content_editor @elastic/appex-sharedux -packages/content-management/content_insights/content_insights_public @elastic/appex-sharedux -packages/content-management/content_insights/content_insights_server @elastic/appex-sharedux -examples/content_management_examples @elastic/appex-sharedux -packages/content-management/favorites/favorites_public @elastic/appex-sharedux -packages/content-management/favorites/favorites_server @elastic/appex-sharedux -src/plugins/content_management @elastic/appex-sharedux -packages/content-management/tabbed_table_list_view @elastic/appex-sharedux -packages/content-management/table_list_view @elastic/appex-sharedux -packages/content-management/table_list_view_common @elastic/appex-sharedux -packages/content-management/table_list_view_table @elastic/appex-sharedux -packages/content-management/user_profiles @elastic/appex-sharedux -packages/kbn-content-management-utils @elastic/kibana-data-discovery -examples/controls_example @elastic/kibana-presentation -src/plugins/controls @elastic/kibana-presentation -src/core @elastic/kibana-core -packages/core/analytics/core-analytics-browser @elastic/kibana-core -packages/core/analytics/core-analytics-browser-internal @elastic/kibana-core -packages/core/analytics/core-analytics-browser-mocks @elastic/kibana-core -packages/core/analytics/core-analytics-server @elastic/kibana-core -packages/core/analytics/core-analytics-server-internal @elastic/kibana-core -packages/core/analytics/core-analytics-server-mocks @elastic/kibana-core -test/plugin_functional/plugins/core_app_status @elastic/kibana-core -packages/core/application/core-application-browser @elastic/kibana-core -packages/core/application/core-application-browser-internal @elastic/kibana-core -packages/core/application/core-application-browser-mocks @elastic/kibana-core -packages/core/application/core-application-common @elastic/kibana-core -packages/core/apps/core-apps-browser-internal @elastic/kibana-core -packages/core/apps/core-apps-browser-mocks @elastic/kibana-core -packages/core/apps/core-apps-server-internal @elastic/kibana-core -packages/core/base/core-base-browser-internal @elastic/kibana-core -packages/core/base/core-base-browser-mocks @elastic/kibana-core -packages/core/base/core-base-common @elastic/kibana-core -packages/core/base/core-base-common-internal @elastic/kibana-core -packages/core/base/core-base-server-internal @elastic/kibana-core -packages/core/base/core-base-server-mocks @elastic/kibana-core -packages/core/capabilities/core-capabilities-browser-internal @elastic/kibana-core -packages/core/capabilities/core-capabilities-browser-mocks @elastic/kibana-core -packages/core/capabilities/core-capabilities-common @elastic/kibana-core -packages/core/capabilities/core-capabilities-server @elastic/kibana-core -packages/core/capabilities/core-capabilities-server-internal @elastic/kibana-core -packages/core/capabilities/core-capabilities-server-mocks @elastic/kibana-core -packages/core/chrome/core-chrome-browser @elastic/appex-sharedux -packages/core/chrome/core-chrome-browser-internal @elastic/appex-sharedux -packages/core/chrome/core-chrome-browser-mocks @elastic/appex-sharedux -packages/core/config/core-config-server-internal @elastic/kibana-core -packages/core/custom-branding/core-custom-branding-browser @elastic/appex-sharedux -packages/core/custom-branding/core-custom-branding-browser-internal @elastic/appex-sharedux -packages/core/custom-branding/core-custom-branding-browser-mocks @elastic/appex-sharedux -packages/core/custom-branding/core-custom-branding-common @elastic/appex-sharedux -packages/core/custom-branding/core-custom-branding-server @elastic/appex-sharedux -packages/core/custom-branding/core-custom-branding-server-internal @elastic/appex-sharedux -packages/core/custom-branding/core-custom-branding-server-mocks @elastic/appex-sharedux -packages/core/deprecations/core-deprecations-browser @elastic/kibana-core -packages/core/deprecations/core-deprecations-browser-internal @elastic/kibana-core -packages/core/deprecations/core-deprecations-browser-mocks @elastic/kibana-core -packages/core/deprecations/core-deprecations-common @elastic/kibana-core -packages/core/deprecations/core-deprecations-server @elastic/kibana-core -packages/core/deprecations/core-deprecations-server-internal @elastic/kibana-core -packages/core/deprecations/core-deprecations-server-mocks @elastic/kibana-core -packages/core/doc-links/core-doc-links-browser @elastic/kibana-core -packages/core/doc-links/core-doc-links-browser-internal @elastic/kibana-core -packages/core/doc-links/core-doc-links-browser-mocks @elastic/kibana-core -packages/core/doc-links/core-doc-links-server @elastic/kibana-core -packages/core/doc-links/core-doc-links-server-internal @elastic/kibana-core -packages/core/doc-links/core-doc-links-server-mocks @elastic/kibana-core -packages/core/elasticsearch/core-elasticsearch-client-server-internal @elastic/kibana-core -packages/core/elasticsearch/core-elasticsearch-client-server-mocks @elastic/kibana-core -packages/core/elasticsearch/core-elasticsearch-server @elastic/kibana-core -packages/core/elasticsearch/core-elasticsearch-server-internal @elastic/kibana-core -packages/core/elasticsearch/core-elasticsearch-server-mocks @elastic/kibana-core -packages/core/environment/core-environment-server-internal @elastic/kibana-core -packages/core/environment/core-environment-server-mocks @elastic/kibana-core -packages/core/execution-context/core-execution-context-browser @elastic/kibana-core -packages/core/execution-context/core-execution-context-browser-internal @elastic/kibana-core -packages/core/execution-context/core-execution-context-browser-mocks @elastic/kibana-core -packages/core/execution-context/core-execution-context-common @elastic/kibana-core -packages/core/execution-context/core-execution-context-server @elastic/kibana-core -packages/core/execution-context/core-execution-context-server-internal @elastic/kibana-core -packages/core/execution-context/core-execution-context-server-mocks @elastic/kibana-core -packages/core/fatal-errors/core-fatal-errors-browser @elastic/kibana-core -packages/core/fatal-errors/core-fatal-errors-browser-internal @elastic/kibana-core -packages/core/fatal-errors/core-fatal-errors-browser-mocks @elastic/kibana-core -packages/core/feature-flags/core-feature-flags-browser @elastic/kibana-core -packages/core/feature-flags/core-feature-flags-browser-internal @elastic/kibana-core -packages/core/feature-flags/core-feature-flags-browser-mocks @elastic/kibana-core -packages/core/feature-flags/core-feature-flags-server @elastic/kibana-core -packages/core/feature-flags/core-feature-flags-server-internal @elastic/kibana-core -packages/core/feature-flags/core-feature-flags-server-mocks @elastic/kibana-core -test/plugin_functional/plugins/core_history_block @elastic/kibana-core -packages/core/http/core-http-browser @elastic/kibana-core -packages/core/http/core-http-browser-internal @elastic/kibana-core -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 -test/plugin_functional/plugins/core_http @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-resources-server @elastic/kibana-core -packages/core/http/core-http-resources-server-internal @elastic/kibana-core -packages/core/http/core-http-resources-server-mocks @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 -packages/core/http/core-http-server-internal @elastic/kibana-core -packages/core/http/core-http-server-mocks @elastic/kibana-core -packages/core/i18n/core-i18n-browser @elastic/kibana-core -packages/core/i18n/core-i18n-browser-internal @elastic/kibana-core -packages/core/i18n/core-i18n-browser-mocks @elastic/kibana-core -packages/core/i18n/core-i18n-server @elastic/kibana-core -packages/core/i18n/core-i18n-server-internal @elastic/kibana-core -packages/core/i18n/core-i18n-server-mocks @elastic/kibana-core -packages/core/injected-metadata/core-injected-metadata-browser-internal @elastic/kibana-core -packages/core/injected-metadata/core-injected-metadata-browser-mocks @elastic/kibana-core -packages/core/injected-metadata/core-injected-metadata-common-internal @elastic/kibana-core -packages/core/integrations/core-integrations-browser-internal @elastic/kibana-core -packages/core/integrations/core-integrations-browser-mocks @elastic/kibana-core -packages/core/lifecycle/core-lifecycle-browser @elastic/kibana-core -packages/core/lifecycle/core-lifecycle-browser-internal @elastic/kibana-core -packages/core/lifecycle/core-lifecycle-browser-mocks @elastic/kibana-core -packages/core/lifecycle/core-lifecycle-server @elastic/kibana-core -packages/core/lifecycle/core-lifecycle-server-internal @elastic/kibana-core -packages/core/lifecycle/core-lifecycle-server-mocks @elastic/kibana-core -packages/core/logging/core-logging-browser-internal @elastic/kibana-core -packages/core/logging/core-logging-browser-mocks @elastic/kibana-core -packages/core/logging/core-logging-common-internal @elastic/kibana-core -packages/core/logging/core-logging-server @elastic/kibana-core -packages/core/logging/core-logging-server-internal @elastic/kibana-core -packages/core/logging/core-logging-server-mocks @elastic/kibana-core -packages/core/metrics/core-metrics-collectors-server-internal @elastic/kibana-core -packages/core/metrics/core-metrics-collectors-server-mocks @elastic/kibana-core -packages/core/metrics/core-metrics-server @elastic/kibana-core -packages/core/metrics/core-metrics-server-internal @elastic/kibana-core -packages/core/metrics/core-metrics-server-mocks @elastic/kibana-core -packages/core/mount-utils/core-mount-utils-browser @elastic/kibana-core -packages/core/mount-utils/core-mount-utils-browser-internal @elastic/kibana-core -packages/core/node/core-node-server @elastic/kibana-core -packages/core/node/core-node-server-internal @elastic/kibana-core -packages/core/node/core-node-server-mocks @elastic/kibana-core -packages/core/notifications/core-notifications-browser @elastic/kibana-core -packages/core/notifications/core-notifications-browser-internal @elastic/kibana-core -packages/core/notifications/core-notifications-browser-mocks @elastic/kibana-core -packages/core/overlays/core-overlays-browser @elastic/kibana-core -packages/core/overlays/core-overlays-browser-internal @elastic/kibana-core -packages/core/overlays/core-overlays-browser-mocks @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_a @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_appleave @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_b @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_chromeless @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_deep_links @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_deprecations @elastic/kibana-core -test/plugin_functional/plugins/core_dynamic_resolving_a @elastic/kibana-core -test/plugin_functional/plugins/core_dynamic_resolving_b @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_execution_context @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_helpmenu @elastic/kibana-core -test/node_roles_functional/plugins/core_plugin_initializer_context @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_route_timeouts @elastic/kibana-core -test/plugin_functional/plugins/core_plugin_static_assets @elastic/kibana-core -packages/core/plugins/core-plugins-base-server-internal @elastic/kibana-core -packages/core/plugins/core-plugins-browser @elastic/kibana-core -packages/core/plugins/core-plugins-browser-internal @elastic/kibana-core -packages/core/plugins/core-plugins-browser-mocks @elastic/kibana-core -packages/core/plugins/core-plugins-contracts-browser @elastic/kibana-core -packages/core/plugins/core-plugins-contracts-server @elastic/kibana-core -packages/core/plugins/core-plugins-server @elastic/kibana-core -packages/core/plugins/core-plugins-server-internal @elastic/kibana-core -packages/core/plugins/core-plugins-server-mocks @elastic/kibana-core -packages/core/preboot/core-preboot-server @elastic/kibana-core -packages/core/preboot/core-preboot-server-internal @elastic/kibana-core -packages/core/preboot/core-preboot-server-mocks @elastic/kibana-core -test/plugin_functional/plugins/core_provider_plugin @elastic/kibana-core -packages/core/rendering/core-rendering-browser-internal @elastic/kibana-core -packages/core/rendering/core-rendering-browser-mocks @elastic/kibana-core -packages/core/rendering/core-rendering-server-internal @elastic/kibana-core -packages/core/rendering/core-rendering-server-mocks @elastic/kibana-core -packages/core/root/core-root-browser-internal @elastic/kibana-core -packages/core/root/core-root-server-internal @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-api-browser @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-api-server @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-api-server-internal @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-api-server-mocks @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-base-server-internal @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-base-server-mocks @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-browser @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-browser-internal @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-browser-mocks @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-common @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-import-export-server-internal @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-import-export-server-mocks @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-migration-server-internal @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-migration-server-mocks @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-server @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-server-internal @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-server-mocks @elastic/kibana-core -packages/core/saved-objects/core-saved-objects-utils-server @elastic/kibana-core -packages/core/security/core-security-browser @elastic/kibana-core -packages/core/security/core-security-browser-internal @elastic/kibana-core -packages/core/security/core-security-browser-mocks @elastic/kibana-core -packages/core/security/core-security-common @elastic/kibana-core @elastic/kibana-security -packages/core/security/core-security-server @elastic/kibana-core -packages/core/security/core-security-server-internal @elastic/kibana-core -packages/core/security/core-security-server-mocks @elastic/kibana-core -packages/core/status/core-status-common @elastic/kibana-core -packages/core/status/core-status-common-internal @elastic/kibana-core -packages/core/status/core-status-server @elastic/kibana-core -packages/core/status/core-status-server-internal @elastic/kibana-core -packages/core/status/core-status-server-mocks @elastic/kibana-core -packages/core/test-helpers/core-test-helpers-deprecations-getters @elastic/kibana-core -packages/core/test-helpers/core-test-helpers-http-setup-browser @elastic/kibana-core -packages/core/test-helpers/core-test-helpers-kbn-server @elastic/kibana-core -packages/core/test-helpers/core-test-helpers-model-versions @elastic/kibana-core -packages/core/test-helpers/core-test-helpers-so-type-serializer @elastic/kibana-core -packages/core/test-helpers/core-test-helpers-test-utils @elastic/kibana-core -packages/core/theme/core-theme-browser @elastic/kibana-core -packages/core/theme/core-theme-browser-internal @elastic/kibana-core -packages/core/theme/core-theme-browser-mocks @elastic/kibana-core -packages/core/ui-settings/core-ui-settings-browser @elastic/appex-sharedux -packages/core/ui-settings/core-ui-settings-browser-internal @elastic/appex-sharedux -packages/core/ui-settings/core-ui-settings-browser-mocks @elastic/appex-sharedux -packages/core/ui-settings/core-ui-settings-common @elastic/appex-sharedux -packages/core/ui-settings/core-ui-settings-server @elastic/appex-sharedux -packages/core/ui-settings/core-ui-settings-server-internal @elastic/appex-sharedux -packages/core/ui-settings/core-ui-settings-server-mocks @elastic/appex-sharedux -packages/core/usage-data/core-usage-data-base-server-internal @elastic/kibana-core -packages/core/usage-data/core-usage-data-server @elastic/kibana-core -packages/core/usage-data/core-usage-data-server-internal @elastic/kibana-core -packages/core/usage-data/core-usage-data-server-mocks @elastic/kibana-core -packages/core/user-profile/core-user-profile-browser @elastic/kibana-core -packages/core/user-profile/core-user-profile-browser-internal @elastic/kibana-core -packages/core/user-profile/core-user-profile-browser-mocks @elastic/kibana-core -packages/core/user-profile/core-user-profile-common @elastic/kibana-core -packages/core/user-profile/core-user-profile-server @elastic/kibana-core -packages/core/user-profile/core-user-profile-server-internal @elastic/kibana-core -packages/core/user-profile/core-user-profile-server-mocks @elastic/kibana-core -packages/core/user-settings/core-user-settings-server @elastic/kibana-security -packages/core/user-settings/core-user-settings-server-internal @elastic/kibana-security -packages/core/user-settings/core-user-settings-server-mocks @elastic/kibana-security -x-pack/plugins/cross_cluster_replication @elastic/kibana-management -packages/kbn-crypto @elastic/kibana-security -packages/kbn-crypto-browser @elastic/kibana-core -x-pack/plugins/custom_branding @elastic/appex-sharedux -packages/kbn-custom-icons @elastic/obs-ux-logs-team -packages/kbn-custom-integrations @elastic/obs-ux-logs-team -src/plugins/custom_integrations @elastic/fleet -packages/kbn-cypress-config @elastic/kibana-operations -x-pack/plugins/dashboard_enhanced @elastic/kibana-presentation -src/plugins/dashboard @elastic/kibana-presentation -x-pack/packages/kbn-data-forge @elastic/obs-ux-management-team -src/plugins/data @elastic/kibana-visualizations @elastic/kibana-data-discovery -x-pack/plugins/data_quality @elastic/obs-ux-logs-team -test/plugin_functional/plugins/data_search @elastic/kibana-data-discovery -packages/kbn-data-service @elastic/kibana-visualizations @elastic/kibana-data-discovery -packages/kbn-data-stream-adapter @elastic/security-threat-hunting-explore -x-pack/plugins/data_usage @elastic/obs-ai-assistant @elastic/security-solution -src/plugins/data_view_editor @elastic/kibana-data-discovery -examples/data_view_field_editor_example @elastic/kibana-data-discovery -src/plugins/data_view_field_editor @elastic/kibana-data-discovery -src/plugins/data_view_management @elastic/kibana-data-discovery -packages/kbn-data-view-utils @elastic/kibana-data-discovery -src/plugins/data_views @elastic/kibana-data-discovery -x-pack/plugins/data_visualizer @elastic/ml-ui -x-pack/plugins/observability_solution/dataset_quality @elastic/obs-ux-logs-team -packages/kbn-datemath @elastic/kibana-data-discovery -packages/deeplinks/analytics @elastic/kibana-data-discovery @elastic/kibana-presentation @elastic/kibana-visualizations -packages/deeplinks/devtools @elastic/kibana-management -packages/deeplinks/fleet @elastic/fleet -packages/deeplinks/management @elastic/kibana-management -packages/deeplinks/ml @elastic/ml-ui -packages/deeplinks/observability @elastic/obs-ux-management-team -packages/deeplinks/search @elastic/search-kibana -packages/deeplinks/security @elastic/security-solution -packages/deeplinks/shared @elastic/appex-sharedux -packages/default-nav/analytics @elastic/kibana-data-discovery @elastic/kibana-presentation @elastic/kibana-visualizations -packages/default-nav/devtools @elastic/kibana-management -packages/default-nav/management @elastic/kibana-management -packages/default-nav/ml @elastic/ml-ui -packages/kbn-dev-cli-errors @elastic/kibana-operations -packages/kbn-dev-cli-runner @elastic/kibana-operations -packages/kbn-dev-proc-runner @elastic/kibana-operations -src/plugins/dev_tools @elastic/kibana-management -packages/kbn-dev-utils @elastic/kibana-operations -examples/developer_examples @elastic/appex-sharedux -packages/kbn-discover-contextual-components @elastic/obs-ux-logs-team @elastic/kibana-data-discovery -examples/discover_customization_examples @elastic/kibana-data-discovery -x-pack/plugins/discover_enhanced @elastic/kibana-data-discovery -src/plugins/discover @elastic/kibana-data-discovery -src/plugins/discover_shared @elastic/kibana-data-discovery @elastic/obs-ux-logs-team -packages/kbn-discover-utils @elastic/kibana-data-discovery -packages/kbn-doc-links @elastic/docs -packages/kbn-docs-utils @elastic/kibana-operations -packages/kbn-dom-drag-drop @elastic/kibana-visualizations @elastic/kibana-data-discovery -packages/kbn-ebt-tools @elastic/kibana-core -x-pack/packages/security-solution/ecs_data_quality_dashboard @elastic/security-threat-hunting-explore -x-pack/plugins/ecs_data_quality_dashboard @elastic/security-threat-hunting-explore -packages/kbn-elastic-agent-utils @elastic/obs-ux-logs-team -x-pack/packages/kbn-elastic-assistant @elastic/security-generative-ai -x-pack/packages/kbn-elastic-assistant-common @elastic/security-generative-ai -x-pack/plugins/elastic_assistant @elastic/security-generative-ai -test/plugin_functional/plugins/elasticsearch_client_plugin @elastic/kibana-core -x-pack/test/plugin_api_integration/plugins/elasticsearch_client @elastic/kibana-core -x-pack/plugins/embeddable_enhanced @elastic/kibana-presentation -examples/embeddable_examples @elastic/kibana-presentation -src/plugins/embeddable @elastic/kibana-presentation -x-pack/examples/embedded_lens_example @elastic/kibana-visualizations -x-pack/plugins/encrypted_saved_objects @elastic/kibana-security -x-pack/plugins/enterprise_search @elastic/search-kibana -x-pack/plugins/observability_solution/entities_data_access @elastic/obs-entities -x-pack/packages/kbn-entities-schema @elastic/obs-entities -x-pack/test/api_integration/apis/entity_manager/fixture_plugin @elastic/obs-entities -x-pack/plugins/entity_manager @elastic/obs-entities -examples/error_boundary @elastic/appex-sharedux -packages/kbn-es @elastic/kibana-operations -packages/kbn-es-archiver @elastic/kibana-operations @elastic/appex-qa -packages/kbn-es-errors @elastic/kibana-core -packages/kbn-es-query @elastic/kibana-data-discovery -packages/kbn-es-types @elastic/kibana-core @elastic/obs-knowledge-team -src/plugins/es_ui_shared @elastic/kibana-management -packages/kbn-eslint-config @elastic/kibana-operations -packages/kbn-eslint-plugin-disable @elastic/kibana-operations -packages/kbn-eslint-plugin-eslint @elastic/kibana-operations -packages/kbn-eslint-plugin-i18n @elastic/obs-knowledge-team @elastic/kibana-operations -packages/kbn-eslint-plugin-imports @elastic/kibana-operations -packages/kbn-eslint-plugin-telemetry @elastic/obs-knowledge-team -examples/eso_model_version_example @elastic/kibana-security -x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security -src/plugins/esql @elastic/kibana-esql -packages/kbn-esql-ast @elastic/kibana-esql -examples/esql_ast_inspector @elastic/kibana-esql -src/plugins/esql_datagrid @elastic/kibana-esql -packages/kbn-esql-editor @elastic/kibana-esql -packages/kbn-esql-utils @elastic/kibana-esql -packages/kbn-esql-validation-autocomplete @elastic/kibana-esql -examples/esql_validation_example @elastic/kibana-esql -test/plugin_functional/plugins/eui_provider_dev_warning @elastic/appex-sharedux -packages/kbn-event-annotation-common @elastic/kibana-visualizations -packages/kbn-event-annotation-components @elastic/kibana-visualizations -src/plugins/event_annotation_listing @elastic/kibana-visualizations -src/plugins/event_annotation @elastic/kibana-visualizations -x-pack/test/plugin_api_integration/plugins/event_log @elastic/response-ops -x-pack/plugins/event_log @elastic/response-ops -packages/kbn-expandable-flyout @elastic/security-threat-hunting-investigations -packages/kbn-expect @elastic/kibana-operations @elastic/appex-qa -x-pack/examples/exploratory_view_example @elastic/obs-ux-infra_services-team -x-pack/plugins/observability_solution/exploratory_view @elastic/obs-ux-management-team -src/plugins/expression_error @elastic/kibana-presentation -src/plugins/chart_expressions/expression_gauge @elastic/kibana-visualizations -src/plugins/chart_expressions/expression_heatmap @elastic/kibana-visualizations -src/plugins/expression_image @elastic/kibana-presentation -src/plugins/chart_expressions/expression_legacy_metric @elastic/kibana-visualizations -src/plugins/expression_metric @elastic/kibana-presentation -src/plugins/chart_expressions/expression_metric @elastic/kibana-visualizations -src/plugins/chart_expressions/expression_partition_vis @elastic/kibana-visualizations -src/plugins/expression_repeat_image @elastic/kibana-presentation -src/plugins/expression_reveal_image @elastic/kibana-presentation -src/plugins/expression_shape @elastic/kibana-presentation -src/plugins/chart_expressions/expression_tagcloud @elastic/kibana-visualizations -src/plugins/chart_expressions/expression_xy @elastic/kibana-visualizations -examples/expressions_explorer @elastic/kibana-visualizations -src/plugins/expressions @elastic/kibana-visualizations -packages/kbn-failed-test-reporter-cli @elastic/kibana-operations @elastic/appex-qa -examples/feature_control_examples @elastic/kibana-security -examples/feature_flags_example @elastic/kibana-core -x-pack/test/plugin_api_integration/plugins/feature_usage_test @elastic/kibana-security -x-pack/plugins/features @elastic/kibana-core -x-pack/test/security_api_integration/plugins/features_provider @elastic/kibana-security -x-pack/test/functional_execution_context/plugins/alerts @elastic/kibana-core -examples/field_formats_example @elastic/kibana-data-discovery -src/plugins/field_formats @elastic/kibana-data-discovery -packages/kbn-field-types @elastic/kibana-data-discovery -packages/kbn-field-utils @elastic/kibana-data-discovery -x-pack/plugins/fields_metadata @elastic/obs-ux-logs-team -x-pack/plugins/file_upload @elastic/kibana-gis @elastic/ml-ui -examples/files_example @elastic/appex-sharedux -src/plugins/files_management @elastic/appex-sharedux -src/plugins/files @elastic/appex-sharedux -packages/kbn-find-used-node-modules @elastic/kibana-operations -x-pack/plugins/fleet @elastic/fleet -packages/kbn-flot-charts @elastic/kibana-operations -x-pack/test/ui_capabilities/common/plugins/foo_plugin @elastic/kibana-security -packages/kbn-formatters @elastic/obs-ux-logs-team -src/plugins/ftr_apis @elastic/kibana-core -packages/kbn-ftr-common-functional-services @elastic/kibana-operations @elastic/appex-qa -packages/kbn-ftr-common-functional-ui-services @elastic/appex-qa -packages/kbn-ftr-screenshot-filename @elastic/kibana-operations @elastic/appex-qa -x-pack/test/functional_with_es_ssl/plugins/cases @elastic/response-ops -x-pack/examples/gen_ai_streaming_response_example @elastic/response-ops -packages/kbn-generate @elastic/kibana-operations -packages/kbn-generate-console-definitions @elastic/kibana-management -packages/kbn-generate-csv @elastic/appex-sharedux -packages/kbn-get-repo-files @elastic/kibana-operations -x-pack/plugins/global_search_bar @elastic/appex-sharedux -x-pack/plugins/global_search @elastic/appex-sharedux -x-pack/plugins/global_search_providers @elastic/appex-sharedux -x-pack/test/plugin_functional/plugins/global_search_test @elastic/kibana-core -x-pack/plugins/graph @elastic/kibana-visualizations -examples/grid_example @elastic/kibana-presentation -packages/kbn-grid-layout @elastic/kibana-presentation -x-pack/plugins/grokdebugger @elastic/kibana-management -packages/kbn-grouping @elastic/response-ops -packages/kbn-guided-onboarding @elastic/appex-sharedux -examples/guided_onboarding_example @elastic/appex-sharedux -src/plugins/guided_onboarding @elastic/appex-sharedux -packages/kbn-handlebars @elastic/kibana-security -packages/kbn-hapi-mocks @elastic/kibana-core -test/plugin_functional/plugins/hardening @elastic/kibana-security -packages/kbn-health-gateway-server @elastic/kibana-core -examples/hello_world @elastic/kibana-core -src/plugins/home @elastic/kibana-core -packages/home/sample_data_card @elastic/appex-sharedux -packages/home/sample_data_tab @elastic/appex-sharedux -packages/home/sample_data_types @elastic/appex-sharedux -packages/kbn-i18n @elastic/kibana-core -packages/kbn-i18n-react @elastic/kibana-core -x-pack/test/functional_embedded/plugins/iframe_embedded @elastic/kibana-core -src/plugins/image_embeddable @elastic/appex-sharedux -packages/kbn-import-locator @elastic/kibana-operations -packages/kbn-import-resolver @elastic/kibana-operations -x-pack/plugins/index_lifecycle_management @elastic/kibana-management -x-pack/plugins/index_management @elastic/kibana-management -x-pack/packages/index-management/index_management_shared_types @elastic/kibana-management -test/plugin_functional/plugins/index_patterns @elastic/kibana-data-discovery -x-pack/packages/ml/inference_integration_flyout @elastic/ml-ui -x-pack/packages/ai-infra/inference-common @elastic/appex-ai-infra -x-pack/plugins/inference @elastic/appex-ai-infra -x-pack/packages/kbn-infra-forge @elastic/obs-ux-management-team -x-pack/plugins/observability_solution/infra @elastic/obs-ux-logs-team @elastic/obs-ux-infra_services-team -x-pack/plugins/ingest_pipelines @elastic/kibana-management -src/plugins/input_control_vis @elastic/kibana-presentation -src/plugins/inspector @elastic/kibana-presentation -x-pack/plugins/integration_assistant @elastic/security-scalability -src/plugins/interactive_setup @elastic/kibana-security -test/interactive_setup_api_integration/plugins/test_endpoints @elastic/kibana-security -packages/kbn-interpreter @elastic/kibana-visualizations -x-pack/plugins/observability_solution/inventory/e2e @elastic/obs-ux-infra_services-team -x-pack/plugins/observability_solution/inventory @elastic/obs-ux-infra_services-team -x-pack/plugins/observability_solution/investigate_app @elastic/obs-ux-management-team -x-pack/plugins/observability_solution/investigate @elastic/obs-ux-management-team -packages/kbn-investigation-shared @elastic/obs-ux-management-team -packages/kbn-io-ts-utils @elastic/obs-knowledge-team -packages/kbn-ipynb @elastic/search-kibana -packages/kbn-jest-serializers @elastic/kibana-operations -packages/kbn-journeys @elastic/kibana-operations @elastic/appex-qa -packages/kbn-json-ast @elastic/kibana-operations -x-pack/packages/ml/json_schemas @elastic/ml-ui -test/health_gateway/plugins/status @elastic/kibana-core -test/plugin_functional/plugins/kbn_sample_panel_action @elastic/appex-sharedux -test/plugin_functional/plugins/kbn_top_nav @elastic/kibana-core -test/plugin_functional/plugins/kbn_tp_custom_visualizations @elastic/kibana-visualizations -test/interpreter_functional/plugins/kbn_tp_run_pipeline @elastic/kibana-core -x-pack/test/functional_cors/plugins/kibana_cors_test @elastic/kibana-security -packages/kbn-kibana-manifest-schema @elastic/kibana-operations -src/plugins/kibana_overview @elastic/appex-sharedux -src/plugins/kibana_react @elastic/appex-sharedux -src/plugins/kibana_usage_collection @elastic/kibana-core -src/plugins/kibana_utils @elastic/appex-sharedux -x-pack/plugins/kubernetes_security @elastic/kibana-cloud-security-posture -x-pack/packages/kbn-langchain @elastic/security-generative-ai -packages/kbn-language-documentation @elastic/kibana-esql -x-pack/examples/lens_config_builder_example @elastic/kibana-visualizations -packages/kbn-lens-embeddable-utils @elastic/obs-ux-infra_services-team @elastic/kibana-visualizations -packages/kbn-lens-formula-docs @elastic/kibana-visualizations -x-pack/examples/lens_embeddable_inline_editing_example @elastic/kibana-visualizations -x-pack/plugins/lens @elastic/kibana-visualizations -x-pack/plugins/license_api_guard @elastic/kibana-management -x-pack/plugins/license_management @elastic/kibana-management -x-pack/plugins/licensing @elastic/kibana-core -src/plugins/links @elastic/kibana-presentation -packages/kbn-lint-packages-cli @elastic/kibana-operations -packages/kbn-lint-ts-projects-cli @elastic/kibana-operations -x-pack/plugins/lists @elastic/security-detection-engine -examples/locator_examples @elastic/appex-sharedux -examples/locator_explorer @elastic/appex-sharedux -packages/kbn-logging @elastic/kibana-core -packages/kbn-logging-mocks @elastic/kibana-core -x-pack/plugins/observability_solution/logs_data_access @elastic/obs-knowledge-team @elastic/obs-ux-logs-team -x-pack/plugins/observability_solution/logs_explorer @elastic/obs-ux-logs-team -x-pack/plugins/observability_solution/logs_shared @elastic/obs-ux-logs-team -x-pack/plugins/logstash @elastic/logstash -packages/kbn-managed-content-badge @elastic/kibana-visualizations -packages/kbn-managed-vscode-config @elastic/kibana-operations -packages/kbn-managed-vscode-config-cli @elastic/kibana-operations -packages/kbn-management/cards_navigation @elastic/kibana-management -src/plugins/management @elastic/kibana-management -packages/kbn-management/settings/application @elastic/kibana-management -packages/kbn-management/settings/components/field_category @elastic/kibana-management -packages/kbn-management/settings/components/field_input @elastic/kibana-management -packages/kbn-management/settings/components/field_row @elastic/kibana-management -packages/kbn-management/settings/components/form @elastic/kibana-management -packages/kbn-management/settings/field_definition @elastic/kibana-management -packages/kbn-management/settings/setting_ids @elastic/appex-sharedux @elastic/kibana-management -packages/kbn-management/settings/section_registry @elastic/appex-sharedux @elastic/kibana-management -packages/kbn-management/settings/types @elastic/kibana-management -packages/kbn-management/settings/utilities @elastic/kibana-management -packages/kbn-management/storybook/config @elastic/kibana-management -test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management -packages/kbn-manifest @elastic/kibana-core -packages/kbn-mapbox-gl @elastic/kibana-gis -x-pack/examples/third_party_maps_source_example @elastic/kibana-gis -src/plugins/maps_ems @elastic/kibana-gis -x-pack/plugins/maps @elastic/kibana-gis -x-pack/packages/maps/vector_tile_utils @elastic/kibana-gis -x-pack/plugins/observability_solution/metrics_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team -x-pack/packages/ml/agg_utils @elastic/ml-ui -x-pack/packages/ml/anomaly_utils @elastic/ml-ui -x-pack/packages/ml/cancellable_search @elastic/ml-ui -x-pack/packages/ml/category_validator @elastic/ml-ui -x-pack/packages/ml/chi2test @elastic/ml-ui -x-pack/packages/ml/creation_wizard_utils @elastic/ml-ui -x-pack/packages/ml/data_frame_analytics_utils @elastic/ml-ui -x-pack/packages/ml/data_grid @elastic/ml-ui -x-pack/packages/ml/data_view_utils @elastic/ml-ui -x-pack/packages/ml/date_picker @elastic/ml-ui -x-pack/packages/ml/date_utils @elastic/ml-ui -x-pack/packages/ml/error_utils @elastic/ml-ui -x-pack/packages/ml/field_stats_flyout @elastic/ml-ui -x-pack/packages/ml/in_memory_table @elastic/ml-ui -x-pack/packages/ml/is_defined @elastic/ml-ui -x-pack/packages/ml/is_populated_object @elastic/ml-ui -x-pack/packages/ml/kibana_theme @elastic/ml-ui -x-pack/packages/ml/local_storage @elastic/ml-ui -x-pack/packages/ml/nested_property @elastic/ml-ui -x-pack/packages/ml/number_utils @elastic/ml-ui -x-pack/packages/ml/parse_interval @elastic/ml-ui -x-pack/plugins/ml @elastic/ml-ui -x-pack/packages/ml/query_utils @elastic/ml-ui -x-pack/packages/ml/random_sampler_utils @elastic/ml-ui -x-pack/packages/ml/response_stream @elastic/ml-ui -x-pack/packages/ml/route_utils @elastic/ml-ui -x-pack/packages/ml/runtime_field_utils @elastic/ml-ui -x-pack/packages/ml/string_hash @elastic/ml-ui -x-pack/packages/ml/time_buckets @elastic/ml-ui -x-pack/packages/ml/trained_models_utils @elastic/ml-ui -x-pack/packages/ml/ui_actions @elastic/ml-ui -x-pack/packages/ml/url_state @elastic/ml-ui -x-pack/packages/ml/validators @elastic/ml-ui -packages/kbn-mock-idp-plugin @elastic/kibana-security -packages/kbn-mock-idp-utils @elastic/kibana-security -packages/kbn-monaco @elastic/appex-sharedux -x-pack/plugins/monitoring_collection @elastic/stack-monitoring -x-pack/plugins/monitoring @elastic/stack-monitoring -src/plugins/navigation @elastic/appex-sharedux -src/plugins/newsfeed @elastic/kibana-core -test/common/plugins/newsfeed @elastic/kibana-core -src/plugins/no_data_page @elastic/appex-sharedux -x-pack/plugins/notifications @elastic/appex-sharedux -packages/kbn-object-versioning @elastic/appex-sharedux -packages/kbn-object-versioning-utils @elastic/appex-sharedux -x-pack/plugins/observability_solution/observability_ai_assistant_app @elastic/obs-ai-assistant -x-pack/plugins/observability_solution/observability_ai_assistant_management @elastic/obs-ai-assistant -x-pack/plugins/observability_solution/observability_ai_assistant @elastic/obs-ai-assistant -x-pack/packages/observability/alert_details @elastic/obs-ux-management-team -x-pack/packages/observability/alerting_rule_utils @elastic/obs-ux-management-team -x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team -x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops -x-pack/packages/observability/get_padded_alert_time_range_util @elastic/obs-ux-management-team -x-pack/plugins/observability_solution/observability_logs_explorer @elastic/obs-ux-logs-team -x-pack/packages/observability/logs_overview @elastic/obs-ux-logs-team -x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team -x-pack/plugins/observability_solution/observability_onboarding @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team -x-pack/plugins/observability_solution/observability @elastic/obs-ux-management-team -x-pack/plugins/observability_solution/observability_shared @elastic/observability-ui -x-pack/packages/observability/synthetics_test_data @elastic/obs-ux-management-team -x-pack/packages/observability/observability_utils @elastic/observability-ui -x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security -test/common/plugins/otel_metrics @elastic/obs-ux-infra_services-team -packages/kbn-openapi-bundler @elastic/security-detection-rule-management -packages/kbn-openapi-common @elastic/security-detection-rule-management -packages/kbn-openapi-generator @elastic/security-detection-rule-management -packages/kbn-optimizer @elastic/kibana-operations -packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations -packages/kbn-osquery-io-ts-types @elastic/security-asset-management -x-pack/plugins/osquery @elastic/security-defend-workflows -examples/partial_results_example @elastic/kibana-data-discovery -x-pack/plugins/painless_lab @elastic/kibana-management -packages/kbn-panel-loader @elastic/kibana-presentation -packages/kbn-peggy @elastic/kibana-operations -packages/kbn-peggy-loader @elastic/kibana-operations -packages/kbn-performance-testing-dataset-extractor @elastic/kibana-performance-testing -packages/kbn-picomatcher @elastic/kibana-operations -packages/kbn-plugin-check @elastic/appex-sharedux -packages/kbn-plugin-generator @elastic/kibana-operations -packages/kbn-plugin-helpers @elastic/kibana-operations -examples/portable_dashboards_example @elastic/kibana-presentation -examples/preboot_example @elastic/kibana-security @elastic/kibana-core -packages/presentation/presentation_containers @elastic/kibana-presentation -src/plugins/presentation_panel @elastic/kibana-presentation -packages/presentation/presentation_publishing @elastic/kibana-presentation -src/plugins/presentation_util @elastic/kibana-presentation -x-pack/packages/ai-infra/product-doc-artifact-builder @elastic/appex-ai-infra -x-pack/plugins/observability_solution/profiling_data_access @elastic/obs-ux-infra_services-team -x-pack/plugins/observability_solution/profiling @elastic/obs-ux-infra_services-team -packages/kbn-profiling-utils @elastic/obs-ux-infra_services-team -x-pack/packages/kbn-random-sampling @elastic/kibana-visualizations -packages/kbn-react-field @elastic/kibana-data-discovery -packages/kbn-react-hooks @elastic/obs-ux-logs-team -packages/react/kibana_context/common @elastic/appex-sharedux -packages/react/kibana_context/render @elastic/appex-sharedux -packages/react/kibana_context/root @elastic/appex-sharedux -packages/react/kibana_context/styled @elastic/appex-sharedux -packages/react/kibana_context/theme @elastic/appex-sharedux -packages/react/kibana_mount @elastic/appex-sharedux -packages/kbn-recently-accessed @elastic/appex-sharedux -x-pack/plugins/remote_clusters @elastic/kibana-management -test/plugin_functional/plugins/rendering_plugin @elastic/kibana-core -packages/kbn-repo-file-maps @elastic/kibana-operations -packages/kbn-repo-info @elastic/kibana-operations -packages/kbn-repo-linter @elastic/kibana-operations -packages/kbn-repo-packages @elastic/kibana-operations -packages/kbn-repo-path @elastic/kibana-operations -packages/kbn-repo-source-classifier @elastic/kibana-operations -packages/kbn-repo-source-classifier-cli @elastic/kibana-operations -packages/kbn-reporting/common @elastic/appex-sharedux -packages/kbn-reporting/get_csv_panel_actions @elastic/appex-sharedux -packages/kbn-reporting/export_types/csv @elastic/appex-sharedux -packages/kbn-reporting/export_types/csv_common @elastic/appex-sharedux -packages/kbn-reporting/export_types/pdf @elastic/appex-sharedux -packages/kbn-reporting/export_types/pdf_common @elastic/appex-sharedux -packages/kbn-reporting/export_types/png @elastic/appex-sharedux -packages/kbn-reporting/export_types/png_common @elastic/appex-sharedux -packages/kbn-reporting/mocks_server @elastic/appex-sharedux -x-pack/plugins/reporting @elastic/appex-sharedux -packages/kbn-reporting/public @elastic/appex-sharedux -packages/kbn-reporting/server @elastic/appex-sharedux -packages/kbn-resizable-layout @elastic/kibana-data-discovery -examples/resizable_layout_examples @elastic/kibana-data-discovery -x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution -packages/response-ops/feature_flag_service @elastic/response-ops -packages/response-ops/rule_params @elastic/response-ops -examples/response_stream @elastic/ml-ui -packages/kbn-rison @elastic/kibana-operations -x-pack/packages/rollup @elastic/kibana-management -x-pack/plugins/rollup @elastic/kibana-management -packages/kbn-router-to-openapispec @elastic/kibana-core -packages/kbn-router-utils @elastic/obs-ux-logs-team -examples/routing_example @elastic/kibana-core -packages/kbn-rrule @elastic/response-ops -packages/kbn-rule-data-utils @elastic/security-detections-response @elastic/response-ops @elastic/obs-ux-management-team -x-pack/plugins/rule_registry @elastic/response-ops @elastic/obs-ux-management-team -x-pack/plugins/runtime_fields @elastic/kibana-management -packages/kbn-safer-lodash-set @elastic/kibana-security -x-pack/test/security_api_integration/plugins/saml_provider @elastic/kibana-security -x-pack/test/plugin_api_integration/plugins/sample_task_plugin @elastic/response-ops -x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget @elastic/response-ops -test/plugin_functional/plugins/saved_object_export_transforms @elastic/kibana-core -test/plugin_functional/plugins/saved_object_import_warnings @elastic/kibana-core -x-pack/test/saved_object_api_integration/common/plugins/saved_object_test_plugin @elastic/kibana-security -src/plugins/saved_objects_finder @elastic/kibana-data-discovery -test/plugin_functional/plugins/saved_objects_hidden_from_http_apis_type @elastic/kibana-core -test/plugin_functional/plugins/saved_objects_hidden_type @elastic/kibana-core -src/plugins/saved_objects_management @elastic/kibana-core -src/plugins/saved_objects @elastic/kibana-core -packages/kbn-saved-objects-settings @elastic/appex-sharedux -src/plugins/saved_objects_tagging_oss @elastic/appex-sharedux -x-pack/plugins/saved_objects_tagging @elastic/appex-sharedux -src/plugins/saved_search @elastic/kibana-data-discovery -examples/screenshot_mode_example @elastic/appex-sharedux -src/plugins/screenshot_mode @elastic/appex-sharedux -x-pack/examples/screenshotting_example @elastic/appex-sharedux -x-pack/plugins/screenshotting @elastic/kibana-reporting-services -packages/kbn-screenshotting-server @elastic/appex-sharedux -packages/kbn-search-api-keys-components @elastic/search-kibana -packages/kbn-search-api-keys-server @elastic/search-kibana -packages/kbn-search-api-panels @elastic/search-kibana -x-pack/plugins/search_assistant @elastic/search-kibana -packages/kbn-search-connectors @elastic/search-kibana -x-pack/plugins/search_connectors @elastic/search-kibana -packages/kbn-search-errors @elastic/kibana-data-discovery -examples/search_examples @elastic/kibana-data-discovery -x-pack/plugins/search_homepage @elastic/search-kibana -packages/kbn-search-index-documents @elastic/search-kibana -x-pack/plugins/search_indices @elastic/search-kibana -x-pack/plugins/search_inference_endpoints @elastic/search-kibana -x-pack/plugins/search_notebooks @elastic/search-kibana -x-pack/plugins/search_playground @elastic/search-kibana -packages/kbn-search-response-warnings @elastic/kibana-data-discovery -x-pack/packages/search/shared_ui @elastic/search-kibana -packages/kbn-search-types @elastic/kibana-data-discovery -x-pack/plugins/searchprofiler @elastic/kibana-management -x-pack/test/security_api_integration/packages/helpers @elastic/kibana-security -x-pack/packages/security/api_key_management @elastic/kibana-security -x-pack/packages/security/authorization_core @elastic/kibana-security -x-pack/packages/security/authorization_core_common @elastic/kibana-security -x-pack/packages/security/form_components @elastic/kibana-security -packages/kbn-security-hardening @elastic/kibana-security -x-pack/plugins/security @elastic/kibana-security -x-pack/packages/security/plugin_types_common @elastic/kibana-security -x-pack/packages/security/plugin_types_public @elastic/kibana-security -x-pack/packages/security/plugin_types_server @elastic/kibana-security -x-pack/packages/security/role_management_model @elastic/kibana-security -x-pack/packages/security-solution/distribution_bar @elastic/kibana-cloud-security-posture -x-pack/plugins/security_solution_ess @elastic/security-solution -x-pack/packages/security-solution/features @elastic/security-threat-hunting-explore -x-pack/test/cases_api_integration/common/plugins/security_solution @elastic/response-ops -x-pack/packages/security-solution/navigation @elastic/security-threat-hunting-explore -x-pack/plugins/security_solution @elastic/security-solution -x-pack/plugins/security_solution_serverless @elastic/security-solution -x-pack/packages/security-solution/side_nav @elastic/security-threat-hunting-explore -x-pack/packages/security-solution/storybook/config @elastic/security-threat-hunting-explore -x-pack/packages/security-solution/upselling @elastic/security-threat-hunting-explore -x-pack/test/security_functional/plugins/test_endpoints @elastic/kibana-security -x-pack/packages/security/ui_components @elastic/kibana-security -packages/kbn-securitysolution-autocomplete @elastic/security-detection-engine -x-pack/packages/security-solution/data_table @elastic/security-threat-hunting-investigations -packages/kbn-securitysolution-ecs @elastic/security-threat-hunting-explore -packages/kbn-securitysolution-endpoint-exceptions-common @elastic/security-detection-engine -packages/kbn-securitysolution-es-utils @elastic/security-detection-engine -packages/kbn-securitysolution-exception-list-components @elastic/security-detection-engine -packages/kbn-securitysolution-exceptions-common @elastic/security-detection-engine -packages/kbn-securitysolution-hook-utils @elastic/security-detection-engine -packages/kbn-securitysolution-io-ts-alerting-types @elastic/security-detection-engine -packages/kbn-securitysolution-io-ts-list-types @elastic/security-detection-engine -packages/kbn-securitysolution-io-ts-types @elastic/security-detection-engine -packages/kbn-securitysolution-io-ts-utils @elastic/security-detection-engine -packages/kbn-securitysolution-list-api @elastic/security-detection-engine -packages/kbn-securitysolution-list-constants @elastic/security-detection-engine -packages/kbn-securitysolution-list-hooks @elastic/security-detection-engine -packages/kbn-securitysolution-list-utils @elastic/security-detection-engine -packages/kbn-securitysolution-lists-common @elastic/security-detection-engine -packages/kbn-securitysolution-rules @elastic/security-detection-engine -packages/kbn-securitysolution-t-grid @elastic/security-detection-engine -packages/kbn-securitysolution-utils @elastic/security-detection-engine -packages/kbn-server-http-tools @elastic/kibana-core -packages/kbn-server-route-repository @elastic/obs-knowledge-team -packages/kbn-server-route-repository-client @elastic/obs-knowledge-team -packages/kbn-server-route-repository-utils @elastic/obs-knowledge-team -x-pack/plugins/serverless @elastic/appex-sharedux -packages/serverless/settings/common @elastic/appex-sharedux @elastic/kibana-management -x-pack/plugins/serverless_observability @elastic/obs-ux-management-team -packages/serverless/settings/observability_project @elastic/appex-sharedux @elastic/kibana-management @elastic/obs-ux-management-team -packages/serverless/project_switcher @elastic/appex-sharedux -x-pack/plugins/serverless_search @elastic/search-kibana -packages/serverless/settings/search_project @elastic/search-kibana @elastic/kibana-management -packages/serverless/settings/security_project @elastic/security-solution @elastic/kibana-management -packages/serverless/storybook/config @elastic/appex-sharedux -packages/serverless/types @elastic/appex-sharedux -test/plugin_functional/plugins/session_notifications @elastic/kibana-core -x-pack/plugins/session_view @elastic/kibana-cloud-security-posture -packages/kbn-set-map @elastic/kibana-operations -examples/share_examples @elastic/appex-sharedux -src/plugins/share @elastic/appex-sharedux -packages/kbn-shared-svg @elastic/obs-ux-infra_services-team -packages/shared-ux/avatar/solution @elastic/appex-sharedux -packages/shared-ux/button/exit_full_screen @elastic/appex-sharedux -packages/shared-ux/button_toolbar @elastic/appex-sharedux -packages/shared-ux/card/no_data/impl @elastic/appex-sharedux -packages/shared-ux/card/no_data/mocks @elastic/appex-sharedux -packages/shared-ux/card/no_data/types @elastic/appex-sharedux -packages/shared-ux/chrome/navigation @elastic/appex-sharedux -packages/shared-ux/error_boundary @elastic/appex-sharedux -packages/shared-ux/file/context @elastic/appex-sharedux -packages/shared-ux/file/image/impl @elastic/appex-sharedux -packages/shared-ux/file/image/mocks @elastic/appex-sharedux -packages/shared-ux/file/mocks @elastic/appex-sharedux -packages/shared-ux/file/file_picker/impl @elastic/appex-sharedux -packages/shared-ux/file/types @elastic/appex-sharedux -packages/shared-ux/file/file_upload/impl @elastic/appex-sharedux -packages/shared-ux/file/util @elastic/appex-sharedux -packages/shared-ux/link/redirect_app/impl @elastic/appex-sharedux -packages/shared-ux/link/redirect_app/mocks @elastic/appex-sharedux -packages/shared-ux/link/redirect_app/types @elastic/appex-sharedux -packages/shared-ux/markdown/impl @elastic/appex-sharedux -packages/shared-ux/markdown/mocks @elastic/appex-sharedux -packages/shared-ux/markdown/types @elastic/appex-sharedux -packages/shared-ux/page/analytics_no_data/impl @elastic/appex-sharedux -packages/shared-ux/page/analytics_no_data/mocks @elastic/appex-sharedux -packages/shared-ux/page/analytics_no_data/types @elastic/appex-sharedux -packages/shared-ux/page/kibana_no_data/impl @elastic/appex-sharedux -packages/shared-ux/page/kibana_no_data/mocks @elastic/appex-sharedux -packages/shared-ux/page/kibana_no_data/types @elastic/appex-sharedux -packages/shared-ux/page/kibana_template/impl @elastic/appex-sharedux -packages/shared-ux/page/kibana_template/mocks @elastic/appex-sharedux -packages/shared-ux/page/kibana_template/types @elastic/appex-sharedux -packages/shared-ux/page/no_data/impl @elastic/appex-sharedux -packages/shared-ux/page/no_data_config/impl @elastic/appex-sharedux -packages/shared-ux/page/no_data_config/mocks @elastic/appex-sharedux -packages/shared-ux/page/no_data_config/types @elastic/appex-sharedux -packages/shared-ux/page/no_data/mocks @elastic/appex-sharedux -packages/shared-ux/page/no_data/types @elastic/appex-sharedux -packages/shared-ux/page/solution_nav @elastic/appex-sharedux -packages/shared-ux/prompt/no_data_views/impl @elastic/appex-sharedux -packages/shared-ux/prompt/no_data_views/mocks @elastic/appex-sharedux -packages/shared-ux/prompt/no_data_views/types @elastic/appex-sharedux -packages/shared-ux/prompt/not_found @elastic/appex-sharedux -packages/shared-ux/router/impl @elastic/appex-sharedux -packages/shared-ux/router/mocks @elastic/appex-sharedux -packages/shared-ux/router/types @elastic/appex-sharedux -packages/shared-ux/storybook/config @elastic/appex-sharedux -packages/shared-ux/storybook/mock @elastic/appex-sharedux -packages/shared-ux/modal/tabbed @elastic/appex-sharedux -packages/shared-ux/table_persist @elastic/appex-sharedux -packages/kbn-shared-ux-utility @elastic/appex-sharedux -x-pack/plugins/observability_solution/slo @elastic/obs-ux-management-team -x-pack/packages/kbn-slo-schema @elastic/obs-ux-management-team -x-pack/plugins/snapshot_restore @elastic/kibana-management -packages/kbn-some-dev-log @elastic/kibana-operations -packages/kbn-sort-package-json @elastic/kibana-operations -packages/kbn-sort-predicates @elastic/kibana-visualizations -x-pack/plugins/spaces @elastic/kibana-security -x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin @elastic/kibana-security -packages/kbn-spec-to-console @elastic/kibana-management -packages/kbn-sse-utils @elastic/obs-knowledge-team -packages/kbn-sse-utils-client @elastic/obs-knowledge-team -packages/kbn-sse-utils-server @elastic/obs-knowledge-team -x-pack/plugins/stack_alerts @elastic/response-ops -x-pack/plugins/stack_connectors @elastic/response-ops -x-pack/test/usage_collection/plugins/stack_management_usage_test @elastic/kibana-management -examples/state_containers_examples @elastic/appex-sharedux -test/server_integration/plugins/status_plugin_a @elastic/kibana-core -test/server_integration/plugins/status_plugin_b @elastic/kibana-core -packages/kbn-std @elastic/kibana-core -packages/kbn-stdio-dev-helpers @elastic/kibana-operations -packages/kbn-storybook @elastic/kibana-operations -x-pack/plugins/observability_solution/synthetics/e2e @elastic/obs-ux-management-team -x-pack/plugins/observability_solution/synthetics @elastic/obs-ux-management-team -x-pack/packages/kbn-synthetics-private-location @elastic/obs-ux-management-team -x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture @elastic/response-ops -x-pack/test/plugin_api_perf/plugins/task_manager_performance @elastic/response-ops -x-pack/plugins/task_manager @elastic/response-ops -src/plugins/telemetry_collection_manager @elastic/kibana-core -x-pack/plugins/telemetry_collection_xpack @elastic/kibana-core -src/plugins/telemetry_management_section @elastic/kibana-core -src/plugins/telemetry @elastic/kibana-core -test/plugin_functional/plugins/telemetry @elastic/kibana-core -packages/kbn-telemetry-tools @elastic/kibana-core -packages/kbn-test @elastic/kibana-operations @elastic/appex-qa -packages/kbn-test-eui-helpers @elastic/kibana-visualizations -x-pack/test/licensing_plugin/plugins/test_feature_usage @elastic/kibana-security -packages/kbn-test-jest-helpers @elastic/kibana-operations @elastic/appex-qa -packages/kbn-test-subj-selector @elastic/kibana-operations @elastic/appex-qa -x-pack/test_serverless -test -x-pack/test -x-pack/performance @elastic/appex-qa -x-pack/examples/testing_embedded_lens @elastic/kibana-visualizations -x-pack/examples/third_party_lens_navigation_prompt @elastic/kibana-visualizations -x-pack/examples/third_party_vis_lens_example @elastic/kibana-visualizations -x-pack/plugins/threat_intelligence @elastic/security-threat-hunting-investigations -x-pack/plugins/timelines @elastic/security-threat-hunting-investigations -packages/kbn-timelion-grammar @elastic/kibana-visualizations -packages/kbn-timerange @elastic/obs-ux-logs-team -packages/kbn-tinymath @elastic/kibana-visualizations -packages/kbn-tooling-log @elastic/kibana-operations -x-pack/plugins/transform @elastic/ml-ui -x-pack/plugins/translations @elastic/kibana-localization -packages/kbn-transpose-utils @elastic/kibana-visualizations -x-pack/examples/triggers_actions_ui_example @elastic/response-ops -x-pack/plugins/triggers_actions_ui @elastic/response-ops -packages/kbn-triggers-actions-ui-types @elastic/response-ops -packages/kbn-try-in-console @elastic/search-kibana -packages/kbn-ts-projects @elastic/kibana-operations -packages/kbn-ts-type-check-cli @elastic/kibana-operations -packages/kbn-typed-react-router-config @elastic/obs-knowledge-team @elastic/obs-ux-management-team -packages/kbn-ui-actions-browser @elastic/appex-sharedux -x-pack/examples/ui_actions_enhanced_examples @elastic/appex-sharedux -src/plugins/ui_actions_enhanced @elastic/appex-sharedux -examples/ui_action_examples @elastic/appex-sharedux -examples/ui_actions_explorer @elastic/appex-sharedux -src/plugins/ui_actions @elastic/appex-sharedux -test/plugin_functional/plugins/ui_settings_plugin @elastic/kibana-core -packages/kbn-ui-shared-deps-npm @elastic/kibana-operations -packages/kbn-ui-shared-deps-src @elastic/kibana-operations -packages/kbn-ui-theme @elastic/kibana-operations -packages/kbn-unified-data-table @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations -packages/kbn-unified-doc-viewer @elastic/kibana-data-discovery -examples/unified_doc_viewer @elastic/kibana-core -src/plugins/unified_doc_viewer @elastic/kibana-data-discovery -packages/kbn-unified-field-list @elastic/kibana-data-discovery -examples/unified_field_list_examples @elastic/kibana-data-discovery -src/plugins/unified_histogram @elastic/kibana-data-discovery -src/plugins/unified_search @elastic/kibana-visualizations -packages/kbn-unsaved-changes-badge @elastic/kibana-data-discovery -packages/kbn-unsaved-changes-prompt @elastic/kibana-management -x-pack/plugins/upgrade_assistant @elastic/kibana-management -x-pack/plugins/observability_solution/uptime @elastic/obs-ux-management-team -x-pack/plugins/drilldowns/url_drilldown @elastic/appex-sharedux -src/plugins/url_forwarding @elastic/kibana-visualizations -src/plugins/usage_collection @elastic/kibana-core -test/plugin_functional/plugins/usage_collection @elastic/kibana-core -packages/kbn-use-tracked-promise @elastic/obs-ux-logs-team -packages/kbn-user-profile-components @elastic/kibana-security -examples/user_profile_examples @elastic/kibana-security -x-pack/test/security_api_integration/plugins/user_profiles_consumer @elastic/kibana-security -packages/kbn-utility-types @elastic/kibana-core -packages/kbn-utility-types-jest @elastic/kibana-operations -packages/kbn-utils @elastic/kibana-operations -x-pack/plugins/observability_solution/ux @elastic/obs-ux-infra_services-team -examples/v8_profiler_examples @elastic/response-ops -packages/kbn-validate-next-docs-cli @elastic/kibana-operations -src/plugins/vis_default_editor @elastic/kibana-visualizations -src/plugins/vis_types/gauge @elastic/kibana-visualizations -src/plugins/vis_types/heatmap @elastic/kibana-visualizations -src/plugins/vis_type_markdown @elastic/kibana-presentation -src/plugins/vis_types/metric @elastic/kibana-visualizations -src/plugins/vis_types/pie @elastic/kibana-visualizations -src/plugins/vis_types/table @elastic/kibana-visualizations -src/plugins/vis_types/tagcloud @elastic/kibana-visualizations -src/plugins/vis_types/timelion @elastic/kibana-visualizations -src/plugins/vis_types/timeseries @elastic/kibana-visualizations -src/plugins/vis_types/vega @elastic/kibana-visualizations -src/plugins/vis_types/vislib @elastic/kibana-visualizations -src/plugins/vis_types/xy @elastic/kibana-visualizations -packages/kbn-visualization-ui-components @elastic/kibana-visualizations -packages/kbn-visualization-utils @elastic/kibana-visualizations -src/plugins/visualizations @elastic/kibana-visualizations -x-pack/plugins/watcher @elastic/kibana-management -packages/kbn-web-worker-stub @elastic/kibana-operations -packages/kbn-whereis-pkg-cli @elastic/kibana-operations -packages/kbn-xstate-utils @elastic/obs-ux-logs-team -packages/kbn-yarn-lock-validator @elastic/kibana-operations -packages/kbn-zod @elastic/kibana-core -packages/kbn-zod-helpers @elastic/security-detection-rule-management -#### -## Everything below this line overrides the default assignments for each package. -## Items lower in the file have higher precedence: -## https://help.github.com/articles/about-codeowners/ -#### - -# The #CC# prefix delineates Code Coverage, -# used for the 'team' designator within Kibana Stats - -/x-pack/test/api_integration/apis/metrics_ui @elastic/obs-ux-logs-team @elastic/obs-ux-infra_services-team -x-pack/test_serverless/api_integration/test_suites/common/platform_security @elastic/kibana-security - -# Observability Entities Team (@elastic/obs-entities) -/x-pack/plugins/observability_solution/entities_data_access @elastic/obs-entities -/x-pack/packages/kbn-entities-schema @elastic/obs-entities -/x-pack/test/api_integration/apis/entity_manager/fixture_plugin @elastic/obs-entities -/x-pack/plugins/entity_manager @elastic/obs-entities -/x-pack/test/api_integration/apis/entity_manager @elastic/obs-entities - -# Data Discovery -/x-pack/test/api_integration/apis/kibana/kql_telemetry @elastic/kibana-data-discovery @elastic/kibana-visualizations -/x-pack/test_serverless/functional/es_archives/pre_calculated_histogram @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/es_archives/kibana_sample_data_flights_index_pattern @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/security/config.examples.ts @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts @elastic/kibana-data-discovery -/test/accessibility/apps/discover.ts @elastic/kibana-data-discovery -/test/api_integration/apis/data_views @elastic/kibana-data-discovery -/test/api_integration/apis/data_view_field_editor @elastic/kibana-data-discovery -/test/api_integration/apis/kql_telemetry @elastic/kibana-data-discovery -/test/api_integration/apis/scripts @elastic/kibana-data-discovery -/test/api_integration/apis/search @elastic/kibana-data-discovery -/test/examples/data_view_field_editor_example @elastic/kibana-data-discovery -/test/examples/discover_customization_examples @elastic/kibana-data-discovery -/test/examples/field_formats @elastic/kibana-data-discovery -/test/examples/partial_results @elastic/kibana-data-discovery -/test/examples/search @elastic/kibana-data-discovery -/test/examples/unified_field_list_examples @elastic/kibana-data-discovery -/test/functional/apps/context @elastic/kibana-data-discovery -/test/functional/apps/discover @elastic/kibana-data-discovery -/test/functional/apps/management/ccs_compatibility/_data_views_ccs.ts @elastic/kibana-data-discovery -/test/functional/apps/management/data_views @elastic/kibana-data-discovery -/test/plugin_functional/test_suites/data_plugin @elastic/kibana-data-discovery -/x-pack/test/accessibility/apps/group3/search_sessions.ts @elastic/kibana-data-discovery -/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @elastic/kibana-data-discovery -/x-pack/test/api_integration/apis/search @elastic/kibana-data-discovery -/x-pack/test/examples/search_examples @elastic/kibana-data-discovery -/x-pack/test/functional/apps/data_views @elastic/kibana-data-discovery -/x-pack/test/functional/apps/discover @elastic/kibana-data-discovery -/x-pack/test/functional/apps/saved_query_management @elastic/kibana-data-discovery -/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover @elastic/kibana-data-discovery -/x-pack/test/search_sessions_integration @elastic/kibana-data-discovery -/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @elastic/kibana-data-discovery -/x-pack/test/stack_functional_integration/apps/management/_index_pattern_create.js @elastic/kibana-data-discovery -/x-pack/test/upgrade/apps/discover @elastic/kibana-data-discovery -/x-pack/test_serverless/api_integration/test_suites/common/data_views @elastic/kibana-data-discovery -/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor @elastic/kibana-data-discovery -/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry @elastic/kibana-data-discovery -/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests @elastic/kibana-data-discovery -/x-pack/test_serverless/api_integration/test_suites/common/search_oss @elastic/kibana-data-discovery -/x-pack/test_serverless/api_integration/test_suites/common/search_xpack @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/context @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/discover @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/examples/field_formats @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/examples/partial_results @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/examples/search @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/examples/search_examples @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples @elastic/kibana-data-discovery -/x-pack/test_serverless/functional/test_suites/common/management/data_views @elastic/kibana-data-discovery -src/plugins/discover/public/context_awareness/profile_providers/security @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations - -# Platform Docs -/x-pack/test_serverless/functional/test_suites/security/screenshot_creation/index.ts @elastic/platform-docs -/x-pack/test_serverless/functional/test_suites/security/config.screenshots.ts @elastic/platform-docs - -# Visualizations -/x-pack/test/accessibility/apps/group3/graph.ts @elastic/kibana-visualizations -/x-pack/test/accessibility/apps/group2/lens.ts @elastic/kibana-visualizations -/src/plugins/visualize/ @elastic/kibana-visualizations -/x-pack/test/functional/apps/lens @elastic/kibana-visualizations -/x-pack/test/api_integration/apis/lens/ @elastic/kibana-visualizations -/test/functional/apps/visualize/ @elastic/kibana-visualizations -/x-pack/test/functional/apps/graph @elastic/kibana-visualizations -/test/api_integration/apis/event_annotations @elastic/kibana-visualizations -/x-pack/test_serverless/functional/test_suites/common/visualizations/ @elastic/kibana-visualizations -/x-pack/test_serverless/functional/fixtures/kbn_archiver/lens/ @elastic/kibana-visualizations -packages/kbn-monaco/src/esql @elastic/kibana-esql - -# Global Experience - -### Global Experience Reporting -/x-pack/test/functional/apps/dashboard/reporting/ @elastic/appex-sharedux -/x-pack/test/functional/apps/reporting/ @elastic/appex-sharedux -/x-pack/test/functional/apps/reporting_management/ @elastic/appex-sharedux -/x-pack/test/examples/screenshotting/ @elastic/appex-sharedux -/x-pack/test/functional/es_archives/lens/reporting/ @elastic/appex-sharedux -/x-pack/test/functional/es_archives/reporting/ @elastic/appex-sharedux -/x-pack/test/functional/fixtures/kbn_archiver/reporting/ @elastic/appex-sharedux -/x-pack/test/reporting_api_integration/ @elastic/appex-sharedux -/x-pack/test/reporting_functional/ @elastic/appex-sharedux -/x-pack/test/stack_functional_integration/apps/reporting/ @elastic/appex-sharedux -/docs/user/reporting @elastic/appex-sharedux -/docs/settings/reporting-settings.asciidoc @elastic/appex-sharedux -/docs/setup/configuring-reporting.asciidoc @elastic/appex-sharedux -/x-pack/test_serverless/**/test_suites/common/reporting/ @elastic/appex-sharedux -/x-pack/test/accessibility/apps/group3/reporting.ts @elastic/appex-sharedux - -### Global Experience Tagging -/x-pack/test/saved_object_tagging/ @elastic/appex-sharedux - -### Kibana React (to be deprecated) -/src/plugins/kibana_react/public/@elastic/appex-sharedux @elastic/kibana-presentation - -### Home Plugin and Packages -/src/plugins/home/public @elastic/appex-sharedux -/src/plugins/home/server/*.ts @elastic/appex-sharedux -/src/plugins/home/server/services/ @elastic/appex-sharedux - -### Code Coverage -#CC# /src/plugins/home/public @elastic/appex-sharedux -#CC# /src/plugins/home/server/services/ @elastic/appex-sharedux -#CC# /src/plugins/home/ @elastic/appex-sharedux -#CC# /x-pack/plugins/reporting/ @elastic/appex-sharedux -#CC# /x-pack/plugins/security_solution_serverless/ @elastic/appex-sharedux - -### Observability Plugins - - -# Observability AI Assistant -x-pack/test/observability_ai_assistant_api_integration @elastic/obs-ai-assistant -x-pack/test/observability_ai_assistant_functional @elastic/obs-ai-assistant -x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai-assistant - -# Infra Monitoring -## This plugin mostly contains the codebase for the infra services, but also includes some code for the Logs UI app. -## To keep @elastic/obs-ux-logs-team as codeowner of the plugin manifest without requiring a review for all the other code changes -## the priority on codeownership will be as follow: -## - infra -> both teams (automatically generated by script) -## - infra/{common,docs,public,server}/{sub-folders}/ -> @elastic/obs-ux-infra_services-team -## - Logs UI code exceptions -> @elastic/obs-ux-logs-team -## This should allow the infra team to work without dependencies on the @elastic/obs-ux-logs-team, which will maintain ownership of the Logs UI code only. - -## infra/{common,docs,public,server}/{sub-folders}/ -> @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/common @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/docs @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/alerting @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/apps @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/common @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/components @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/containers @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/hooks @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/images @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/lib @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/pages @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/services @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/test_utils @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/public/utils @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/server/lib @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/server/routes @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/server/saved_objects @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/server/services @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/server/usage @elastic/obs-ux-infra_services-team -/x-pack/plugins/observability_solution/infra/server/utils @elastic/obs-ux-infra_services-team - -## Logs UI code exceptions -> @elastic/obs-ux-logs-team -/x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_stream_log_file.ts @elastic/obs-ux-logs-team -/x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_page.ts @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/common/http_api/log_alerts @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/common/http_api/log_analysis @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/common/log_analysis @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/common/log_search_result @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/common/log_search_summary @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/common/log_text_scale @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/common/performance_tracing.ts @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/common/search_strategies/log_entries @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/docs/state_machines @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/public/apps/logs_app.tsx @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/public/components/log_stream @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/public/components/logging @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/public/containers/logs @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/public/observability_logs @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/public/pages/logs @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/server/lib/log_analysis @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/server/routes/log_alerts @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/server/routes/log_analysis @elastic/obs-ux-logs-team -/x-pack/plugins/observability_solution/infra/server/services/rules @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team -# Infra Monitoring tests -/x-pack/test/api_integration/apis/infra @elastic/obs-ux-infra_services-team -/x-pack/test/functional/apps/infra @elastic/obs-ux-infra_services-team -/x-pack/test/functional/apps/infra/logs @elastic/obs-ux-logs-team - -# Observability UX management team -/x-pack/test/api_integration/apis/slos @elastic/obs-ux-management-team -/x-pack/test/accessibility/apps/group1/uptime.ts @elastic/obs-ux-management-team -/x-pack/test/accessibility/apps/group3/observability.ts @elastic/obs-ux-management-team -/x-pack/packages/observability/alert_details @elastic/obs-ux-management-team -/x-pack/test/observability_functional @elastic/obs-ux-management-team -/x-pack/plugins/observability_solution/infra/public/alerting @elastic/obs-ux-management-team -/x-pack/plugins/observability_solution/infra/server/lib/alerting @elastic/obs-ux-management-team -/x-pack/test_serverless/**/test_suites/observability/custom_threshold_rule/ @elastic/obs-ux-management-team -/x-pack/test_serverless/**/test_suites/observability/slos/ @elastic/obs-ux-management-team -/x-pack/test_serverless/api_integration/test_suites/observability/es_query_rule @elastic/obs-ux-management-team -/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/burn_rate_rule @elastic/obs-ux-management-team -/x-pack/test/api_integration/deployment_agnostic/services/alerting_api @elastic/obs-ux-management-team -/x-pack/test/api_integration/deployment_agnostic/services/slo_api @elastic/obs-ux-management-team -/x-pack/test_serverless/**/test_suites/observability/infra/ @elastic/obs-ux-infra_services-team - -# Elastic Stack Monitoring -/x-pack/test/functional/apps/monitoring @elastic/stack-monitoring -/x-pack/test/api_integration/apis/monitoring @elastic/stack-monitoring -/x-pack/test/api_integration/apis/monitoring_collection @elastic/stack-monitoring -/x-pack/test/accessibility/apps/group1/kibana_overview.ts @elastic/stack-monitoring -/x-pack/test/accessibility/apps/group3/stack_monitoring.ts @elastic/stack-monitoring - -# Fleet -/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 @elastic/fleet @elastic/kibana-operations -/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @elastic/fleet @elastic/obs-cloudnative-monitoring -/x-pack/test_serverless/**/test_suites/**/fleet/ @elastic/fleet - -# APM -/x-pack/test/functional/apps/apm/ @elastic/obs-ux-infra_services-team -/x-pack/test/apm_api_integration/ @elastic/obs-ux-infra_services-team -/src/apm.js @elastic/kibana-core @vigneshshanmugam -/packages/kbn-utility-types/src/dot.ts @dgieselaar -/packages/kbn-utility-types/src/dot_test.ts @dgieselaar -/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/ @elastic/obs-ux-infra_services-team -#CC# /src/plugins/apm_oss/ @elastic/apm-ui -#CC# /x-pack/plugins/observability_solution/observability/ @elastic/apm-ui - -# Uptime -/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/ @elastic/obs-ux-management-team -/x-pack/test/functional/apps/uptime @elastic/obs-ux-management-team -/x-pack/test/functional/es_archives/uptime @elastic/obs-ux-management-team -/x-pack/test/functional/services/uptime @elastic/obs-ux-management-team -/x-pack/test/api_integration/apis/uptime @elastic/obs-ux-management-team -/x-pack/test/api_integration/apis/synthetics @elastic/obs-ux-management-team -/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts @elastic/obs-ux-management-team -/x-pack/test/alerting_api_integration/observability/index.ts @elastic/obs-ux-management-team -/x-pack/test_serverless/api_integration/test_suites/observability/synthetics @elastic/obs-ux-management-team - -# obs-ux-logs-team -/x-pack/test_serverless/api_integration/test_suites/observability/index.feature_flags.ts @elastic/obs-ux-logs-team -/x-pack/test/api_integration/apis/logs_ui @elastic/obs-ux-logs-team -/x-pack/test/dataset_quality_api_integration @elastic/obs-ux-logs-team -/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration @elastic/obs-ux-logs-team -/x-pack/test/functional/apps/observability_logs_explorer @elastic/obs-ux-logs-team -/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer @elastic/obs-ux-logs-team -/x-pack/test/functional/apps/dataset_quality @elastic/obs-ux-logs-team -/x-pack/test_serverless/functional/test_suites/observability/dataset_quality @elastic/obs-ux-logs-team -/x-pack/test_serverless/functional/test_suites/observability/ @elastic/obs-ux-logs-team -/src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview @elastic/obs-ux-logs-team -/x-pack/test/api_integration/apis/logs_shared @elastic/obs-ux-logs-team - -# Observability onboarding tour -/x-pack/plugins/observability_solution/observability_shared/public/components/tour @elastic/appex-sharedux -/x-pack/test/functional/apps/infra/tour.ts @elastic/appex-sharedux - -# Observability settings -/x-pack/plugins/observability_solution/observability/server/ui_settings.ts @elastic/obs-docs - -### END Observability Plugins - -# Presentation -/x-pack/test/functional/apps/dashboard @elastic/kibana-presentation -/x-pack/test/accessibility/apps/group3/maps.ts @elastic/kibana-presentation -/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts @elastic/kibana-presentation -/x-pack/test/accessibility/apps/group1/dashboard_links.ts @elastic/kibana-presentation -/x-pack/test/accessibility/apps/group1/dashboard_controls.ts @elastic/kibana-presentation -/test/functional/apps/dashboard/ @elastic/kibana-presentation -/test/functional/apps/dashboard_elements/ @elastic/kibana-presentation -/test/functional/services/dashboard/ @elastic/kibana-presentation -/x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation -/x-pack/test_serverless/functional/test_suites/search/dashboards/ @elastic/kibana-presentation -/test/plugin_functional/test_suites/panel_actions @elastic/kibana-presentation -/x-pack/test/functional/es_archives/canvas/logstash_lens @elastic/kibana-presentation -#CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation - -# Machine Learning -/x-pack/test/api_integration/apis/file_upload @elastic/ml-ui -/x-pack/test/accessibility/apps/group2/ml.ts @elastic/ml-ui -/x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts @elastic/ml-ui -/x-pack/test/api_integration/apis/ml/ @elastic/ml-ui -/x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui -/x-pack/test/functional/apps/ml/ @elastic/ml-ui -/x-pack/test/functional/es_archives/ml/ @elastic/ml-ui -/x-pack/test/functional/services/ml/ @elastic/ml-ui -/x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui -/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/ml/ @elastic/ml-ui -/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/ @elastic/ml-ui -/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/ @elastic/ml-ui -/x-pack/test/screenshot_creation/apps/ml_docs @elastic/ml-ui -/x-pack/test/screenshot_creation/services/ml_screenshots.ts @elastic/ml-ui -/x-pack/test_serverless/**/test_suites/**/ml/ @elastic/ml-ui -/x-pack/test_serverless/**/test_suites/common/management/transforms/ @elastic/ml-ui - -# Additional plugins and packages maintained by the ML team. -/x-pack/test/accessibility/apps/group2/transform.ts @elastic/ml-ui -/x-pack/test/api_integration/apis/aiops/ @elastic/ml-ui -/x-pack/test/api_integration/apis/transform/ @elastic/ml-ui -/x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui -/x-pack/test/functional/apps/transform/ @elastic/ml-ui -/x-pack/test/functional/services/transform/ @elastic/ml-ui -/x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui - -# Maps -#CC# /x-pack/plugins/maps/ @elastic/kibana-gis -/x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis -/x-pack/test/functional/apps/maps/ @elastic/kibana-gis -/x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis -/x-pack/plugins/stack_alerts/server/rule_types/geo_containment @elastic/kibana-gis -/x-pack/plugins/stack_alerts/public/rule_types/geo_containment @elastic/kibana-gis -#CC# /x-pack/plugins/file_upload @elastic/kibana-gis - -# Operations -/src/dev/license_checker/config.ts @elastic/kibana-operations -/src/dev/ @elastic/kibana-operations -/src/setup_node_env/ @elastic/kibana-operations -/src/cli/keystore/ @elastic/kibana-operations -/src/cli/serve/ @elastic/kibana-operations -/src/cli_keystore/ @elastic/kibana-operations -/.github/workflows/ @elastic/kibana-operations -/vars/ @elastic/kibana-operations -/.bazelignore @elastic/kibana-operations -/.bazeliskversion @elastic/kibana-operations -/.bazelrc @elastic/kibana-operations -/.bazelrc.common @elastic/kibana-operations -/.bazelversion @elastic/kibana-operations -/WORKSPACE.bazel @elastic/kibana-operations -/.buildkite/ @elastic/kibana-operations -/.buildkite/scripts/steps/esql_grammar_sync.sh @elastic/kibana-esql -/.buildkite/scripts/steps/esql_generate_function_metadata.sh @elastic/kibana-esql -/.buildkite/pipelines/esql_grammar_sync.yml @elastic/kibana-esql -/.buildkite/scripts/steps/code_generation/security_solution_codegen.sh @elastic/security-detection-rule-management -/kbn_pm/ @elastic/kibana-operations -/x-pack/dev-tools @elastic/kibana-operations -/catalog-info.yaml @elastic/kibana-operations @elastic/kibana-tech-leads -/.devcontainer/ @elastic/kibana-operations -/.eslintrc.js @elastic/kibana-operations -/.eslintignore @elastic/kibana-operations - -# Appex QA -/x-pack/test/functional/config.*.* @elastic/appex-qa -/x-pack/test/api_integration/ftr_provider_context.d.ts @elastic/appex-qa # Maybe this should be a glob? -/x-pack/test/accessibility/services.ts @elastic/appex-qa -/x-pack/test/accessibility/page_objects.ts @elastic/appex-qa -/x-pack/test/accessibility/ftr_provider_context.d.ts @elastic/appex-qa -/x-pack/test_serverless/tsconfig.json @elastic/appex-qa -/x-pack/test_serverless/kibana.jsonc @elastic/appex-qa -/x-pack/test_serverless/functional/test_suites/common/README.md @elastic/appex-qa -/x-pack/test_serverless/functional/page_objects/index.ts @elastic/appex-qa -/x-pack/test_serverless/functional/ftr_provider_context.d.ts @elastic/appex-qa -/x-pack/test_serverless/functional/test_suites/common/management/index.ts @elastic/appex-qa -/x-pack/test_serverless/functional/test_suites/common/examples/index.ts @elastic/appex-qa -/x-pack/test_serverless/functional/page_objects/svl_common_page.ts @elastic/appex-qa -/x-pack/test_serverless/README.md @elastic/appex-qa -/x-pack/test_serverless/api_integration/ftr_provider_context.d.ts @elastic/appex-qa -/x-pack/test_serverless/api_integration/test_suites/common/README.md @elastic/appex-qa -/src/dev/code_coverage @elastic/appex-qa -/test/functional/services/common @elastic/appex-qa -/test/functional/services/lib @elastic/appex-qa -/test/functional/services/remote @elastic/appex-qa -/test/visual_regression @elastic/appex-qa -/x-pack/test/visual_regression @elastic/appex-qa -/packages/kbn-test/src/functional_test_runner @elastic/appex-qa -/packages/kbn-performance-testing-dataset-extractor @elastic/appex-qa -/x-pack/test_serverless/**/*config.base.ts @elastic/appex-qa -/x-pack/test_serverless/**/deployment_agnostic_services.ts @elastic/appex-qa -/x-pack/test_serverless/shared/ @elastic/appex-qa -/x-pack/test_serverless/**/test_suites/**/common_configs/ @elastic/appex-qa -/x-pack/test_serverless/api_integration/test_suites/common/elasticsearch_api @elastic/appex-qa -/x-pack/test_serverless/functional/test_suites/security/ftr/ @elastic/appex-qa -/x-pack/test_serverless/functional/test_suites/common/home_page/ @elastic/appex-qa -/x-pack/test_serverless/**/services/ @elastic/appex-qa -/packages/kbn-es/src/stateful_resources/roles.yml @elastic/appex-qa -x-pack/test/api_integration/deployment_agnostic/default_configs/ @elastic/appex-qa -x-pack/test/api_integration/deployment_agnostic/services/ @elastic/appex-qa -x-pack/test/**/deployment_agnostic/ @elastic/appex-qa #temporarily to monitor tests migration - -# Core -/x-pack/test/api_integration/apis/telemetry @elastic/kibana-core -/x-pack/test/api_integration/apis/status @elastic/kibana-core -/x-pack/test/api_integration/apis/stats @elastic/kibana-core -/x-pack/test/api_integration/apis/kibana/stats @elastic/kibana-core -/x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts @elastic/kibana-core -/config/ @elastic/kibana-core -/config/serverless.yml @elastic/kibana-core @elastic/kibana-security -/config/serverless.es.yml @elastic/kibana-core @elastic/kibana-security -/config/serverless.oblt.yml @elastic/kibana-core @elastic/kibana-security -/config/serverless.security.yml @elastic/kibana-core @elastic/kibana-security -/typings/ @elastic/kibana-core -/test/analytics @elastic/kibana-core -/packages/kbn-test/src/jest/setup/mocks.kbn_i18n_react.js @elastic/kibana-core -/x-pack/test/saved_objects_field_count/ @elastic/kibana-core -/x-pack/test_serverless/**/test_suites/common/saved_objects_management/ @elastic/kibana-core -/x-pack/test_serverless/api_integration/test_suites/common/core/ @elastic/kibana-core -/x-pack/test_serverless/api_integration/test_suites/**/telemetry/ @elastic/kibana-core -/x-pack/test/functional/es_archives/cases/migrations/8.8.0 @elastic/response-ops - -#CC# /src/core/server/csp/ @elastic/kibana-core -#CC# /src/plugins/saved_objects/ @elastic/kibana-core -#CC# /x-pack/plugins/cloud/ @elastic/kibana-core -#CC# /x-pack/plugins/features/ @elastic/kibana-core -#CC# /x-pack/plugins/global_search/ @elastic/kibana-core -#CC# /src/plugins/newsfeed @elastic/kibana-core -#CC# /x-pack/plugins/global_search_providers/ @elastic/kibana-core - -# AppEx AI Infra -/x-pack/plugins/inference @elastic/appex-ai-infra @elastic/obs-ai-assistant @elastic/security-generative-ai - -# AppEx Platform Services Security -//x-pack/test_serverless/api_integration/test_suites/common/security_response_headers.ts @elastic/kibana-security -/x-pack/test/api_integration/apis/es @elastic/kibana-security - -/x-pack/test/api_integration/apis/features @elastic/kibana-security - -# Kibana Telemetry -/.telemetryrc.json @elastic/kibana-core -/x-pack/.telemetryrc.json @elastic/kibana-core -/src/plugins/telemetry/schema/ @elastic/kibana-core -/x-pack/plugins/telemetry_collection_xpack/schema/ @elastic/kibana-core -x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kibana-core @shahinakmal - -# Kibana Localization -/src/dev/i18n_tools/ @elastic/kibana-localization @elastic/kibana-core -/src/core/public/i18n/ @elastic/kibana-localization @elastic/kibana-core -#CC# /x-pack/plugins/translations/ @elastic/kibana-localization @elastic/kibana-core - -# Kibana Platform Security -/.github/codeql @elastic/kibana-security -/.github/workflows/codeql.yml @elastic/kibana-security -/.github/workflows/codeql-stats.yml @elastic/kibana-security -/src/dev/eslint/security_eslint_rule_tests.ts @elastic/kibana-security -/src/core/server/integration_tests/config/check_dynamic_config.test.ts @elastic/kibana-security -/src/plugins/telemetry/server/config/telemetry_labels.ts @elastic/kibana-security -/packages/kbn-std/src/is_internal_url.test.ts @elastic/kibana-core @elastic/kibana-security -/packages/kbn-std/src/is_internal_url.ts @elastic/kibana-core @elastic/kibana-security -/packages/kbn-std/src/parse_next_url.test.ts @elastic/kibana-core @elastic/kibana-security -/packages/kbn-std/src/parse_next_url.ts @elastic/kibana-core @elastic/kibana-security -/test/interactive_setup_api_integration/ @elastic/kibana-security -/test/interactive_setup_functional/ @elastic/kibana-security -/test/plugin_functional/plugins/hardening @elastic/kibana-security -/test/plugin_functional/test_suites/core_plugins/rendering.ts @elastic/kibana-security -/test/plugin_functional/test_suites/hardening @elastic/kibana-security -/x-pack/test/accessibility/apps/group1/login_page.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/group1/roles.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/group1/spaces.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/group1/users.ts @elastic/kibana-security -/x-pack/test/api_integration/apis/security/ @elastic/kibana-security -/x-pack/test/api_integration/apis/spaces/ @elastic/kibana-security -/x-pack/test/ui_capabilities/ @elastic/kibana-security -/x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security -/x-pack/test/functional/apps/security/ @elastic/kibana-security -/x-pack/test/functional/apps/spaces/ @elastic/kibana-security -/x-pack/test/security_api_integration/ @elastic/kibana-security -/x-pack/test/security_functional/ @elastic/kibana-security -/x-pack/test/spaces_api_integration/ @elastic/kibana-security -/x-pack/test/saved_object_api_integration/ @elastic/kibana-security -/x-pack/test_serverless/**/test_suites/common/platform_security/ @elastic/kibana-security -/x-pack/test_serverless/**/test_suites/search/platform_security/ @elastic/kibana-security -/x-pack/test_serverless/**/test_suites/security/platform_security/ @elastic/kibana-security -/x-pack/test_serverless/**/test_suites/observability/platform_security/ @elastic/kibana-security -/packages/core/http/core-http-server-internal/src/cdn_config/ @elastic/kibana-security @elastic/kibana-core -#CC# /x-pack/plugins/security/ @elastic/kibana-security - -# Response Ops team -/x-pack/test/accessibility/apps/group3/rules_connectors.ts @elastic/response-ops -/x-pack/test/functional/es_archives/cases/default @elastic/response-ops -/x-pack/test_serverless/api_integration/test_suites/observability/config.ts @elastic/response-ops -/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @elastic/response-ops -/x-pack/test_serverless/functional/page_objects/svl_triggers_actions_ui_page.ts @elastic/response-ops -/x-pack/test_serverless/functional/page_objects/svl_rule_details_ui_page.ts @elastic/response-ops -/x-pack/test_serverless/functional/page_objects/svl_oblt_overview_page.ts @elastic/response-ops -/x-pack/test/alerting_api_integration/ @elastic/response-ops -/x-pack/test/alerting_api_integration/observability @elastic/obs-ux-management-team -/x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/response-ops -/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/response-ops -/x-pack/test/task_manager_claimer_mget/ @elastic/response-ops -/docs/user/alerting/ @elastic/response-ops -/docs/management/connectors/ @elastic/response-ops -/x-pack/test/cases_api_integration/ @elastic/response-ops -/x-pack/test/functional/services/cases/ @elastic/response-ops -/x-pack/test/functional_with_es_ssl/apps/cases/ @elastic/response-ops -/x-pack/test/api_integration/apis/cases/ @elastic/response-ops -/x-pack/test_serverless/functional/test_suites/observability/cases @elastic/response-ops -/x-pack/test_serverless/functional/test_suites/search/cases/ @elastic/response-ops -/x-pack/test_serverless/functional/test_suites/security/ftr/cases/ @elastic/response-ops -/x-pack/test_serverless/api_integration/test_suites/search/cases/ @elastic/response-ops -/x-pack/test_serverless/api_integration/test_suites/observability/cases/ @elastic/response-ops -/x-pack/test_serverless/api_integration/test_suites/security/cases/ @elastic/response-ops -/x-pack/test_serverless/functional/test_suites/search/screenshot_creation/response_ops_docs @elastic/response-ops -/x-pack/test_serverless/functional/test_suites/security/screenshot_creation/response_ops_docs @elastic/response-ops -/x-pack/test_serverless/functional/test_suites/observability/screenshot_creation/response_ops_docs @elastic/response-ops -/x-pack/test_serverless/api_integration/test_suites/common/alerting/ @elastic/response-ops -/x-pack/test/functional/es_archives/action_task_params @elastic/response-ops -/x-pack/test/functional/es_archives/actions @elastic/response-ops -/x-pack/test/functional/es_archives/alerting @elastic/response-ops -/x-pack/test/functional/es_archives/alerts @elastic/response-ops -/x-pack/test/functional/es_archives/alerts_legacy @elastic/response-ops -/x-pack/test/functional/es_archives/observability/alerts @elastic/response-ops -/x-pack/test/functional/es_archives/actions @elastic/response-ops -/x-pack/test/functional/es_archives/rules_scheduled_task_id @elastic/response-ops -/x-pack/test/functional/es_archives/alerting/8_2_0 @elastic/response-ops -/x-pack/test/functional/es_archives/cases/signals/default @elastic/response-ops -/x-pack/test/functional/es_archives/cases/signals/hosts_users @elastic/response-ops - -# Enterprise Search -/x-pack/test_serverless/functional/page_objects/svl_ingest_pipelines.ts @elastic/search-kibana -/x-pack/test/functional/apps/dev_tools/embedded_console.ts @elastic/search-kibana -/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @elastic/search-kibana -/x-pack/test/functional/page_objects/embedded_console.ts @elastic/search-kibana -/x-pack/test/functional_enterprise_search/ @elastic/search-kibana -/x-pack/plugins/enterprise_search/public/applications/shared/doc_links @elastic/platform-docs -/x-pack/test_serverless/api_integration/test_suites/search/serverless_search @elastic/search-kibana -/x-pack/test_serverless/functional/test_suites/search/ @elastic/search-kibana -/x-pack/test_serverless/functional/test_suites/search/config.ts @elastic/search-kibana @elastic/appex-qa -x-pack/test/api_integration/apis/management/index_management/inference_endpoints.ts @elastic/search-kibana -/x-pack/test_serverless/api_integration/test_suites/search @elastic/search-kibana -/x-pack/test_serverless/functional/page_objects/svl_api_keys.ts @elastic/search-kibana -/x-pack/test_serverless/functional/page_objects/svl_search_* @elastic/search-kibana -/x-pack/test/functional_search/ @elastic/search-kibana - -# Management Experience - Deployment Management -/x-pack/test/api_integration/services/index_management.ts @elastic/kibana-management -/x-pack/test/functional/services/grok_debugger.js @elastic/kibana-management -/x-pack/test/functional/apps/grok_debugger @elastic/kibana-management -/x-pack/test/functional/apps/index_lifecycle_management @elastic/kibana-management -/x-pack/test/functional/apps/index_management @elastic/kibana-management -/x-pack/test/api_integration/services/ingest_pipelines @elastic/kibana-management -/x-pack/test/functional/apps/watcher @elastic/kibana-management -/x-pack/test/api_integration/apis/watcher @elastic/kibana-management -/x-pack/test/api_integration/apis/upgrade_assistant @elastic/kibana-management -/x-pack/test/api_integration/apis/searchprofiler @elastic/kibana-management -/x-pack/test/api_integration/apis/console @elastic/kibana-management -/x-pack/test_serverless/**/test_suites/common/index_management/ @elastic/kibana-management -/x-pack/test_serverless/**/test_suites/common/management/index_management/ @elastic/kibana-management -/x-pack/test_serverless/**/test_suites/common/painless_lab/ @elastic/kibana-management -/x-pack/test_serverless/**/test_suites/common/console/ @elastic/kibana-management -/x-pack/test_serverless/api_integration/test_suites/common/management/ @elastic/kibana-management -/x-pack/test_serverless/api_integration/test_suites/common/search_profiler/ @elastic/kibana-management -/x-pack/test_serverless/functional/test_suites/**/advanced_settings.ts @elastic/kibana-management -/x-pack/test_serverless/functional/test_suites/common/management/disabled_uis.ts @elastic/kibana-management -/x-pack/test_serverless/functional/test_suites/common/management/ingest_pipelines.ts @elastic/kibana-management -/x-pack/test_serverless/functional/test_suites/common/management/landing_page.ts @elastic/kibana-management -/x-pack/test_serverless/functional/test_suites/common/dev_tools/ @elastic/kibana-management -/x-pack/test_serverless/**/test_suites/common/grok_debugger/ @elastic/kibana-management -/x-pack/test/api_integration/apis/management/ @elastic/kibana-management -/x-pack/test/functional/apps/rollup_job/ @elastic/kibana-management -/x-pack/test/api_integration/apis/grok_debugger @elastic/kibana-management -/x-pack/test/accessibility/apps/group1/advanced_settings.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/**/grok_debugger.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group1/helpers.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group1/home.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group1/index_lifecycle_management.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group1/ingest_node_pipelines.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group1/management.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group1/painless_lab.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group1/search_profiler.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group3/cross_cluster_replication.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group3/license_management.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group3/remote_clusters.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group3/rollup_jobs.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts @elastic/kibana-management -/x-pack/test/accessibility/apps/group3/watcher.ts @elastic/kibana-management - -#CC# /x-pack/plugins/cross_cluster_replication/ @elastic/kibana-management - -# Security Solution -/x-pack/test/common/services/security_solution @elastic/security-solution -/x-pack/test/api_integration/services/security_solution_*.gen.ts @elastic/security-solution -/x-pack/test/accessibility/apps/group3/security_solution.ts @elastic/security-solution -/x-pack/test_serverless/functional/test_suites/security/config.ts @elastic/security-solution @elastic/appex-qa -/x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts @elastic/security-solution -/x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts @elastic/security-solution -/x-pack/test_serverless/functional/test_suites/common/spaces/multiple_spaces_enabled.ts @elastic/security-solution -/x-pack/test/functional/es_archives/endpoint/ @elastic/security-solution -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution -/x-pack/test/security_solution_api_integration @elastic/security-solution -/x-pack/test/api_integration/apis/security_solution @elastic/security-solution -/x-pack/test/functional/es_archives/auditbeat/default @elastic/security-solution -/x-pack/test/functional/es_archives/auditbeat/hosts @elastic/security-solution -/x-pack/test_serverless/functional/page_objects/svl_management_page.ts @elastic/security-solution -/x-pack/test_serverless/api_integration/test_suites/security @elastic/security-solution - -/x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts @elastic/security-solution -/x-pack/test_serverless/functional/test_suites/security/index.ts @elastic/security-solution -#CC# /x-pack/plugins/security_solution/ @elastic/security-solution -/x-pack/test/functional/es_archives/cases/signals/duplicate_ids @elastic/response-ops - -# Security Solution OpenAPI bundles -/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_* @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_* @elastic/security-defend-workflows -/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_* @elastic/security-entity-analytics -/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_* @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_* @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_* @elastic/security-defend-workflows -/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_* @elastic/security-entity-analytics -/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_* @elastic/security-threat-hunting-investigations - -# Security Solution Offering plugins -# TODO: assign sub directories to sub teams -/x-pack/plugins/security_solution_ess/ @elastic/security-solution -/x-pack/plugins/security_solution_serverless/ @elastic/security-solution - -# GenAI in Security Solution -/x-pack/plugins/security_solution/public/assistant @elastic/security-generative-ai -/x-pack/plugins/security_solution/public/attack_discovery @elastic/security-generative-ai -/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant @elastic/security-generative-ai - -# Security Solution cross teams ownership -/x-pack/test/security_solution_cypress/cypress/fixtures @elastic/security-detections-response @elastic/security-threat-hunting -/x-pack/test/security_solution_cypress/cypress/helpers @elastic/security-detections-response @elastic/security-threat-hunting -/x-pack/test/security_solution_cypress/cypress/objects @elastic/security-detections-response @elastic/security-threat-hunting -/x-pack/test/security_solution_cypress/cypress/plugins @elastic/security-detections-response @elastic/security-threat-hunting -/x-pack/test/security_solution_cypress/cypress/screens/common @elastic/security-detections-response @elastic/security-threat-hunting -/x-pack/test/security_solution_cypress/cypress/support @elastic/security-detections-response @elastic/security-threat-hunting -/x-pack/test/security_solution_cypress/cypress/urls @elastic/security-threat-hunting-investigations @elastic/security-detection-engine - -/x-pack/plugins/security_solution/common/ecs @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/common/test @elastic/security-detections-response @elastic/security-threat-hunting - -/x-pack/plugins/security_solution/public/common/components/callouts @elastic/security-detections-response -/x-pack/plugins/security_solution/public/common/components/hover_actions @elastic/security-threat-hunting-explore @elastic/security-threat-hunting-investigations - -/x-pack/plugins/security_solution/server/routes @elastic/security-detections-response @elastic/security-threat-hunting -/x-pack/plugins/security_solution/server/utils @elastic/security-detections-response @elastic/security-threat-hunting -x-pack/test/security_solution_api_integration/test_suites/detections_response/utils @elastic/security-detections-response -x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry @elastic/security-detections-response -x-pack/test/security_solution_api_integration/test_suites/detections_response/user_roles @elastic/security-detections-response -x-pack/test/security_solution_api_integration/test_suites/explore @elastic/security-threat-hunting-explore -x-pack/test/security_solution_api_integration/test_suites/investigations @elastic/security-threat-hunting-investigations -x-pack/test/security_solution_api_integration/test_suites/sources @elastic/security-detections-response -/x-pack/test/common/utils/security_solution/detections_response @elastic/security-detections-response - -# Security Solution sub teams - -## Security Solution sub teams - security-engineering-productivity -## NOTE: It's important to keep this above other teams' sections because test automation doesn't process -## the CODEOWNERS file correctly. See https://github.com/elastic/kibana/issues/173307#issuecomment-1855858929 -/x-pack/test/security_solution_cypress/* @elastic/security-engineering-productivity -/x-pack/test/security_solution_cypress/cypress/* @elastic/security-engineering-productivity -/x-pack/test/security_solution_cypress/cypress/tasks/login.ts @elastic/security-engineering-productivity -/x-pack/test/security_solution_cypress/es_archives @elastic/security-engineering-productivity -/x-pack/test/security_solution_playwright @elastic/security-engineering-productivity -/x-pack/plugins/security_solution/scripts/run_cypress @MadameSheema @patrykkopycinski @maximpn @banderror - -## Security Solution sub teams - Threat Hunting - -/x-pack/plugins/security_solution/server/lib/siem_migrations @elastic/security-threat-hunting -/x-pack/plugins/security_solution/common/siem_migrations @elastic/security-threat-hunting - -## Security Solution Threat Hunting areas - Threat Hunting Investigations - -/x-pack/plugins/security_solution/common/api/timeline @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/common/search_strategy/timeline @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/common/types/timeline @elastic/security-threat-hunting-investigations - -/x-pack/test/security_solution_cypress/cypress/e2e/investigations @elastic/security-threat-hunting-investigations -/x-pack/test/security_solution_cypress/cypress/e2e/sourcerer/sourcerer_timeline.cy.ts @elastic/security-threat-hunting-investigations - -x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout @elastic/security-threat-hunting-investigations -x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/security-threat-hunting-investigations - -/x-pack/plugins/security_solution/common/timelines @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/common/components/alerts_viewer @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_action @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/common/components/event_details @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/common/components/events_viewer @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/common/components/markdown_editor @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/detections/components/alerts_kpis @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/detections/components/alerts_table @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/detections/components/alerts_info @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/flyout/document_details @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/flyout/shared @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/notes @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/resolver @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/threat_intelligence @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/timelines @elastic/security-threat-hunting-investigations - -/x-pack/plugins/security_solution/server/lib/timeline @elastic/security-threat-hunting-investigations - -## Security Solution Threat Hunting areas - Threat Hunting Explore -/x-pack/plugins/security_solution/common/api/tags @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/common/search_strategy/security_solution/network @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/common/search_strategy/security_solution/user @elastic/security-threat-hunting-explore - -/x-pack/test/security_solution_cypress/cypress/e2e/explore @elastic/security-threat-hunting-explore -/x-pack/test/security_solution_cypress/cypress/screens/hosts @elastic/security-threat-hunting-explore -/x-pack/test/security_solution_cypress/cypress/screens/network @elastic/security-threat-hunting-explore -/x-pack/test/security_solution_cypress/cypress/tasks/hosts @elastic/security-threat-hunting-explore -/x-pack/test/security_solution_cypress/cypress/tasks/network @elastic/security-threat-hunting-explore - -/x-pack/plugins/security_solution/public/app/actions @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/charts @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/header_page @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/header_section @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/inspect @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/last_event_time @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/links @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/matrix_histogram @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/navigation @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/news_feed @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/overview_description_list @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/page @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/sidebar_header @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/tables @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/top_n @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/components/with_hover_actions @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/containers/matrix_histogram @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/common/lib/cell_actions @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/cases @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/explore @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/overview @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/public/dashboards @elastic/security-threat-hunting-explore - -/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network @elastic/security-threat-hunting-explore -/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users @elastic/security-threat-hunting-explore - -/x-pack/test/functional/es_archives/auditbeat/overview @elastic/security-threat-hunting-explore -/x-pack/test/functional/es_archives/auditbeat/users @elastic/security-threat-hunting-explore - -/x-pack/test/functional/es_archives/auditbeat/uncommon_processes @elastic/security-threat-hunting-explore - -## Generative AI owner connectors -# OpenAI -/x-pack/plugins/stack_connectors/public/connector_types/openai @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra -/x-pack/plugins/stack_connectors/server/connector_types/openai @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra -/x-pack/plugins/stack_connectors/common/openai @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra -# Bedrock -/x-pack/plugins/stack_connectors/public/connector_types/bedrock @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra -/x-pack/plugins/stack_connectors/server/connector_types/bedrock @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra -/x-pack/plugins/stack_connectors/common/bedrock @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra - -# Gemini -/x-pack/plugins/stack_connectors/public/connector_types/gemini @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra -/x-pack/plugins/stack_connectors/server/connector_types/gemini @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra -/x-pack/plugins/stack_connectors/common/gemini @elastic/security-generative-ai @elastic/obs-ai-assistant @elastic/appex-ai-infra - -# Inference API -/x-pack/plugins/stack_connectors/public/connector_types/inference @elastic/appex-ai-infra @elastic/security-generative-ai @elastic/obs-ai-assistant -/x-pack/plugins/stack_connectors/server/connector_types/inference @elastic/appex-ai-infra @elastic/security-generative-ai @elastic/obs-ai-assistant -/x-pack/plugins/stack_connectors/common/inference @elastic/appex-ai-infra @elastic/security-generative-ai @elastic/obs-ai-assistant - -## Defend Workflows owner connectors -/x-pack/plugins/stack_connectors/public/connector_types/sentinelone @elastic/security-defend-workflows -/x-pack/plugins/stack_connectors/server/connector_types/sentinelone @elastic/security-defend-workflows -/x-pack/plugins/stack_connectors/common/sentinelone @elastic/security-defend-workflows -/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike @elastic/security-defend-workflows -/x-pack/plugins/stack_connectors/common/crowdstrike @elastic/security-defend-workflows - -## Security Solution shared OAS schemas -/x-pack/plugins/security_solution/common/api/model @elastic/security-detection-rule-management @elastic/security-detection-engine - -## Security Solution sub teams - Detection Rule Management -/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/common/api/detection_engine/rule_management @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/common/detection_engine/rule_management @elastic/security-detection-rule-management - -/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/docs/rfcs/detection_response @elastic/security-detection-rule-management @elastic/security-detection-engine -/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/rule_management @elastic/security-detection-rule-management -/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management @elastic/security-detection-rule-management - -/x-pack/plugins/security_solution/public/common/components/health_truncate_text @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/common/components/links_to_docs @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/common/components/ml_popover @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/common/components/popover_items @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detection_engine/rule_management @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detections/components/callouts @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detections/components/rules @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detections/mitre @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/rules @elastic/security-detection-rule-management - -/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine - -/x-pack/plugins/security_solution/scripts/openapi @elastic/security-detection-rule-management - -## Security Solution sub teams - Detection Engine -/x-pack/plugins/security_solution/common/api/detection_engine/alert_tags @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/api/detection_engine/index_management @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/api/detection_engine/signals @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/api/detection_engine/signals_migration @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/cti @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/field_maps @elastic/security-detection-engine -/x-pack/test/functional/es_archives/entity/risks @elastic/security-detection-engine -/x-pack/test/functional/es_archives/entity/host_risk @elastic/security-detection-engine -/x-pack/test/api_integration/apis/lists @elastic/security-detection-engine - -/x-pack/plugins/security_solution/public/sourcerer @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/public/detection_engine/rule_creation @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/detection_engine/rule_gaps @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/detections/pages/alerts @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/exceptions @elastic/security-detection-engine - -/x-pack/plugins/security_solution/server/lib/detection_engine/migrations @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals @elastic/security-detection-engine - -/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine @elastic/security-detection-engine - -/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine @elastic/security-detection-engine -/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/rule_gaps.ts @elastic/security-detection-engine -/x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists @elastic/security-detection-engine -/x-pack/test/functional/es_archives/asset_criticality @elastic/security-detection-engine - -## Security Threat Intelligence - Under Security Platform -/x-pack/plugins/security_solution/public/common/components/threat_match @elastic/security-detection-engine - -## Security Solution sub teams - security-defend-workflows -/x-pack/test/api_integration/apis/osquery @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/management/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/common/lib/endpoint/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/common/components/endpoint/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/common/hooks/endpoint/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/common/mock/endpoint @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/common/endpoint/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/common/api/endpoint/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/server/endpoint/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/server/lists_integration/endpoint/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/server/lib/license/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/server/fleet_integration/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution/scripts/endpoint/ @elastic/security-defend-workflows -/x-pack/test/security_solution_endpoint/ @elastic/security-defend-workflows -/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/ @elastic/security-defend-workflows -/x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows -/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management @elastic/security-defend-workflows -/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management @elastic/security-defend-workflows -/x-pack/plugins/security_solution_serverless/server/endpoint @elastic/security-defend-workflows - -## Security Solution sub teams - security-telemetry (Data Engineering) -x-pack/plugins/security_solution/server/usage/ @elastic/security-data-analytics -x-pack/plugins/security_solution/server/lib/telemetry/ @elastic/security-data-analytics - -## Security Solution sub teams - adaptive-workload-protection -x-pack/plugins/security_solution/public/common/components/sessions_viewer @elastic/kibana-cloud-security-posture -x-pack/plugins/security_solution/public/kubernetes @elastic/kibana-cloud-security-posture - -## Security Solution sub teams - Entity Analytics -x-pack/plugins/security_solution/common/entity_analytics @elastic/security-entity-analytics -x-pack/plugins/security_solution/common/search_strategy/security_solution/risk_score @elastic/security-entity-analytics -x-pack/plugins/security_solution/public/entity_analytics @elastic/security-entity-analytics -x-pack/plugins/security_solution/server/lib/entity_analytics @elastic/security-entity-analytics -x-pack/plugins/security_solution/server/lib/risk_score @elastic/security-entity-analytics -x-pack/test/security_solution_api_integration/test_suites/entity_analytics @elastic/security-entity-analytics -x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics @elastic/security-entity-analytics -x-pack/plugins/security_solution/public/flyout/entity_details @elastic/security-entity-analytics -x-pack/plugins/security_solution/common/api/entity_analytics @elastic/security-entity-analytics - -## Security Solution sub teams - GenAI -x-pack/test/security_solution_api_integration/test_suites/genai @elastic/security-generative-ai - -# Security Defend Workflows - OSQuery Ownership -x-pack/plugins/osquery @elastic/security-defend-workflows -/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_response_actions @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions @elastic/security-defend-workflows -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions @elastic/security-defend-workflows -/x-pack/plugins/security_solution/public/detections/components/osquery @elastic/security-defend-workflows - -# Cloud Defend -/x-pack/plugins/cloud_defend/ @elastic/kibana-cloud-security-posture -/x-pack/plugins/security_solution/public/cloud_defend @elastic/kibana-cloud-security-posture - -# Cloud Security Posture -/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.* @elastic/kibana-cloud-security-posture -/x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture -/x-pack/test/api_integration/apis/cloud_security_posture/ @elastic/kibana-cloud-security-posture -/x-pack/test/cloud_security_posture_functional/ @elastic/kibana-cloud-security-posture -/x-pack/test/cloud_security_posture_api/ @elastic/kibana-cloud-security-posture -/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/ @elastic/kibana-cloud-security-posture -/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.basic.ts @elastic/kibana-cloud-security-posture -/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.essentials.ts @elastic/kibana-cloud-security-posture -/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/ @elastic/kibana-cloud-security-posture -/x-pack/plugins/fleet/public/components/cloud_security_posture @elastic/fleet @elastic/kibana-cloud-security-posture -/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/cloud_security_posture @elastic/fleet @elastic/kibana-cloud-security-posture -/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.* @elastic/fleet @elastic/kibana-cloud-security-posture -/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/cloud_posture_third_party_support_callout.* @elastic/fleet @elastic/kibana-cloud-security-posture -/x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture -/x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/misconfiguration_contextual_flyout.cy.ts @elastic/kibana-cloud-security-posture -/x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/vulnerabilities_contextual_flyout.cy.ts @elastic/kibana-cloud-security-posture - -# Security Solution onboarding tour -/x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/security-threat-hunting-explore - -# Security Service Integrations -x-pack/plugins/security_solution/common/security_integrations @elastic/security-service-integrations -x-pack/plugins/security_solution/public/security_integrations @elastic/security-service-integrations -x-pack/plugins/security_solution/server/security_integrations @elastic/security-service-integrations -x-pack/plugins/security_solution/server/lib/security_integrations @elastic/security-service-integrations - -# Kibana design -# scss overrides should be below this line for specificity -**/*.scss @elastic/kibana-design - -# Observability design -/x-pack/plugins/fleet/**/*.scss @elastic/observability-design -/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design - -# Ent. Search design -/x-pack/plugins/enterprise_search/**/*.scss @elastic/search-design -/x-pack/test/accessibility/apps/group3/enterprise_search.ts @elastic/search-kibana - -# Security design -/x-pack/plugins/endpoint/**/*.scss @elastic/security-design -/x-pack/plugins/security_solution/**/*.scss @elastic/security-design -/x-pack/plugins/security_solution_ess/**/*.scss @elastic/security-design -/x-pack/plugins/security_solution_serverless/**/*.scss @elastic/security-design - -# Logstash -/x-pack/test/api_integration/apis/logstash @elastic/logstash -#CC# /x-pack/plugins/logstash/ @elastic/logstash - -# EUI team -/src/plugins/kibana_react/public/page_template/ @elastic/eui-team @elastic/appex-sharedux - -# Landing page for guided onboarding in Home plugin -/src/plugins/home/public/application/components/guided_onboarding @elastic/appex-sharedux - -# Changes to translation files should not ping code reviewers -x-pack/plugins/translations/translations - -# Profiling api integration testing -x-pack/test/profiling_api_integration @elastic/obs-ux-infra_services-team - -# Observability shared profiling -x-pack/plugins/observability_solution/observability_shared/public/components/profiling @elastic/obs-ux-infra_services-team - -# Shared UX -/x-pack/test/api_integration/apis/content_management @elastic/appex-sharedux -/x-pack/test/accessibility/apps/group3/tags.ts @elastic/appex-sharedux -/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts @elastic/appex-sharedux -/x-pack/test_serverless/functional/test_suites/common/spaces/spaces_selection.ts @elastic/appex-sharedux -/x-pack/test_serverless/functional/test_suites/common/spaces/index.ts @elastic/appex-sharedux -packages/react @elastic/appex-sharedux -test/functional/page_objects/solution_navigation.ts @elastic/appex-sharedux -/x-pack/test_serverless/functional/page_objects/svl_common_navigation.ts @elastic/appex-sharedux -/x-pack/test_serverless/functional/fixtures/kbn_archiver/reporting @elastic/appex-sharedux -/x-pack/test_serverless/functional/page_objects/svl_sec_landing_page.ts @elastic/appex-sharedux -/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts @elastic/appex-sharedux - -# OpenAPI spec files -oas_docs/.spectral.yaml @elastic/platform-docs -oas_docs/kibana.info.serverless.yaml @elastic/platform-docs -oas_docs/kibana.info.yaml @elastic/platform-docs - -# Plugin manifests -/src/plugins/**/kibana.jsonc @elastic/kibana-core -/x-pack/plugins/**/kibana.jsonc @elastic/kibana-core - -# Temporary Encrypted Saved Objects (ESO) guarding -# This additional code-ownership is meant to be a temporary precaution to notify the Kibana platform security team -# when an encrypted saved object is changed. Very careful review is necessary to ensure any changes are compatible -# with serverless zero downtime upgrades (ZDT). This section should be removed only when proper guidance for -# maintaining ESOs has been documented and consuming teams have acclimated to ZDT changes. -x-pack/plugins/actions/server/saved_objects/index.ts @elastic/response-ops @elastic/kibana-security -x-pack/plugins/alerting/server/saved_objects/index.ts @elastic/response-ops @elastic/kibana-security -x-pack/plugins/fleet/server/saved_objects/index.ts @elastic/fleet @elastic/kibana-security -x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts @elastic/obs-ux-management-team @elastic/kibana-security -x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics_monitor.ts @elastic/obs-ux-management-team @elastic/kibana-security -x-pack/plugins/observability_solution/synthetics/server/saved_objects/synthetics_param.ts @elastic/obs-ux-management-team @elastic/kibana-security - -# Specialised GitHub workflows for the Observability robots -/.github/workflows/deploy-my-kibana.yml @elastic/observablt-robots @elastic/kibana-operations -/.github/workflows/oblt-github-commands @elastic/observablt-robots @elastic/kibana-operations -/.github/workflows/undeploy-my-kibana.yml @elastic/observablt-robots @elastic/kibana-operations - -#### -## These rules are always last so they take ultimate priority over everything else -#### From 37e030a714deed6cf29c8bd4003f56ac239d5e23 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:27:29 +1100 Subject: [PATCH 20/47] [8.x] Allow empty spaces un Gsub processor (#197815) (#199293) # Backport This will backport the following commits from `main` to `8.x`: - [Allow empty spaces un Gsub processor (#197815)](https://github.com/elastic/kibana/pull/197815) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sonia Sanz Vivas --- .../field_validators/empty_field.test.ts | 48 +++++++++++++++++++ .../helpers/field_validators/empty_field.ts | 6 ++- .../static/validators/string/is_empty.ts | 3 +- .../processor_form/processors/gsub.tsx | 3 +- 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 src/plugins/es_ui_shared/static/forms/helpers/field_validators/empty_field.test.ts diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/empty_field.test.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/empty_field.test.ts new file mode 100644 index 0000000000000..e05f7c86b8a60 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/empty_field.test.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ValidationFuncArg } from '../../hook_form_lib'; +import { emptyField } from './empty_field'; + +describe('emptyField', () => { + const message = 'test error message'; + const code = 'ERR_FIELD_MISSING'; + const path = 'path'; + + const validator = (value: string | any[], trimString?: boolean) => + emptyField(message, trimString)({ value, path } as ValidationFuncArg); + + test('should return Validation function if value is an empty string and trimString is true', () => { + expect(validator('')).toMatchObject({ message, code, path }); + }); + + test('should return Validation function if value is an empty string and trimString is false', () => { + expect(validator('', false)).toMatchObject({ message, code, path }); + }); + + test('should return Validation function if value is a space and trimString is true', () => { + expect(validator(' ')).toMatchObject({ message, code, path }); + }); + + test('should return undefined if value is a space and trimString is false', () => { + expect(validator(' ', false)).toBeUndefined(); + }); + + test('should return undefined if value is a string and is not empty', () => { + expect(validator('not Empty')).toBeUndefined(); + }); + + test('should return undefined if value an array and is not empty', () => { + expect(validator(['not Empty'])).toBeUndefined(); + }); + + test('should return undefined if value an array and is empty', () => { + expect(validator([])).toMatchObject({ message, code, path }); + }); +}); diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/empty_field.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/empty_field.ts index 3b09e165984d4..9917b273d666c 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/empty_field.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/empty_field.ts @@ -13,12 +13,14 @@ import { isEmptyArray } from '../../../validators/array'; import { ERROR_CODE } from './types'; export const emptyField = - (message: string) => + (message: string, trimString: boolean = true) => (...args: Parameters): ReturnType> => { const [{ value, path }] = args; if (typeof value === 'string') { - return isEmptyString(value) ? { code: 'ERR_FIELD_MISSING', path, message } : undefined; + return isEmptyString(value, trimString) + ? { code: 'ERR_FIELD_MISSING', path, message } + : undefined; } if (Array.isArray(value)) { diff --git a/src/plugins/es_ui_shared/static/validators/string/is_empty.ts b/src/plugins/es_ui_shared/static/validators/string/is_empty.ts index f70cbd36213ed..197d707f5edbf 100644 --- a/src/plugins/es_ui_shared/static/validators/string/is_empty.ts +++ b/src/plugins/es_ui_shared/static/validators/string/is_empty.ts @@ -7,4 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export const isEmptyString = (value: string) => value.trim() === ''; +export const isEmptyString = (value: string, trimString: boolean = true) => + (trimString ? value.trim() : value) === ''; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx index 7e72848485c11..8e12be6880d00 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx @@ -37,7 +37,8 @@ const fieldsConfig: FieldsConfig = { validator: emptyField( i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError', { defaultMessage: 'A value is required.', - }) + }), + false ), }, { From 704c4fd2e1435ecf048b5a4197c302ac8983abe1 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 01:29:44 +1100 Subject: [PATCH 21/47] [8.x] [ResponseOps][Connectors] Allow to use POST method for get case information in case management webhook (#197437) (#199307) # Backport This will backport the following commits from `main` to `8.x`: - [[ResponseOps][Connectors] Allow to use POST method for get case information in case management webhook (#197437)](https://github.com/elastic/kibana/pull/197437) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> --- .../connector_types.test.ts.snap | 72 +++++ .../stack_connectors/common/auth/constants.ts | 1 + .../cases_webhook/steps/get.tsx | 252 ++++++++++------ .../cases_webhook/translations.ts | 35 ++- .../cases_webhook/validator.ts | 29 +- .../cases_webhook/webhook_connectors.test.tsx | 74 +++++ .../cases_webhook/webhook_connectors.tsx | 4 +- .../connector_types/cases_webhook/schema.ts | 7 + .../cases_webhook/service.test.ts | 269 ++++++++++++++++++ .../connector_types/cases_webhook/service.ts | 24 ++ .../actions/connector_types/cases_webhook.ts | 98 ++++++- .../tests/trial/configure/get_connectors.ts | 2 + .../tests/trial/configure/get_connectors.ts | 2 + 13 files changed, 775 insertions(+), 94 deletions(-) diff --git a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index d778849347d18..fad093938de40 100644 --- a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -2251,6 +2251,78 @@ Object { ], "type": "string", }, + "getIncidentJson": Object { + "flags": Object { + "default": null, + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + Object { + "schema": Object { + "allow": Array [ + null, + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "type": "alternatives", + }, + "getIncidentMethod": Object { + "flags": Object { + "default": "get", + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "allow": Array [ + "get", + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + Object { + "schema": Object { + "allow": Array [ + "post", + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "type": "alternatives", + }, "getIncidentResponseExternalTitleKey": Object { "flags": Object { "error": [Function], diff --git a/x-pack/plugins/stack_connectors/common/auth/constants.ts b/x-pack/plugins/stack_connectors/common/auth/constants.ts index bdd5b7352f921..ecf7637c956ee 100644 --- a/x-pack/plugins/stack_connectors/common/auth/constants.ts +++ b/x-pack/plugins/stack_connectors/common/auth/constants.ts @@ -19,4 +19,5 @@ export enum WebhookMethods { PATCH = 'patch', POST = 'post', PUT = 'put', + GET = 'get', } diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx index e8f233408a4c9..5bf2689506ec4 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx @@ -6,14 +6,25 @@ */ import React, { FunctionComponent } from 'react'; +import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { + FIELD_TYPES, + UseField, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; -import { MustacheTextFieldWrapper } from '@kbn/triggers-actions-ui-plugin/public'; -import { containsExternalId, containsExternalIdOrTitle } from '../validator'; +import { JsonFieldWrapper, MustacheTextFieldWrapper } from '@kbn/triggers-actions-ui-plugin/public'; +import { WebhookMethods } from '../../../../common/auth/constants'; +import { + containsExternalIdForGet, + containsExternalIdOrTitle, + requiredJsonForPost, +} from '../validator'; import { urlVars, urlVarsExt } from '../action_variables'; import * as i18n from '../translations'; + const { emptyField, urlField } = fieldValidators; interface Props { @@ -21,88 +32,157 @@ interface Props { readOnly: boolean; } -export const GetStep: FunctionComponent = ({ display, readOnly }) => ( - - -

{i18n.STEP_3}

- -

{i18n.STEP_3_DESCRIPTION}

-
-
- - - - - - = ({ display, readOnly }) => { + const [{ config }] = useFormData({ + watch: ['config.getIncidentMethod'], + }); + const { getIncidentMethod = WebhookMethods.GET } = config ?? {}; + + return ( + + +

{i18n.STEP_3}

+ +

{i18n.STEP_3_DESCRIPTION}

+
+
+ + + + ({ + text: verb.toUpperCase(), + value: verb, + })), + readOnly, + }, + }} + /> + + + + + + {getIncidentMethod === WebhookMethods.POST ? ( + + + + ) : null} + + - - - + + + - - -
-); + }} + /> +
+
+
+ ); +}; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts index 0b007e07cfd91..8c44b6197ef9c 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts @@ -178,13 +178,24 @@ export const ADD_CASES_VARIABLE = i18n.translate( defaultMessage: 'Add variable', } ); - +export const GET_INCIDENT_METHOD = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.getIncidentMethodTextFieldLabel', + { + defaultMessage: 'Get case method', + } +); export const GET_INCIDENT_URL = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.getIncidentUrlTextFieldLabel', { defaultMessage: 'Get case URL', } ); +export const GET_METHOD_REQUIRED = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.error.requiredGetMethodText', + { + defaultMessage: 'Get case method is required.', + } +); export const GET_INCIDENT_URL_HELP = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.getIncidentUrlHelp', { @@ -206,6 +217,28 @@ export const GET_INCIDENT_TITLE_KEY_HELP = i18n.translate( } ); +export const GET_INCIDENT_JSON_HELP = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.getIncidentJsonHelp', + { + defaultMessage: + 'JSON object to get a case. Use the variable selector to add cases data to the payload.', + } +); + +export const GET_INCIDENT_JSON = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.getIncidentJsonTextFieldLabel', + { + defaultMessage: 'Get case object', + } +); + +export const GET_INCIDENT_REQUIRED = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.error.requiredGetIncidentText', + { + defaultMessage: 'Get case object is required and must be valid JSON.', + } +); + export const EXTERNAL_INCIDENT_VIEW_URL = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.viewIncidentUrlTextFieldLabel', { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts index d3d7f6dc8e612..d972c9bbd1f86 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types'; import { ValidationError, @@ -12,6 +13,7 @@ import { } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { containsChars, isUrl } from '@kbn/es-ui-shared-plugin/static/validators/string'; import { templateActionVariable } from '@kbn/triggers-actions-ui-plugin/public'; +import { WebhookMethods } from '../../../common/auth/constants'; import * as i18n from './translations'; import { casesVars, commentVars, urlVars, urlVarsExt } from './action_variables'; @@ -42,17 +44,20 @@ export const containsTitleAndDesc = } }; -export const containsExternalId = - () => +export const containsExternalIdForGet = + (method?: string) => (...args: Parameters): ReturnType> => { const [{ value, path }] = args; const id = templateActionVariable( urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! ); - return containsChars(id)(value as string).doesContain - ? undefined - : missingVariableErrorMessage(path, [id]); + + return method === WebhookMethods.GET && + value !== null && + !containsChars(id)(value as string).doesContain + ? missingVariableErrorMessage(path, [id]) + : undefined; }; export const containsExternalIdOrTitle = @@ -77,6 +82,20 @@ export const containsExternalIdOrTitle = return error; }; +export const requiredJsonForPost = + (method?: string) => + (...args: Parameters): ReturnType> => { + const [{ value, path }] = args; + + const error = { + code: errorCode, + path, + message: i18n.GET_INCIDENT_REQUIRED, + }; + + return method === WebhookMethods.POST && (value === null || isEmpty(value)) ? error : undefined; + }; + export const containsCommentsOrEmpty = (message: string) => (...args: Parameters): ReturnType> => { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx index 8df473fef2ae8..713f2bd9e6f83 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx @@ -42,6 +42,7 @@ const config = { headers: [{ key: 'content-type', value: 'text' }], viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}', + getIncidentMethod: 'get', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', @@ -536,5 +537,78 @@ describe('CasesWebhookActionConnectorFields renders', () => { ).toBeInTheDocument(); } ); + + it('validates get incident json required correctly', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + getIncidentUrl: 'https://coolsite.net/rest/api/2/issue', + getIncidentMethod: 'post', + headers: [], + }, + }; + + render( + + {}} + /> + + ); + + await userEvent.click(await screen.findByTestId('form-test-provide-submit')); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false })); + expect(await screen.findByText(i18n.GET_INCIDENT_REQUIRED)).toBeInTheDocument(); + }); + + it('validation succeeds get incident url with post correctly', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}', + getIncidentMethod: 'post', + getIncidentJson: '{"id": {{{external.system.id}}} }', + headers: [], + }, + }; + + const { isPreconfigured, ...rest } = actionConnector; + const { headers, ...rest2 } = actionConnector.config; + + render( + + {}} + /> + + ); + + await userEvent.click(await screen.findByTestId('form-test-provide-submit')); + + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith({ + data: { + __internal__: { + hasCA: false, + hasHeaders: true, + }, + ...rest, + config: { + ...rest2, + getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}', + getIncidentMethod: 'post', + getIncidentJson: '{"id": {{{external.system.id}}} }', + }, + }, + isValid: true, + }) + ); + }); }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.tsx index 73e424901469a..5aaf56fa8dd90 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.tsx @@ -22,7 +22,7 @@ import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; import * as i18n from './translations'; import { AuthStep, CreateStep, GetStep, UpdateStep } from './steps'; -export const HTTP_VERBS = ['post', 'put', 'patch']; +export const HTTP_VERBS = ['post', 'put', 'patch', 'get']; const fields = { step1: [ 'config.hasAuth', @@ -38,7 +38,9 @@ const fields = { 'config.createIncidentResponseKey', ], step3: [ + 'config.getIncidentMethod', 'config.getIncidentUrl', + 'config.getIncidentJson', 'config.getIncidentResponseExternalTitleKey', 'config.viewIncidentUrl', ], diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts index 00b4fdc60a3ab..25b0d66e885b4 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts @@ -21,7 +21,14 @@ export const ExternalIncidentServiceConfiguration = { ), createIncidentJson: schema.string(), // stringified object createIncidentResponseKey: schema.string(), + getIncidentMethod: schema.oneOf( + [schema.literal(WebhookMethods.GET), schema.literal(WebhookMethods.POST)], + { + defaultValue: WebhookMethods.GET, + } + ), getIncidentUrl: schema.string(), + getIncidentJson: schema.nullable(schema.string()), getIncidentResponseExternalTitleKey: schema.string(), viewIncidentUrl: schema.string(), updateIncidentUrl: schema.string(), diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts index a44b34bf88fce..aaeca30be920a 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts @@ -47,6 +47,8 @@ const config: CasesWebhookPublicConfigurationType = { headers: { ['content-type']: 'application/json', foo: 'bar' }, viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://coolsite.net/issue/{{{external.system.id}}}', + getIncidentMethod: WebhookMethods.GET, + getIncidentJson: null, updateIncidentJson: '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: WebhookMethods.PUT, @@ -239,6 +241,7 @@ describe('Cases webhook service', () => { configurationUtilities, sslOverrides: defaultSSLOverrides, connectorUsageCollector: expect.any(ConnectorUsageCollector), + method: WebhookMethods.GET, }); }); @@ -282,6 +285,7 @@ describe('Cases webhook service', () => { "trace": [MockFunction], "warn": [MockFunction], }, + "method": "get", "sslOverrides": Object { "cert": Object { "data": Array [ @@ -440,6 +444,271 @@ describe('Cases webhook service', () => { '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: Response is missing the expected field: key' ); }); + + it('it returns the incident correctly with POST', async () => { + const postService: ExternalService = createExternalService( + actionId, + { + config: { + ...config, + getIncidentMethod: WebhookMethods.POST, + getIncidentJson: '{"id": {{{external.system.id}}} }', + getIncidentUrl: 'https://coolsite.net/issue', + }, + secrets, + }, + logger, + configurationUtilities, + connectorUsageCollector + ); + + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); + const res = await postService.getIncident('1'); + expect(res).toEqual({ + id: '1', + title: 'CK-1', + }); + }); + + it('it should call request with correct arguments using POST', async () => { + const postService: ExternalService = createExternalService( + actionId, + { + config: { + ...config, + getIncidentMethod: WebhookMethods.POST, + getIncidentJson: '{"id": {{{external.system.id}}} }', + getIncidentUrl: 'https://coolsite.net/issue', + }, + secrets, + }, + logger, + configurationUtilities, + connectorUsageCollector + ); + + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); + + await postService.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://coolsite.net/issue', + logger, + configurationUtilities, + sslOverrides: defaultSSLOverrides, + connectorUsageCollector: expect.any(ConnectorUsageCollector), + method: WebhookMethods.POST, + data: '{"id": "1" }', + }); + }); + + it('it should call request with correct arguments when authType=SSL using POST', async () => { + const postSslService = createExternalService( + actionId, + { + config: { + ...sslConfig, + getIncidentMethod: WebhookMethods.POST, + getIncidentJson: '{"id": {{{external.system.id}}} }', + getIncidentUrl: 'https://coolsite.net/issue', + }, + secrets: sslSecrets, + }, + logger, + configurationUtilities, + connectorUsageCollector + ); + + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); + + await postSslService.getIncident('1'); + + // irrelevant snapshot content + delete requestMock.mock.calls[0][0].configurationUtilities; + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "axios": [Function], + "connectorUsageCollector": ConnectorUsageCollector { + "connectorId": "test-connector-id", + "logger": Object { + "context": Array [], + "debug": [MockFunction], + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "isLevelEnabled": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "usage": Object { + "requestBodyBytes": 0, + }, + }, + "data": "{\\"id\\": \\"1\\" }", + "logger": Object { + "context": Array [], + "debug": [MockFunction], + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "isLevelEnabled": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "method": "post", + "sslOverrides": Object { + "cert": Object { + "data": Array [ + 10, + 45, + 45, + 45, + 45, + 45, + 66, + 69, + 71, + 73, + 78, + 32, + 67, + 69, + 82, + 84, + 73, + 70, + 73, + 67, + 65, + 84, + 69, + 45, + 45, + 45, + 45, + 45, + 10, + 45, + 45, + 45, + 45, + 45, + 69, + 78, + 68, + 32, + 67, + 69, + 82, + 84, + 73, + 70, + 73, + 67, + 65, + 84, + 69, + 45, + 45, + 45, + 45, + 45, + 10, + ], + "type": "Buffer", + }, + "key": Object { + "data": Array [ + 10, + 45, + 45, + 45, + 45, + 45, + 66, + 69, + 71, + 73, + 78, + 32, + 80, + 82, + 73, + 86, + 65, + 84, + 69, + 32, + 75, + 69, + 89, + 45, + 45, + 45, + 45, + 45, + 10, + 45, + 45, + 45, + 45, + 45, + 69, + 78, + 68, + 32, + 80, + 82, + 73, + 86, + 65, + 84, + 69, + 32, + 75, + 69, + 89, + 45, + 45, + 45, + 45, + 45, + 10, + ], + "type": "Buffer", + }, + "passphrase": "foobar", + }, + "url": "https://coolsite.net/issue", + } + `); + }); + + it('it should throw if the request payload is not a valid JSON for POST', async () => { + const newService = createExternalService( + actionId, + { + config: { + ...config, + getIncidentMethod: WebhookMethods.POST, + getIncidentJson: '{"id": }', + getIncidentUrl: 'https://coolsite.net/issue', + }, + secrets, + }, + logger, + configurationUtilities, + connectorUsageCollector + ); + + await expect(newService.getIncident('1')).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: JSON Error: Get case JSON body must be valid JSON. ' + ); + }); }); describe('createIncident', () => { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts index 170c63a1d4e5b..9f14f494c9424 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts @@ -14,6 +14,7 @@ import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/action import { combineHeadersWithBasicAuthHeader } from '@kbn/actions-plugin/server/lib'; import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types'; import { buildConnectorAuth, validateConnectorAuthConfiguration } from '../../../common/auth/utils'; +import { WebhookMethods } from '../../../common/auth/constants'; import { validateAndNormalizeUrl, validateJson } from './validators'; import { createServiceError, @@ -52,6 +53,8 @@ export const createExternalService = ( createIncidentUrl: createIncidentUrlConfig, getIncidentResponseExternalTitleKey, getIncidentUrl, + getIncidentMethod, + getIncidentJson, hasAuth, authType, headers, @@ -113,10 +116,28 @@ export const createExternalService = ( configurationUtilities, 'Get case URL' ); + + const json = + getIncidentMethod === WebhookMethods.POST && getIncidentJson + ? renderMustacheStringNoEscape(getIncidentJson, { + external: { + system: { + id: JSON.stringify(id), + }, + }, + }) + : null; + + if (json !== null) { + validateJson(json, 'Get case JSON body'); + } + const res = await request({ axios: axiosInstance, url: normalizedUrl, + method: getIncidentMethod, logger, + ...(getIncidentMethod === WebhookMethods.POST ? { data: json } : {}), configurationUtilities, sslOverrides, connectorUsageCollector, @@ -128,6 +149,7 @@ export const createExternalService = ( }); const title = getObjectValueByKeyAsString(res.data, getIncidentResponseExternalTitleKey)!; + return { id, title }; } catch (error) { throw createServiceError(error, `Unable to get case with id ${id}`); @@ -157,6 +179,7 @@ export const createExternalService = ( ); validateJson(json, 'Create case JSON body'); + const res: AxiosResponse = await request({ axios: axiosInstance, url: normalizedUrl, @@ -175,6 +198,7 @@ export const createExternalService = ( requiredAttributesToBeInTheResponse: [createIncidentResponseKey], }); const externalId = getObjectValueByKeyAsString(data, createIncidentResponseKey)!; + const insertedIncident = await getIncident(externalId); logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts index 72f726d18b0e1..b425db8569f50 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts @@ -38,6 +38,8 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { headers: { ['content-type']: 'application/json', ['kbn-xsrf']: 'abcd' }, viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}', + getIncidentMethod: 'get', + getIncidentJson: null, updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', @@ -79,7 +81,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { }; let casesWebhookSimulatorURL: string = ''; - let simulatorConfig: Record>; + let simulatorConfig: Record>; describe('CasesWebhook', () => { before(() => { // use jira because cases webhook works with any third party case management system @@ -135,6 +137,53 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { config: simulatorConfig, }); }); + + it('should return 200 when creating a casesWebhook action with get case info using POST successfully', async () => { + const newConfig = { + ...simulatorConfig, + getIncidentMethod: 'post', + getIncidentJson: '{"id": {{{external.system.id}}} }', + getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue`, + }; + + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + config: newConfig, + secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + is_system_action: false, + is_deprecated: false, + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + is_missing_secrets: false, + config: newConfig, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + is_system_action: false, + is_deprecated: false, + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + is_missing_secrets: false, + config: newConfig, + }); + }); + describe('400s for all required fields when missing', () => { requiredFields.forEach((field) => { it(`should respond with a 400 Bad Request when creating a casesWebhook action with no ${field}`, async () => { @@ -529,6 +578,53 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { }); expect(proxyHaveBeenCalled).to.equal(false); }); + + it('should respond with bad JSON error when get case POST JSON is bad', async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook simulator', + connector_type_id: '.cases-webhook', + config: { + ...simulatorConfig, + getIncidentJson: '{"id": "{{{external.system.id}}}" }', + getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue`, + getIncidentMethod: 'post', + }, + secrets, + }); + + simulatedActionId = body.id; + + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + title: 'success', + description: 'success', + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: true, + message: 'an error occurred while running the action', + errorSource: TaskErrorSource.FRAMEWORK, + service_message: + '[Action][Webhook - Case Management]: Unable to create case. Error: [Action][Webhook - Case Management]: Unable to get case with id 123. Error: JSON Error: Get case JSON body must be valid JSON. . ', + }); + }); + }); + after(() => { if (proxyServer) { proxyServer.close(); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index d124047831e28..a6e98788e62a3 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -78,6 +78,8 @@ export default ({ getService }: FtrProviderContext): void => { headers: { [`content-type`]: 'application/json' }, viewIncidentUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + getIncidentMethod: 'get', + getIncidentJson: null, updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 5ddc3df660142..5029c57d8aec1 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -110,6 +110,8 @@ export default ({ getService }: FtrProviderContext): void => { headers: { [`content-type`]: 'application/json' }, viewIncidentUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + getIncidentMethod: 'get', + getIncidentJson: null, updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', From 210fd078b05981e9b885420c9786bbc88e36acdd Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 01:35:15 +1100 Subject: [PATCH 22/47] [8.x] [Obs AI Assistant] Update the word "chat" to "conversation" across the UI (#199310) # Backport This will backport the following commits from `main` to `8.x`: - [[Obs AI Assistant] Update the word "chat" to "conversation" across the UI](https://github.com/elastic/kibana/pull/199216) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Viduni Wickramarachchi --- .../packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx | 2 +- x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx | 4 ++-- x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx | 4 ++-- x-pack/packages/kbn-ai-assistant/src/chat/disclaimer.tsx | 2 +- .../public/components/buttons/start_chat_button.tsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx index 3e515e87c2197..60e37d85b92d9 100644 --- a/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx @@ -19,7 +19,7 @@ export function NewChatButton( {...nextProps} > {i18n.translate('xpack.aiAssistant.newChatButton', { - defaultMessage: 'New chat', + defaultMessage: 'New conversation', })} ) : ( diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx index 320beb1ca6b05..f7d8b3b20c433 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx @@ -234,14 +234,14 @@ export function ChatFlyout({ {i18n.translate('xpack.aiAssistant.disclaimer.disclaimerLabel', { defaultMessage: - "This chat is powered by an integration with your LLM provider. LLMs are known to sometimes present incorrect information as if it's correct. Elastic supports configuration and connection to the LLM provider and your knowledge base, but is not responsible for the LLM's responses.", + "This conversation is powered by an integration with your LLM provider. LLMs are known to sometimes present incorrect information as if it's correct. Elastic supports configuration and connection to the LLM provider and your knowledge base, but is not responsible for the LLM's responses.", })} ); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/buttons/start_chat_button.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/buttons/start_chat_button.tsx index 15042a251302e..f32b11aaa7abf 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/buttons/start_chat_button.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/buttons/start_chat_button.tsx @@ -18,7 +18,7 @@ export function StartChatButton(props: React.ComponentProps) { {...props} > {i18n.translate('xpack.observabilityAiAssistant.insight.response.startChat', { - defaultMessage: 'Start chat', + defaultMessage: 'Start conversation', })} ); From 79c4b223a48ea9e5055bfb018d7e29538c67408b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 01:37:06 +1100 Subject: [PATCH 23/47] [8.x] Fixes flaky backfill tests. (#198592) (#199309) # Backport This will backport the following commits from `main` to `8.x`: - [Fixes flaky backfill tests. (#198592)](https://github.com/elastic/kibana/pull/198592) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Ying Mao --- .../group1/tests/alerting/backfill/api_key.ts | 6 +++--- .../group1/tests/alerting/backfill/schedule.ts | 2 +- .../group1/tests/alerting/backfill/task_runner.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/api_key.ts index cf818de0e4fab..a36d7708bb99c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/api_key.ts @@ -263,11 +263,11 @@ export default function apiKeyBackfillTests({ getService }: FtrProviderContext) } }); - // invoke the invalidate task - await runInvalidateTask(); - // pending API key should now be deleted because backfill is done await retry.try(async () => { + // invoke the invalidate task + await runInvalidateTask(); + const results = await getApiKeysPendingInvalidation(); expect(results.length).to.eql(0); return results; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/schedule.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/schedule.ts index 8f25a3e181c66..9d7b79d6cbce0 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/schedule.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/schedule.ts @@ -759,7 +759,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext }); it('should handle schedule request where some requests succeed and some requests fail appropriately', async () => { - const start = moment().utc().startOf('day').subtract(7, 'days').toISOString(); + const start = moment().utc().startOf('day').subtract(14, 'days').toISOString(); const end = moment().utc().startOf('day').subtract(5, 'days').toISOString(); // create 2 rules const rresponse1 = await supertest diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts index 86b6690972d1c..2083a79c0d0f5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts @@ -166,7 +166,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); const start = moment(originalDocTimestamps[1]).utc().startOf('day').toISOString(); - const end = moment().utc().startOf('day').subtract(9, 'days').toISOString(); + const end = moment(originalDocTimestamps[11]).utc().startOf('day').toISOString(); // Schedule backfill for this rule const response2 = await supertestWithoutAuth From 027f843f510df9cc3b8421a2e2723bf7e2943baa Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 02:23:00 +1100 Subject: [PATCH 24/47] [8.x] [ES|QL] Support params in column nodes and function names (#198957) (#199316) # Backport This will backport the following commits from `main` to `8.x`: - [[ES|QL] Support params in column nodes and function names (#198957)](https://github.com/elastic/kibana/pull/198957) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> --- packages/kbn-esql-ast/src/builder/builder.ts | 14 +- .../src/mutate/commands/from/metadata.test.ts | 37 ++- .../src/mutate/commands/from/metadata.ts | 22 +- .../src/mutate/commands/sort/index.test.ts | 4 +- .../src/mutate/commands/sort/index.ts | 30 +- .../src/parser/__tests__/columns.test.ts | 305 +++++++++++++++++- packages/kbn-esql-ast/src/parser/factories.ts | 108 ++++--- .../src/pretty_print/leaf_printer.ts | 58 ++-- packages/kbn-esql-ast/src/types.ts | 9 + .../kbn-esql-ast/src/walker/walker.test.ts | 106 ++++++ packages/kbn-esql-ast/src/walker/walker.ts | 24 +- .../src/autocomplete/helper.ts | 17 +- .../src/code_actions/actions.ts | 5 +- .../src/definitions/commands.ts | 3 +- .../src/shared/context.ts | 5 +- .../src/shared/helpers.ts | 67 ++-- .../validation/__tests__/functions.test.ts | 6 +- .../__tests__/validation.params.test.ts | 165 ++++++++++ .../src/validation/errors.ts | 12 +- .../esql_validation_meta_tests.json | 6 +- .../src/validation/validation.test.ts | 2 - .../src/validation/validation.ts | 90 +++--- 22 files changed, 910 insertions(+), 185 deletions(-) diff --git a/packages/kbn-esql-ast/src/builder/builder.ts b/packages/kbn-esql-ast/src/builder/builder.ts index 894ab99e5b3e8..fae1981b454c2 100644 --- a/packages/kbn-esql-ast/src/builder/builder.ts +++ b/packages/kbn-esql-ast/src/builder/builder.ts @@ -9,6 +9,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { LeafPrinter } from '../pretty_print'; import { ESQLAstComment, ESQLAstCommentMultiLine, @@ -125,16 +126,23 @@ export namespace Builder { }; export const column = ( - template: Omit, 'name' | 'quoted'>, + template: Omit, 'name' | 'quoted' | 'parts'>, fromParser?: Partial ): ESQLColumn => { - return { + const node: ESQLColumn = { ...template, ...Builder.parserFields(fromParser), + parts: template.args.map((arg) => + arg.type === 'identifier' ? arg.name : LeafPrinter.param(arg) + ), quoted: false, - name: template.parts.join('.'), + name: '', type: 'column', }; + + node.name = LeafPrinter.column(node); + + return node; }; export const order = ( diff --git a/packages/kbn-esql-ast/src/mutate/commands/from/metadata.test.ts b/packages/kbn-esql-ast/src/mutate/commands/from/metadata.test.ts index b6cb485395a6c..e4161994d224b 100644 --- a/packages/kbn-esql-ast/src/mutate/commands/from/metadata.test.ts +++ b/packages/kbn-esql-ast/src/mutate/commands/from/metadata.test.ts @@ -28,7 +28,13 @@ describe('commands.from.metadata', () => { expect(column).toMatchObject({ type: 'column', - parts: ['a'], + args: [ + { + type: 'identifier', + name: 'a', + }, + ], + // parts: ['a'], }); }); @@ -40,19 +46,39 @@ describe('commands.from.metadata', () => { expect(columns).toMatchObject([ { type: 'column', - parts: ['a'], + args: [ + { + type: 'identifier', + name: 'a', + }, + ], }, { type: 'column', - parts: ['b'], + args: [ + { + type: 'identifier', + name: 'b', + }, + ], }, { type: 'column', - parts: ['_id'], + args: [ + { + type: 'identifier', + name: '_id', + }, + ], }, { type: 'column', - parts: ['_lang'], + args: [ + { + type: 'identifier', + name: '_lang', + }, + ], }, ]); }); @@ -156,7 +182,6 @@ describe('commands.from.metadata', () => { it('return inserted `column` node, and parent `option` node', () => { const src1 = 'FROM index METADATA a'; const { root } = parse(src1); - const tuple = commands.from.metadata.insert(root, 'b'); expect(tuple).toMatchObject([ diff --git a/packages/kbn-esql-ast/src/mutate/commands/from/metadata.ts b/packages/kbn-esql-ast/src/mutate/commands/from/metadata.ts index 5160ab65954cb..4d637a1fd0570 100644 --- a/packages/kbn-esql-ast/src/mutate/commands/from/metadata.ts +++ b/packages/kbn-esql-ast/src/mutate/commands/from/metadata.ts @@ -74,7 +74,10 @@ export const find = ( } const predicate: Predicate<[ESQLColumn, unknown]> = ([field]) => - cmpArr(field.parts, fieldName as string[]); + cmpArr( + field.args.map((arg) => (arg.type === 'identifier' ? arg.name : '')), + fieldName as string[] + ); return findByPredicate(list(ast), predicate); }; @@ -128,7 +131,12 @@ export const remove = ( fieldName = [fieldName]; } - return removeByPredicate(ast, (field) => cmpArr(field.parts, fieldName as string[])); + return removeByPredicate(ast, (field) => + cmpArr( + field.args.map((arg) => (arg.type === 'identifier' ? arg.name : '')), + fieldName as string[] + ) + ); }; /** @@ -161,7 +169,8 @@ export const insert = ( } const parts: string[] = typeof fieldName === 'string' ? [fieldName] : fieldName; - const column = Builder.expression.column({ parts }); + const args = parts.map((part) => Builder.identifier({ name: part })); + const column = Builder.expression.column({ args }); if (index === -1) { option.args.push(column); @@ -195,7 +204,12 @@ export const upsert = ( const parts = Array.isArray(fieldName) ? fieldName : [fieldName]; const existing = Walker.find( option, - (node) => node.type === 'column' && cmpArr(node.parts, parts) + (node) => + node.type === 'column' && + cmpArr( + node.args.map((arg) => (arg.type === 'identifier' ? arg.name : '')), + parts + ) ); if (existing) { return undefined; diff --git a/packages/kbn-esql-ast/src/mutate/commands/sort/index.test.ts b/packages/kbn-esql-ast/src/mutate/commands/sort/index.test.ts index d04f79b96541a..1342a059254fd 100644 --- a/packages/kbn-esql-ast/src/mutate/commands/sort/index.test.ts +++ b/packages/kbn-esql-ast/src/mutate/commands/sort/index.test.ts @@ -244,13 +244,13 @@ describe('commands.sort', () => { args: [ { type: 'column', - parts: ['b', 'a'], + args: [{ name: 'b' }, { name: 'a' }], }, ], }); expect(node2).toMatchObject({ type: 'column', - parts: ['a', 'b'], + args: [{ name: 'a' }, { name: 'b' }], }); }); diff --git a/packages/kbn-esql-ast/src/mutate/commands/sort/index.ts b/packages/kbn-esql-ast/src/mutate/commands/sort/index.ts index d2b2c7cd5f3d4..d2f64e5b6a1cb 100644 --- a/packages/kbn-esql-ast/src/mutate/commands/sort/index.ts +++ b/packages/kbn-esql-ast/src/mutate/commands/sort/index.ts @@ -49,15 +49,17 @@ export type NewSortExpressionTemplate = const createSortExpression = ( template: string | string[] | NewSortExpressionTemplate ): SortExpression => { + const parts: string[] = + typeof template === 'string' + ? [template] + : Array.isArray(template) + ? template + : typeof template.parts === 'string' + ? [template.parts] + : template.parts; + const identifiers = parts.map((part) => Builder.identifier({ name: part })); const column = Builder.expression.column({ - parts: - typeof template === 'string' - ? [template] - : Array.isArray(template) - ? template - : typeof template.parts === 'string' - ? [template.parts] - : template.parts, + args: identifiers, }); if (typeof template === 'string' || Array.isArray(template)) { @@ -189,12 +191,18 @@ export const find = ( return findByPredicate(ast, ([node]) => { let isMatch = false; if (node.type === 'column') { - isMatch = util.cmpArr(node.parts, arrParts); + isMatch = util.cmpArr( + node.args.map((arg) => (arg.type === 'identifier' ? arg.name : '')), + arrParts + ); } else if (node.type === 'order') { - const columnParts = (node.args[0] as ESQLColumn)?.parts; + const columnParts = (node.args[0] as ESQLColumn)?.args; if (Array.isArray(columnParts)) { - isMatch = util.cmpArr(columnParts, arrParts); + isMatch = util.cmpArr( + columnParts.map((arg) => (arg.type === 'identifier' ? arg.name : '')), + arrParts + ); } } diff --git a/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts index 38e98104d41bd..f235005a2c10f 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts @@ -10,21 +10,86 @@ import { parse } from '..'; describe('Column Identifier Expressions', () => { + it('can parse star column as function argument', () => { + const text = 'ROW fn(*)'; + const { root } = parse(text); + + expect(root.commands).toMatchObject([ + { + type: 'command', + name: 'row', + args: [ + { + type: 'function', + name: 'fn', + args: [ + { + type: 'column', + args: [ + { + type: 'identifier', + name: '*', + }, + ], + }, + ], + }, + ], + }, + ]); + }); + + it('can parse a single identifier', () => { + const text = 'ROW hello'; + const { root } = parse(text); + + expect(root.commands).toMatchObject([ + { + type: 'command', + args: [ + { + type: 'column', + args: [ + { + type: 'identifier', + name: 'hello', + }, + ], + }, + ], + }, + ]); + }); + it('can parse un-quoted identifiers', () => { const text = 'ROW a, b.c'; - const { ast } = parse(text); + const { root } = parse(text); - expect(ast).toMatchObject([ + expect(root.commands).toMatchObject([ { type: 'command', args: [ { type: 'column', - parts: ['a'], + args: [ + { + type: 'identifier', + name: 'a', + }, + ], }, { type: 'column', - parts: ['b', 'c'], + args: [ + { + type: 'identifier', + name: 'b', + }, + { + type: 'identifier', + name: 'c', + }, + ], }, ], }, @@ -33,23 +98,50 @@ describe('Column Identifier Expressions', () => { it('can parse quoted identifiers', () => { const text = 'ROW `a`, `b`.c, `d`.`👍`.`123``123`'; - const { ast } = parse(text); + const { root } = parse(text); - expect(ast).toMatchObject([ + expect(root.commands).toMatchObject([ { type: 'command', args: [ { type: 'column', - parts: ['a'], + args: [ + { + type: 'identifier', + name: 'a', + }, + ], }, { type: 'column', - parts: ['b', 'c'], + args: [ + { + type: 'identifier', + name: 'b', + }, + { + type: 'identifier', + name: 'c', + }, + ], }, { type: 'column', - parts: ['d', '👍', '123`123'], + args: [ + { + type: 'identifier', + name: 'd', + }, + { + type: 'identifier', + name: '👍', + }, + { + type: 'identifier', + name: '123`123', + }, + ], }, ], }, @@ -58,15 +150,28 @@ describe('Column Identifier Expressions', () => { it('can mix quoted and un-quoted identifiers', () => { const text = 'ROW part1.part2.`part``3️⃣`'; - const { ast } = parse(text); + const { root } = parse(text); - expect(ast).toMatchObject([ + expect(root.commands).toMatchObject([ { type: 'command', args: [ { type: 'column', - parts: ['part1', 'part2', 'part`3️⃣'], + args: [ + { + type: 'identifier', + name: 'part1', + }, + { + type: 'identifier', + name: 'part2', + }, + { + type: 'identifier', + name: 'part`3️⃣', + }, + ], }, ], }, @@ -75,19 +180,189 @@ describe('Column Identifier Expressions', () => { it('in KEEP command', () => { const text = 'FROM a | KEEP a.b'; - const { ast } = parse(text); + const { root } = parse(text); - expect(ast).toMatchObject([ + expect(root.commands).toMatchObject([ {}, { type: 'command', args: [ { type: 'column', - parts: ['a', 'b'], + args: [ + { + type: 'identifier', + name: 'a', + }, + { + type: 'identifier', + name: 'b', + }, + ], }, ], }, ]); }); + + describe('params', () => { + it('can parse named param as a single param node', () => { + const text = 'ROW ?test'; + const { root } = parse(text); + + expect(root.commands).toMatchObject([ + { + type: 'command', + args: [ + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'test', + }, + ], + }, + ]); + }); + + it('can parse nested named params as column', () => { + const text = 'ROW ?test1.?test2'; + const { root } = parse(text); + + expect(root.commands).toMatchObject([ + { + type: 'command', + args: [ + { + type: 'column', + args: [ + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'test1', + }, + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'test2', + }, + ], + }, + ], + }, + ]); + }); + + it('can mix param and identifier in column name', () => { + const text = 'ROW ?par.id'; + const { root } = parse(text); + + expect(root.commands).toMatchObject([ + { + type: 'command', + args: [ + { + type: 'column', + args: [ + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'par', + }, + { + type: 'identifier', + name: 'id', + }, + ], + }, + ], + }, + ]); + }); + + it('can mix param and identifier in column name - 2', () => { + const text = 'ROW `😱`.?par'; + const { root } = parse(text); + + expect(root.commands).toMatchObject([ + { + type: 'command', + args: [ + { + type: 'column', + args: [ + { + type: 'identifier', + name: '😱', + }, + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'par', + }, + ], + }, + ], + }, + ]); + }); + + it('supports all three different param types', () => { + const text = 'ROW ?.?name.?123'; + const { root } = parse(text); + + expect(root.commands).toMatchObject([ + { + type: 'command', + args: [ + { + type: 'column', + args: [ + { + type: 'literal', + literalType: 'param', + paramType: 'unnamed', + }, + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'name', + }, + { + type: 'literal', + literalType: 'param', + paramType: 'positional', + value: 123, + }, + ], + }, + ], + }, + ]); + }); + + it('parses DROP command args as "column" nodes', () => { + const text = 'FROM index | DROP any#Char$Field'; + const { root } = parse(text); + + expect(root.commands).toMatchObject([ + { type: 'command' }, + { + type: 'command', + name: 'drop', + args: [ + { + type: 'column', + name: 'any', + }, + ], + }, + ]); + }); + }); }); diff --git a/packages/kbn-esql-ast/src/parser/factories.ts b/packages/kbn-esql-ast/src/parser/factories.ts index b575447f7e744..311dcced8a617 100644 --- a/packages/kbn-esql-ast/src/parser/factories.ts +++ b/packages/kbn-esql-ast/src/parser/factories.ts @@ -31,6 +31,7 @@ import { IdentifierContext, InputParamContext, InputNamedOrPositionalParamContext, + IdentifierOrParameterContext, } from '../antlr/esql_parser'; import { DOUBLE_TICKS_REGEX, SINGLE_BACKTICK, TICKS_REGEX } from './constants'; import type { @@ -227,26 +228,35 @@ export const createFunctionCall = (ctx: FunctionContext): ESQLFunctionCallExpres }; const identifierOrParameter = functionName.identifierOrParameter(); - if (identifierOrParameter) { - const identifier = identifierOrParameter.identifier(); - if (identifier) { - node.operator = createIdentifier(identifier); - } else { - const parameter = identifierOrParameter.parameter(); - if (parameter) { - node.operator = createParam(parameter); - } + + if (identifierOrParameter instanceof IdentifierOrParameterContext) { + const operator = createIdentifierOrParam(identifierOrParameter); + + if (operator) { + node.operator = operator; } } return node; }; -const createIdentifier = (identifier: IdentifierContext): ESQLIdentifier => { - return Builder.identifier( - { name: identifier.getText().toLowerCase() }, - createParserFields(identifier) - ); +export const createIdentifierOrParam = (ctx: IdentifierOrParameterContext) => { + const identifier = ctx.identifier(); + if (identifier) { + return createIdentifier(identifier); + } else { + const parameter = ctx.parameter(); + if (parameter) { + return createParam(parameter); + } + } +}; + +export const createIdentifier = (identifier: IdentifierContext): ESQLIdentifier => { + const text = identifier.getText(); + const name = parseIdentifier(text); + + return Builder.identifier({ name }, createParserFields(identifier)); }; export const createParam = (ctx: ParseTree) => { @@ -473,42 +483,68 @@ export function createSource( export function createColumnStar(ctx: TerminalNode): ESQLColumn { const text = ctx.getText(); - - return { - type: 'column', - name: text, - parts: [text], + const parserFields = { text, location: getPosition(ctx.symbol), incomplete: ctx.getText() === '', quoted: false, }; + const node = Builder.expression.column( + { args: [Builder.identifier({ name: '*' }, parserFields)] }, + parserFields + ); + + node.name = text; + + return node; } export function createColumn(ctx: ParserRuleContext): ESQLColumn { - const parts: string[] = []; + const args: ESQLColumn['args'] = []; + if (ctx instanceof QualifiedNamePatternContext) { - parts.push( - ...ctx.identifierPattern_list().map((identifier) => parseIdentifier(identifier.getText())) - ); + const list = ctx.identifierPattern_list(); + + for (const identifier of list) { + const name = parseIdentifier(identifier.getText()); + const node = Builder.identifier({ name }, createParserFields(identifier)); + + args.push(node); + } } else if (ctx instanceof QualifiedNameContext) { - parts.push( - ...ctx.identifierOrParameter_list().map((identifier) => parseIdentifier(identifier.getText())) - ); + const list = ctx.identifierOrParameter_list(); + + for (const item of list) { + if (item instanceof IdentifierOrParameterContext) { + const node = createIdentifierOrParam(item); + + if (node) { + args.push(node); + } + } + } } else { - parts.push(sanitizeIdentifierString(ctx)); + const name = sanitizeIdentifierString(ctx); + const node = Builder.identifier({ name }, createParserFields(ctx)); + + args.push(node); } + const text = sanitizeIdentifierString(ctx); const hasQuotes = Boolean(getQuotedText(ctx) || isQuoted(ctx.getText())); - return { - type: 'column' as const, - name: text, - parts, - text: ctx.getText(), - location: getPosition(ctx.start, ctx.stop), - incomplete: Boolean(ctx.exception || text === ''), - quoted: hasQuotes, - }; + const column = Builder.expression.column( + { args }, + { + text: ctx.getText(), + location: getPosition(ctx.start, ctx.stop), + incomplete: Boolean(ctx.exception || text === ''), + } + ); + + column.name = text; + column.quoted = hasQuotes; + + return column; } export function createOption(name: string, ctx: ParserRuleContext): ESQLCommandOption { diff --git a/packages/kbn-esql-ast/src/pretty_print/leaf_printer.ts b/packages/kbn-esql-ast/src/pretty_print/leaf_printer.ts index 3c12de90e4454..b413234cbe263 100644 --- a/packages/kbn-esql-ast/src/pretty_print/leaf_printer.ts +++ b/packages/kbn-esql-ast/src/pretty_print/leaf_printer.ts @@ -12,6 +12,7 @@ import { ESQLAstCommentMultiLine, ESQLColumn, ESQLLiteral, + ESQLParamLiteral, ESQLSource, ESQLTimeInterval, } from '../types'; @@ -27,20 +28,37 @@ export const LeafPrinter = { source: (node: ESQLSource) => node.name, column: (node: ESQLColumn) => { - const parts: string[] = node.parts; + const args = node.args; let formatted = ''; - for (const part of parts) { - if (formatted.length > 0) { - formatted += '.'; - } - if (regexUnquotedIdPattern.test(part)) { - formatted += part; - } else { - // Escape backticks "`" with double backticks "``". - const escaped = part.replace(/`/g, '``'); - formatted += '`' + escaped + '`'; + for (const arg of args) { + switch (arg.type) { + case 'identifier': { + const name = arg.name; + + if (formatted.length > 0) { + formatted += '.'; + } + if (regexUnquotedIdPattern.test(name)) { + formatted += name; + } else { + // Escape backticks "`" with double backticks "``". + const escaped = name.replace(/`/g, '``'); + formatted += '`' + escaped + '`'; + } + + break; + } + case 'literal': { + if (formatted.length > 0) { + formatted += '.'; + } + + formatted += LeafPrinter.literal(arg); + + break; + } } } @@ -56,13 +74,7 @@ export const LeafPrinter = { return String(node.value).toUpperCase() === 'TRUE' ? 'TRUE' : 'FALSE'; } case 'param': { - switch (node.paramType) { - case 'named': - case 'positional': - return '?' + node.value; - default: - return '?'; - } + return LeafPrinter.param(node); } case 'keyword': { return String(node.value); @@ -82,6 +94,16 @@ export const LeafPrinter = { } }, + param: (node: ESQLParamLiteral) => { + switch (node.paramType) { + case 'named': + case 'positional': + return '?' + node.value; + default: + return '?'; + } + }, + timeInterval: (node: ESQLTimeInterval) => { const { quantity, unit } = node; diff --git a/packages/kbn-esql-ast/src/types.ts b/packages/kbn-esql-ast/src/types.ts index ea76fc3e0b9a4..2a8513fc2ced1 100644 --- a/packages/kbn-esql-ast/src/types.ts +++ b/packages/kbn-esql-ast/src/types.ts @@ -276,6 +276,15 @@ export interface ESQLSource extends ESQLAstBaseItem { export interface ESQLColumn extends ESQLAstBaseItem { type: 'column'; + /** + * A ES|QL column name can be composed of multiple parts, + * e.g: part1.part2.`part``3️⃣`.?param. Where parts can be quoted, or not + * quoted, or even be a parameter. + * + * The args list contains the parts of the column name. + */ + args: Array; + /** * An identifier can be composed of multiple parts, e.g: part1.part2.`part``3️⃣`. * This property contains the parsed unquoted parts of the identifier. diff --git a/packages/kbn-esql-ast/src/walker/walker.test.ts b/packages/kbn-esql-ast/src/walker/walker.test.ts index 980e1499e62aa..49c50a0f7fa5d 100644 --- a/packages/kbn-esql-ast/src/walker/walker.test.ts +++ b/packages/kbn-esql-ast/src/walker/walker.test.ts @@ -812,6 +812,112 @@ describe('Walker.params()', () => { }, ]); }); + + test('can collect params from column names', () => { + const query = 'ROW ?a.?b'; + const { ast } = parse(query); + const params = Walker.params(ast); + + expect(params).toMatchObject([ + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'a', + }, + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'b', + }, + ]); + }); + + test('can collect params from column names, where first part is not a param', () => { + const query = 'ROW a.?b'; + const { ast } = parse(query); + const params = Walker.params(ast); + + expect(params).toMatchObject([ + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'b', + }, + ]); + }); + + test('can collect all types of param from column name', () => { + const query = 'ROW ?.?0.?a'; + const { ast } = parse(query); + const params = Walker.params(ast); + + expect(params).toMatchObject([ + { + type: 'literal', + literalType: 'param', + paramType: 'unnamed', + }, + { + type: 'literal', + literalType: 'param', + paramType: 'positional', + value: 0, + }, + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'a', + }, + ]); + }); + + test('can collect params from function names', () => { + const query = 'FROM a | STATS ?lala()'; + const { ast } = parse(query); + const params = Walker.params(ast); + + expect(params).toMatchObject([ + { + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'lala', + }, + ]); + }); + + test('can collect params from function names (unnamed)', () => { + const query = 'FROM a | STATS ?()'; + const { ast } = parse(query); + const params = Walker.params(ast); + + expect(params).toMatchObject([ + { + type: 'literal', + literalType: 'param', + paramType: 'unnamed', + }, + ]); + }); + + test('can collect params from function names (positional)', () => { + const query = 'FROM a | STATS agg(test), ?123()'; + const { ast } = parse(query); + const params = Walker.params(ast); + + expect(params).toMatchObject([ + { + type: 'literal', + literalType: 'param', + paramType: 'positional', + value: 123, + }, + ]); + }); }); describe('Walker.find()', () => { diff --git a/packages/kbn-esql-ast/src/walker/walker.ts b/packages/kbn-esql-ast/src/walker/walker.ts index dbbbc3b090f29..f3b6de91649b7 100644 --- a/packages/kbn-esql-ast/src/walker/walker.ts +++ b/packages/kbn-esql-ast/src/walker/walker.ts @@ -20,6 +20,7 @@ import type { ESQLCommandMode, ESQLCommandOption, ESQLFunction, + ESQLIdentifier, ESQLInlineCast, ESQLList, ESQLLiteral, @@ -49,6 +50,7 @@ export interface WalkerOptions { visitTimeIntervalLiteral?: (node: ESQLTimeInterval) => void; visitInlineCast?: (node: ESQLInlineCast) => void; visitUnknown?: (node: ESQLUnknownItem) => void; + visitIdentifier?: (node: ESQLIdentifier) => void; /** * Called for any node type that does not have a specific visitor. @@ -346,11 +348,27 @@ export class Walker { } } + public walkColumn(node: ESQLColumn): void { + const { options } = this; + const { args } = node; + + (options.visitColumn ?? options.visitAny)?.(node); + + if (args) { + for (const value of args) { + this.walkAstItem(value); + } + } + } + public walkFunction(node: ESQLFunction): void { const { options } = this; (options.visitFunction ?? options.visitAny)?.(node); const args = node.args; const length = args.length; + + if (node.operator) this.walkAstItem(node.operator); + for (let i = 0; i < length; i++) { const arg = args[i]; this.walkAstItem(arg); @@ -393,7 +411,7 @@ export class Walker { break; } case 'column': { - (options.visitColumn ?? options.visitAny)?.(node); + this.walkColumn(node); break; } case 'literal': { @@ -412,6 +430,10 @@ export class Walker { (options.visitInlineCast ?? options.visitAny)?.(node); break; } + case 'identifier': { + (options.visitIdentifier ?? options.visitAny)?.(node); + break; + } case 'unknown': { (options.visitUnknown ?? options.visitAny)?.(node); break; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts index 9724878611b01..3ccddfc5ff241 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts @@ -27,6 +27,7 @@ import { isAssignment, isColumnItem, isFunctionItem, + isIdentifier, isLiteralItem, isTimeIntervalItem, } from '../shared/helpers'; @@ -457,15 +458,13 @@ export function extractTypeFromASTArg( if (Array.isArray(arg)) { return extractTypeFromASTArg(arg[0], references); } - if (isColumnItem(arg) || isLiteralItem(arg)) { - if (isLiteralItem(arg)) { - return arg.literalType; - } - if (isColumnItem(arg)) { - const hit = getColumnForASTNode(arg, references); - if (hit) { - return hit.type; - } + if (isLiteralItem(arg)) { + return arg.literalType; + } + if (isColumnItem(arg) || isIdentifier(arg)) { + const hit = getColumnForASTNode(arg, references); + if (hit) { + return hit.type; } } if (isTimeIntervalItem(arg)) { diff --git a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts index 37ab56350ffb2..7c9d5d7ae8ba2 100644 --- a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts @@ -20,6 +20,7 @@ import { getAllFunctions, getCommandDefinition, isColumnItem, + isIdentifier, isSourceItem, shouldBeQuotedText, } from '../shared/helpers'; @@ -138,7 +139,7 @@ function extractUnquotedFieldText( if (errorType === 'syntaxError') { // scope it down to column items for now const { node } = getAstContext(query, ast, possibleStart - 1); - if (node && isColumnItem(node)) { + if (node && (isColumnItem(node) || isIdentifier(node))) { return { start: node.location.min + 1, name: query.substring(node.location.min, end).trimEnd(), @@ -379,7 +380,7 @@ function inferCodeFromError( if (error.message.startsWith('SyntaxError: token recognition error at:')) { // scope it down to column items for now const { node } = getAstContext(rawText, ast, error.startColumn - 2); - return node && isColumnItem(node) ? 'quotableFields' : undefined; + return node && (isColumnItem(node) || isIdentifier(node)) ? 'quotableFields' : undefined; } } diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index d07511665ad31..fd6dbfbd453e5 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -20,6 +20,7 @@ import { isAssignment, isColumnItem, isFunctionItem, + isFunctionOperatorParam, isLiteralItem, } from '../shared/helpers'; import { ENRICH_MODES } from './settings'; @@ -72,7 +73,7 @@ const statsValidator = (command: ESQLCommand) => { function checkAggExistence(arg: ESQLFunction): boolean { // TODO the grouping function check may not // hold true for all future cases - if (isAggFunction(arg)) { + if (isAggFunction(arg) || isFunctionOperatorParam(arg)) { return true; } if (isOtherFunction(arg)) { diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts index 42e63d7623e49..cc7c36abf64f7 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts @@ -26,6 +26,7 @@ import { isSettingItem, pipePrecedesCurrentWord, getFunctionDefinition, + isIdentifier, } from './helpers'; function findNode(nodes: ESQLAstItem[], offset: number): ESQLSingleAstItem | undefined { @@ -87,7 +88,9 @@ function findCommandSubType( function isMarkerNode(node: ESQLSingleAstItem | undefined): boolean { return Boolean( - node && (isColumnItem(node) || isSourceItem(node)) && node.name.endsWith(EDITOR_MARKER) + node && + (isColumnItem(node) || isIdentifier(node) || isSourceItem(node)) && + node.name.endsWith(EDITOR_MARKER) ); } diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 43bbb2b571a50..e86cb4f6ae8f2 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -7,18 +7,24 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { - ESQLAstItem, - ESQLColumn, - ESQLCommandMode, - ESQLCommandOption, - ESQLFunction, - ESQLLiteral, - ESQLSingleAstItem, - ESQLSource, - ESQLTimeInterval, +import { + Walker, + type ESQLAstItem, + type ESQLColumn, + type ESQLCommandMode, + type ESQLCommandOption, + type ESQLFunction, + type ESQLLiteral, + type ESQLSingleAstItem, + type ESQLSource, + type ESQLTimeInterval, } from '@kbn/esql-ast'; -import { ESQLInlineCast, ESQLParamLiteral } from '@kbn/esql-ast/src/types'; +import { + ESQLIdentifier, + ESQLInlineCast, + ESQLParamLiteral, + ESQLProperNode, +} from '@kbn/esql-ast/src/types'; import { aggregationFunctionDefinitions } from '../definitions/generated/aggregation_functions'; import { builtinFunctions } from '../definitions/builtin'; import { commandDefinitions } from '../definitions/commands'; @@ -78,6 +84,10 @@ export function isColumnItem(arg: ESQLAstItem): arg is ESQLColumn { return isSingleItem(arg) && arg.type === 'column'; } +export function isIdentifier(arg: ESQLAstItem): arg is ESQLIdentifier { + return isSingleItem(arg) && arg.type === 'identifier'; +} + export function isLiteralItem(arg: ESQLAstItem): arg is ESQLLiteral { return isSingleItem(arg) && arg.type === 'literal'; } @@ -254,10 +264,11 @@ function doesLiteralMatchParameterType(argType: FunctionParameterType, item: ESQ * This function returns the variable or field matching a column */ export function getColumnForASTNode( - column: ESQLColumn, + node: ESQLColumn | ESQLIdentifier, { fields, variables }: Pick ): ESQLRealField | ESQLVariable | undefined { - return getColumnByName(column.parts.join('.'), { fields, variables }); + const formatted = node.type === 'identifier' ? node.name : node.parts.join('.'); + return getColumnByName(formatted, { fields, variables }); } /** @@ -438,7 +449,10 @@ export function checkFunctionArgMatchesDefinition( parentCommand?: string ) { const argType = parameterDefinition.type; - if (argType === 'any' || isParam(arg)) { + if (argType === 'any') { + return true; + } + if (isParam(arg)) { return true; } if (arg.type === 'literal') { @@ -465,7 +479,8 @@ export function checkFunctionArgMatchesDefinition( const wrappedTypes: Array<(typeof validHit)['type']> = Array.isArray(validHit.type) ? validHit.type : [validHit.type]; - return wrappedTypes.some((ct) => ct === argType || ct === 'null'); + + return wrappedTypes.some((ct) => ct === argType || ct === 'null' || ct === 'unknown'); } if (arg.type === 'inlineCast') { const lowerArgType = argType?.toLowerCase(); @@ -543,20 +558,20 @@ export function isVariable( * * E.g. "`bytes`" will be "`bytes`" * - * @param column + * @param node * @returns */ -export const getQuotedColumnName = (column: ESQLColumn) => - column.quoted ? column.text : column.name; +export const getQuotedColumnName = (node: ESQLColumn | ESQLIdentifier) => + node.type === 'identifier' ? node.name : node.quoted ? node.text : node.name; /** * TODO - consider calling lookupColumn under the hood of this function. Seems like they should really do the same thing. */ export function getColumnExists( - column: ESQLColumn, + node: ESQLColumn | ESQLIdentifier, { fields, variables }: Pick ) { - const columnName = column.parts.join('.'); + const columnName = node.type === 'identifier' ? node.name : node.parts.join('.'); if (fields.has(columnName) || variables.has(columnName)) { return true; } @@ -645,6 +660,18 @@ export const isParam = (x: unknown): x is ESQLParamLiteral => (x as ESQLParamLiteral).type === 'literal' && (x as ESQLParamLiteral).literalType === 'param'; +export const isFunctionOperatorParam = (fn: ESQLFunction): boolean => + !!fn.operator && isParam(fn.operator); + +/** + * Returns `true` if the function is an aggregation function or a function + * name is a parameter, which potentially could be an aggregation function. + */ +export const isMaybeAggFunction = (fn: ESQLFunction): boolean => + isAggFunction(fn) || isFunctionOperatorParam(fn); + +export const isParametrized = (node: ESQLProperNode): boolean => Walker.params(node).length > 0; + /** * Compares two strings in a case-insensitive manner */ diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts index ec0fbe5395334..d0d2a1bfec0a4 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts @@ -552,11 +552,7 @@ describe('function validation', () => { | EVAL foo = TEST1(1.) | EVAL TEST2(foo) | EVAL TEST3(foo)`, - [ - 'Argument of [test1] must be [keyword], found value [1.] type [double]', - 'Argument of [test2] must be [keyword], found value [foo] type [unknown]', - 'Argument of [test3] must be [long], found value [foo] type [unknown]', - ] + ['Argument of [test1] must be [keyword], found value [1.] type [double]'] ); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts index bbb2867981425..e2de846651b07 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts @@ -42,3 +42,168 @@ test('allow params in WHERE command expressions', async () => { expect(res2).toMatchObject({ errors: [], warnings: [] }); expect(res3).toMatchObject({ errors: [], warnings: [] }); }); + +describe('allows named params', () => { + test('WHERE boolean expression can contain a param', async () => { + const { validate } = await setup(); + + const res0 = await validate('FROM index | STATS var = ?func(?field) | WHERE var >= ?value'); + expect(res0).toMatchObject({ errors: [], warnings: [] }); + + const res1 = await validate('FROM index | STATS var = ?param | WHERE var >= ?value'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('FROM index | STATS var = ?param | WHERE var >= ?value'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + + const res3 = await validate('FROM index | STATS var = ?param | WHERE ?value >= var'); + expect(res3).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in column names', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW ?test'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('ROW ?test, ?one_more, ?asldfkjasldkfjasldkfj'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in nested column names', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW ?test.?test2'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('ROW ?test, ?test.?test2.?test3'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in nested column names, where first part is not a param', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW not_a_param.?test2'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('ROW not_a_param.?asdfasdfasdf, not_a_param.?test2.?test3'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in function name, function arg, and column name in STATS command', async () => { + const { validate } = await setup(); + + const res1 = await validate('FROM index | STATS x = max(doubleField) BY textField'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('FROM index | STATS x = max(?param1) BY textField'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + + const res3 = await validate('FROM index | STATS x = max(?param1) BY ?param2'); + expect(res3).toMatchObject({ errors: [], warnings: [] }); + + const res4 = await validate('FROM index | STATS x = ?param3(?param1) BY ?param2'); + expect(res4).toMatchObject({ errors: [], warnings: [] }); + + const res5 = await validate( + 'FROM index | STATS x = ?param3(?param1, ?param4), y = ?param4(?param4, ?param4, ?param4) BY ?param2, ?param5' + ); + expect(res5).toMatchObject({ errors: [], warnings: [] }); + }); +}); + +describe('allows unnamed params', () => { + test('in column names', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW ?'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in nested column names', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW ?.?'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('ROW ?, ?.?.?'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in nested column names, where first part is not a param', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW not_a_param.?'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('ROW not_a_param.?, not_a_param.?.?'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in function name, function arg, and column name in STATS command', async () => { + const { validate } = await setup(); + + const res1 = await validate('FROM index | STATS x = max(doubleField) BY textField'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('FROM index | STATS x = max(?) BY textField'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + + const res3 = await validate('FROM index | STATS x = max(?) BY ?'); + expect(res3).toMatchObject({ errors: [], warnings: [] }); + + const res4 = await validate('FROM index | STATS x = ?(?) BY ?'); + expect(res4).toMatchObject({ errors: [], warnings: [] }); + + const res5 = await validate('FROM index | STATS x = ?(?, ?), y = ?(?, ?, ?) BY ?, ?'); + expect(res5).toMatchObject({ errors: [], warnings: [] }); + }); +}); + +describe('allows positional params', () => { + test('in column names', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW ?0'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in nested column names', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW ?0.?0'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('ROW ?0, ?0.?0.?0'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in nested column names, where first part is not a param', async () => { + const { validate } = await setup(); + + const res1 = await validate('ROW not_a_param.?1'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('ROW not_a_param.?2, not_a_param.?3.?4'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + }); + + test('in function name, function arg, and column name in STATS command', async () => { + const { validate } = await setup(); + + const res1 = await validate('FROM index | STATS x = max(doubleField) BY textField'); + expect(res1).toMatchObject({ errors: [], warnings: [] }); + + const res2 = await validate('FROM index | STATS x = max(?0) BY textField'); + expect(res2).toMatchObject({ errors: [], warnings: [] }); + + const res3 = await validate('FROM index | STATS x = max(?0) BY ?0'); + expect(res3).toMatchObject({ errors: [], warnings: [] }); + + const res4 = await validate('FROM index | STATS x = ?1(?1) BY ?1'); + expect(res4).toMatchObject({ errors: [], warnings: [] }); + + const res5 = await validate('FROM index | STATS x = ?0(?0, ?0), y = ?2(?2, ?2, ?2) BY ?3, ?3'); + expect(res5).toMatchObject({ errors: [], warnings: [] }); + }); +}); diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts b/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts index ae00f300c1878..0f82d7fe4aad9 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts @@ -15,6 +15,7 @@ import type { ESQLLocation, ESQLMessage, } from '@kbn/esql-ast'; +import { ESQLIdentifier } from '@kbn/esql-ast/src/types'; import type { ErrorTypes, ErrorValues } from './types'; function getMessageAndTypeFromId({ @@ -477,7 +478,7 @@ export const errors = { unknownFunction: (fn: ESQLFunction): ESQLMessage => errors.byId('unknownFunction', fn.location, fn), - unknownColumn: (column: ESQLColumn): ESQLMessage => + unknownColumn: (column: ESQLColumn | ESQLIdentifier): ESQLMessage => errors.byId('unknownColumn', column.location, { name: column.name, }), @@ -494,9 +495,12 @@ export const errors = { expression: fn.text, }), - unknownAggFunction: (col: ESQLColumn, type: string = 'FieldAttribute'): ESQLMessage => - errors.byId('unknownAggregateFunction', col.location, { - value: col.name, + unknownAggFunction: ( + node: ESQLColumn | ESQLIdentifier, + type: string = 'FieldAttribute' + ): ESQLMessage => + errors.byId('unknownAggregateFunction', node.location, { + value: node.name, type, }), diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index f1e71c9ff6a97..d799b30d5f20a 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -2666,8 +2666,7 @@ { "query": "from a_index | dissect textField .", "error": [ - "SyntaxError: mismatched input '' expecting {'?', NAMED_OR_POSITIONAL_PARAM, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}", - "Unknown column [textField.]" + "SyntaxError: mismatched input '' expecting {'?', NAMED_OR_POSITIONAL_PARAM, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}" ], "warning": [] }, @@ -2761,8 +2760,7 @@ { "query": "from a_index | grok textField .", "error": [ - "SyntaxError: mismatched input '' expecting {'?', NAMED_OR_POSITIONAL_PARAM, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}", - "Unknown column [textField.]" + "SyntaxError: mismatched input '' expecting {'?', NAMED_OR_POSITIONAL_PARAM, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}" ], "warning": [] }, diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index a9ecac9663609..c29273e4c3f35 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -700,7 +700,6 @@ describe('validation logic', () => { ]); testErrorsAndWarnings('from a_index | dissect textField .', [ "SyntaxError: mismatched input '' expecting {'?', NAMED_OR_POSITIONAL_PARAM, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}", - 'Unknown column [textField.]', ]); testErrorsAndWarnings('from a_index | dissect textField %a', [ "SyntaxError: mismatched input '%' expecting QUOTED_STRING", @@ -751,7 +750,6 @@ describe('validation logic', () => { ]); testErrorsAndWarnings('from a_index | grok textField .', [ "SyntaxError: mismatched input '' expecting {'?', NAMED_OR_POSITIONAL_PARAM, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}", - 'Unknown column [textField.]', ]); testErrorsAndWarnings('from a_index | grok textField %a', [ "SyntaxError: mismatched input '%' expecting QUOTED_STRING", diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts index 111fe79b3f5e0..b43a9e5c336b5 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -21,7 +21,7 @@ import { ESQLSource, walk, } from '@kbn/esql-ast'; -import type { ESQLAstField } from '@kbn/esql-ast/src/types'; +import type { ESQLAstField, ESQLIdentifier } from '@kbn/esql-ast/src/types'; import { CommandModeDefinition, CommandOptionsDefinition, @@ -54,6 +54,10 @@ import { getQuotedColumnName, isInlineCastItem, getSignaturesWithMatchingArity, + isIdentifier, + isFunctionOperatorParam, + isMaybeAggFunction, + isParametrized, } from '../shared/helpers'; import { collectVariables } from '../shared/variables'; import { getMessageFromId, errors } from './errors'; @@ -235,7 +239,7 @@ function validateFunctionColumnArg( parentCommand: string ) { const messages: ESQLMessage[] = []; - if (!isColumnItem(actualArg)) { + if (!(isColumnItem(actualArg) || isIdentifier(actualArg))) { return messages; } @@ -317,7 +321,7 @@ function removeInlineCasts(arg: ESQLAstItem): ESQLAstItem { } function validateFunction( - astFunction: ESQLFunction, + fn: ESQLFunction, parentCommand: string, parentOption: string | undefined, references: ReferenceMaps, @@ -326,16 +330,20 @@ function validateFunction( ): ESQLMessage[] { const messages: ESQLMessage[] = []; - if (astFunction.incomplete) { + if (fn.incomplete) { return messages; } - const fnDefinition = getFunctionDefinition(astFunction.name)!; - const isFnSupported = isSupportedFunction(astFunction.name, parentCommand, parentOption); + if (isFunctionOperatorParam(fn)) { + return messages; + } + + const fnDefinition = getFunctionDefinition(fn.name)!; + const isFnSupported = isSupportedFunction(fn.name, parentCommand, parentOption); if (!isFnSupported.supported) { if (isFnSupported.reason === 'unknownFunction') { - messages.push(errors.unknownFunction(astFunction)); + messages.push(errors.unknownFunction(fn)); } // for nested functions skip this check and make the nested check fail later on if (isFnSupported.reason === 'unsupportedFunction' && !isNested) { @@ -344,16 +352,16 @@ function validateFunction( ? getMessageFromId({ messageId: 'unsupportedFunctionForCommandOption', values: { - name: astFunction.name, + name: fn.name, command: parentCommand.toUpperCase(), option: parentOption.toUpperCase(), }, - locations: astFunction.location, + locations: fn.location, }) : getMessageFromId({ messageId: 'unsupportedFunctionForCommand', - values: { name: astFunction.name, command: parentCommand.toUpperCase() }, - locations: astFunction.location, + values: { name: fn.name, command: parentCommand.toUpperCase() }, + locations: fn.location, }) ); } @@ -361,7 +369,7 @@ function validateFunction( return messages; } } - const matchingSignatures = getSignaturesWithMatchingArity(fnDefinition, astFunction); + const matchingSignatures = getSignaturesWithMatchingArity(fnDefinition, fn); if (!matchingSignatures.length) { const { max, min } = getMaxMinNumberOfParams(fnDefinition); if (max === min) { @@ -369,24 +377,24 @@ function validateFunction( getMessageFromId({ messageId: 'wrongArgumentNumber', values: { - fn: astFunction.name, + fn: fn.name, numArgs: max, - passedArgs: astFunction.args.length, + passedArgs: fn.args.length, }, - locations: astFunction.location, + locations: fn.location, }) ); - } else if (astFunction.args.length > max) { + } else if (fn.args.length > max) { messages.push( getMessageFromId({ messageId: 'wrongArgumentNumberTooMany', values: { - fn: astFunction.name, + fn: fn.name, numArgs: max, - passedArgs: astFunction.args.length, - extraArgs: astFunction.args.length - max, + passedArgs: fn.args.length, + extraArgs: fn.args.length - max, }, - locations: astFunction.location, + locations: fn.location, }) ); } else { @@ -394,19 +402,19 @@ function validateFunction( getMessageFromId({ messageId: 'wrongArgumentNumberTooFew', values: { - fn: astFunction.name, + fn: fn.name, numArgs: min, - passedArgs: astFunction.args.length, - missingArgs: min - astFunction.args.length, + passedArgs: fn.args.length, + missingArgs: min - fn.args.length, }, - locations: astFunction.location, + locations: fn.location, }) ); } } // now perform the same check on all functions args - for (let i = 0; i < astFunction.args.length; i++) { - const arg = astFunction.args[i]; + for (let i = 0; i < fn.args.length; i++) { + const arg = fn.args[i]; const allMatchingArgDefinitionsAreConstantOnly = matchingSignatures.every((signature) => { return signature.params[i]?.constantOnly; @@ -446,7 +454,7 @@ function validateFunction( // use the nesting flag for now just for stats and metrics // TODO: revisit this part later on to make it more generic ['stats', 'inlinestats', 'metrics'].includes(parentCommand) - ? isNested || !isAssignment(astFunction) + ? isNested || !isAssignment(fn) : false ); @@ -454,7 +462,7 @@ function validateFunction( const consolidatedMessage = getMessageFromId({ messageId: 'expectedConstant', values: { - fn: astFunction.name, + fn: fn.name, given: subArg.text, }, locations: subArg.location, @@ -472,7 +480,7 @@ function validateFunction( } // check if the definition has some specific validation to apply: if (fnDefinition.validate) { - const payloads = fnDefinition.validate(astFunction); + const payloads = fnDefinition.validate(fn); if (payloads.length) { messages.push(...payloads); } @@ -481,7 +489,7 @@ function validateFunction( const failingSignatures: ESQLMessage[][] = []; for (const signature of matchingSignatures) { const failingSignature: ESQLMessage[] = []; - astFunction.args.forEach((outerArg, index) => { + fn.args.forEach((outerArg, index) => { const argDef = getParamAtPosition(signature, index); if ((!outerArg && argDef?.optional) || !argDef) { // that's ok, just skip it @@ -502,7 +510,7 @@ function validateFunction( validateInlineCastArg, ].flatMap((validateFn) => { return validateFn( - astFunction, + fn, arg, { ...argDef, @@ -521,7 +529,7 @@ function validateFunction( ? collapseWrongArgumentTypeMessages( messagesFromAllArgElements, outerArg, - astFunction.name, + fn.name, argDef.type as string, parentCommand, references @@ -599,10 +607,11 @@ function validateSetting( * recursively terminate at either a literal or an aggregate function. */ const isFunctionAggClosed = (fn: ESQLFunction): boolean => - isAggFunction(fn) || areFunctionArgsAggClosed(fn); + isMaybeAggFunction(fn) || areFunctionArgsAggClosed(fn); const areFunctionArgsAggClosed = (fn: ESQLFunction): boolean => - fn.args.every((arg) => isLiteralItem(arg) || (isFunctionItem(arg) && isFunctionAggClosed(arg))); + fn.args.every((arg) => isLiteralItem(arg) || (isFunctionItem(arg) && isFunctionAggClosed(arg))) || + isFunctionOperatorParam(fn); /** * Looks for first nested aggregate function in an aggregate function, recursively. @@ -610,7 +619,7 @@ const areFunctionArgsAggClosed = (fn: ESQLFunction): boolean => const findNestedAggFunctionInAggFunction = (agg: ESQLFunction): ESQLFunction | undefined => { for (const arg of agg.args) { if (isFunctionItem(arg)) { - return isAggFunction(arg) ? arg : findNestedAggFunctionInAggFunction(arg); + return isMaybeAggFunction(arg) ? arg : findNestedAggFunctionInAggFunction(arg); } } }; @@ -627,7 +636,7 @@ const findNestedAggFunction = ( fn: ESQLFunction, parentIsAgg: boolean = false ): ESQLFunction | undefined => { - if (isAggFunction(fn)) { + if (isMaybeAggFunction(fn)) { return parentIsAgg ? fn : findNestedAggFunctionInAggFunction(fn); } @@ -675,7 +684,7 @@ const validateAggregates = ( hasMissingAggregationFunctionError = true; messages.push(errors.noAggFunction(command, aggregate)); } - } else if (isColumnItem(aggregate)) { + } else if (isColumnItem(aggregate) || isIdentifier(aggregate)) { messages.push(errors.unknownAggFunction(aggregate)); } else { // Should never happen. @@ -834,14 +843,13 @@ function validateSource( } function validateColumnForCommand( - column: ESQLColumn, + column: ESQLColumn | ESQLIdentifier, commandName: string, references: ReferenceMaps ): ESQLMessage[] { const messages: ESQLMessage[] = []; - if (commandName === 'row') { - if (!references.variables.has(column.name)) { + if (!references.variables.has(column.name) && !isParametrized(column)) { messages.push(errors.unknownColumn(column)); } } else { @@ -990,7 +998,7 @@ function validateCommand(command: ESQLCommand, references: ReferenceMaps): ESQLM ) ); } - if (isColumnItem(arg)) { + if (isColumnItem(arg) || isIdentifier(arg)) { if (command.name === 'stats' || command.name === 'inlinestats') { messages.push(errors.unknownAggFunction(arg)); } else { From f81ee324d2735d202c6d42c553b39adbf1af6d0d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 02:37:18 +1100 Subject: [PATCH 25/47] [8.x] [Security Solution] Deletes Old Timeline Code (#196243) (#199287) # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Deletes Old Timeline Code (#196243)](https://github.com/elastic/kibana/pull/196243) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Jatin Kathuria --- .../control_columns/row_action/index.tsx | 2 +- .../common/components/events_viewer/index.tsx | 3 +- .../header_actions/header_actions.tsx | 2 +- .../utils/get_mapped_non_ecs_value.test.ts | 56 + .../common/utils/get_mapped_non_ecs_value.ts | 40 + .../components/alerts_table/actions.test.tsx | 2 +- .../use_investigate_in_timeline.tsx | 2 +- .../observablity_alerts/render_cell_value.tsx | 2 +- .../render_cell_value.tsx | 2 +- .../components/graph_overlay/index.tsx | 2 +- .../actions/new_timeline_button.test.tsx | 2 +- .../components/new_timeline/index.test.tsx | 2 +- .../components/open_timeline/helpers.test.ts | 2 +- .../components/open_timeline/helpers.ts | 6 +- .../components/open_timeline/index.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 513 -------- .../body/column_headers/actions/index.tsx | 70 -- .../body/column_headers/column_header.tsx | 307 ----- .../body/column_headers/default_headers.ts | 17 +- .../filter/__snapshots__/index.test.tsx.snap | 9 - .../body/column_headers/filter/index.test.tsx | 55 - .../body/column_headers/filter/index.tsx | 39 - .../header/__snapshots__/index.test.tsx.snap | 77 -- .../column_headers/header/header_content.tsx | 84 -- .../body/column_headers/header/helpers.ts | 56 - .../body/column_headers/header/index.test.tsx | 364 ------ .../body/column_headers/header/index.tsx | 118 -- .../body/column_headers/header/selectors.tsx | 13 - .../__snapshots__/index.test.tsx.snap | 67 -- .../header_tooltip_content/index.test.tsx | 89 -- .../header_tooltip_content/index.tsx | 86 -- .../body/column_headers/helpers.test.ts | 11 +- .../body/column_headers/index.test.tsx | 336 ------ .../timeline/body/column_headers/index.tsx | 316 ----- .../range_picker/index.test.tsx | 47 - .../column_headers/range_picker/index.tsx | 51 - .../column_headers/range_picker/ranges.ts | 11 - .../range_picker/translations.ts | 24 - .../__snapshots__/index.test.tsx.snap | 11 - .../column_headers/text_filter/index.test.tsx | 79 -- .../body/column_headers/text_filter/index.tsx | 57 - .../__snapshots__/index.test.tsx.snap | 1037 ----------------- .../body/data_driven_columns/index.test.tsx | 93 -- .../body/data_driven_columns/index.tsx | 472 -------- .../stateful_cell.test.tsx | 171 --- .../data_driven_columns/stateful_cell.tsx | 71 -- .../body/data_driven_columns/translations.ts | 28 - .../body/events/event_column_view.test.tsx | 164 --- .../body/events/event_column_view.tsx | 225 ---- .../components/timeline/body/events/index.tsx | 111 -- .../timeline/body/events/stateful_event.tsx | 266 ----- .../components/timeline/body/index.test.tsx | 407 ------- .../components/timeline/body/index.tsx | 271 ----- .../body/mini_map/date_ranges.test.ts | 147 --- .../timeline/body/mini_map/date_ranges.ts | 76 -- .../renderers/formatted_field_udt.test.tsx | 2 +- .../renderers/get_column_renderer.test.tsx | 10 +- .../renderers/reason_column_renderer.test.tsx | 2 +- .../sort_indicator.test.tsx.snap | 19 - .../components/timeline/body/sort/index.ts | 11 - .../body/sort/sort_indicator.test.tsx | 85 -- .../timeline/body/sort/sort_indicator.tsx | 68 -- .../timeline/body/sort/sort_number.tsx | 27 - .../body/unified_timeline_body.test.tsx | 2 +- .../timeline/body/unified_timeline_body.tsx | 2 +- .../cell_rendering/default_cell_renderer.tsx | 2 +- .../components/timeline/helpers.test.tsx | 40 + .../timelines/components/timeline/helpers.tsx | 11 + .../timelines/components/timeline/index.tsx | 2 +- .../timelines/components/timeline/styles.tsx | 279 ----- .../timeline/tabs/eql/index.test.tsx | 3 - .../timeline/tabs/pinned/index.test.tsx | 5 +- .../components/timeline/tabs/pinned/index.tsx | 4 +- .../timeline/tabs/query/index.test.tsx | 10 +- .../tabs/session/use_session_view.tsx | 2 +- .../timeline/tabs/shared/layout.tsx | 43 +- .../tabs/shared/use_timeline_columns.test.ts | 2 +- .../tabs/shared/use_timeline_columns.tsx | 2 +- .../components/timeline/tabs/shared/utils.ts | 2 +- .../custom_timeline_data_grid_body.test.tsx | 2 +- .../data_table/index.test.tsx | 2 +- .../unified_components/default_headers.tsx | 53 - .../unified_components/index.test.tsx | 6 +- .../timeline/unified_components/index.tsx | 5 +- .../hooks/use_create_timeline.test.tsx | 2 +- .../timelines/hooks/use_create_timeline.tsx | 2 +- .../public/timelines/store/defaults.ts | 5 +- .../public/timelines/store/helpers.test.ts | 6 +- .../public/timelines/store/helpers.ts | 4 +- .../translations/translations/fr-FR.json | 7 - .../translations/translations/ja-JP.json | 7 - .../translations/translations/zh-CN.json | 7 - 92 files changed, 212 insertions(+), 7104 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/selectors.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/default_headers.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx index 5ec3e0c2d0e3d..aebf34f094027 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -19,12 +19,12 @@ import type { SetEventsLoading, ControlColumnProps, } from '../../../../../common/types'; -import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns'; import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { useTourContext } from '../../guided_onboarding_tour'; import { AlertsCasesTourSteps, SecurityStepId } from '../../guided_onboarding_tour/tour_config'; +import { getMappedNonEcsValue } from '../../../utils/get_mapped_non_ecs_value'; export type RowActionProps = EuiDataGridCellValueElementProps & { columnHeaders: ColumnHeaderOptions[]; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index e251370c7e4d3..fb13cf4a3ceed 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -36,7 +36,6 @@ import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; import type { EuiDataGridRowHeightsOptions } from '@elastic/eui'; import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy'; import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../../../common/constants'; -import type { Sort } from '../../../timelines/components/timeline/body/sort'; import type { ControlColumnProps, OnRowSelected, @@ -44,7 +43,7 @@ import type { SetEventsDeleted, SetEventsLoading, } from '../../../../common/types'; -import type { RowRenderer } from '../../../../common/types/timeline'; +import type { RowRenderer, SortColumnTimeline as Sort } from '../../../../common/types/timeline'; import { InputsModelId } from '../../store/inputs/constants'; import type { State } from '../../store'; import { inputsActions } from '../../store/actions'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx index 1341cb72104d8..db88061d86013 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx @@ -10,9 +10,9 @@ import { EuiButtonIcon, EuiToolTip, EuiCheckbox } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { isFullScreen } from '../../../timelines/components/timeline/helpers'; import type { HeaderActionProps } from '../../../../common/types'; import { TimelineId } from '../../../../common/types'; -import { isFullScreen } from '../../../timelines/components/timeline/body/column_headers'; import { isActiveTimeline } from '../../../helpers'; import { getColumnHeader } from '../../../timelines/components/timeline/body/column_headers/helpers'; import { timelineActions } from '../../../timelines/store'; diff --git a/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.test.ts b/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.test.ts new file mode 100644 index 0000000000000..70e3078fd6301 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; +import { getMappedNonEcsValue, useGetMappedNonEcsValue } from './get_mapped_non_ecs_value'; +import { renderHook } from '@testing-library/react-hooks'; + +describe('getMappedNonEcsValue', () => { + it('should return the correct value', () => { + const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }]; + const fieldName = 'field1'; + const result = getMappedNonEcsValue({ data, fieldName }); + expect(result).toEqual(['value1']); + }); + + it('should return undefined if item is null', () => { + const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }]; + const fieldName = 'field2'; + const result = getMappedNonEcsValue({ data, fieldName }); + expect(result).toEqual(undefined); + }); + + it('should return undefined if item.value is null', () => { + const data: TimelineNonEcsData[] = [{ field: 'field1', value: null }]; + const fieldName = 'non_existent_field'; + const result = getMappedNonEcsValue({ data, fieldName }); + expect(result).toEqual(undefined); + }); + + it('should return undefined if data is undefined', () => { + const data = undefined; + const fieldName = 'field1'; + const result = getMappedNonEcsValue({ data, fieldName }); + expect(result).toEqual(undefined); + }); + + it('should return undefined if data is empty', () => { + const data: TimelineNonEcsData[] = []; + const fieldName = 'field1'; + const result = getMappedNonEcsValue({ data, fieldName }); + expect(result).toEqual(undefined); + }); +}); + +describe('useGetMappedNonEcsValue', () => { + it('should return the correct value', () => { + const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }]; + const fieldName = 'field1'; + const { result } = renderHook(() => useGetMappedNonEcsValue({ data, fieldName })); + expect(result.current).toEqual(['value1']); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.ts b/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.ts new file mode 100644 index 0000000000000..e0711127e1e40 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; +import { useMemo } from 'react'; + +export const getMappedNonEcsValue = ({ + data, + fieldName, +}: { + data?: TimelineNonEcsData[]; + fieldName: string; +}): string[] | undefined => { + /* + While data _should_ always be defined + There is the potential for race conditions where a component using this function + is still visible in the UI, while the data has since been removed. + To cover all scenarios where this happens we'll check for the presence of data here + */ + if (!data || data.length === 0) return undefined; + const item = data.find((d) => d.field === fieldName); + if (item != null && item.value != null) { + return item.value; + } + return undefined; +}; + +export const useGetMappedNonEcsValue = ({ + data, + fieldName, +}: { + data?: TimelineNonEcsData[]; + fieldName: string; +}): string[] | undefined => { + return useMemo(() => getMappedNonEcsValue({ data, fieldName }), [data, fieldName]); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 3d284b89f0745..4eeb343134014 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -57,7 +57,7 @@ import { } from '@kbn/lists-plugin/common/constants.mock'; import { of } from 'rxjs'; import { timelineDefaults } from '../../../timelines/store/defaults'; -import { defaultUdtHeaders } from '../../../timelines/components/timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; jest.mock('../../../timelines/containers/api', () => ({ getTimelineTemplate: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index a67eb08496dd0..d7df06616f221 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -33,7 +33,7 @@ import { getField } from '../../../../helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; -import { defaultUdtHeaders } from '../../../../timelines/components/timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../../../../timelines/components/timeline/body/column_headers/default_headers'; interface UseInvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx index 557a1ff9038df..761ad573a5cd8 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx @@ -12,9 +12,9 @@ import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; import { ALERT_DURATION, ALERT_REASON, ALERT_SEVERITY, ALERT_STATUS } from '@kbn/rule-data-utils'; +import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value'; import { TruncatableText } from '../../../../common/components/truncatable_text'; import { Severity } from '../../../components/severity'; -import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns'; import type { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering'; import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { Status } from '../../../components/status'; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx index 75b888f84f334..62eddb4b6b49c 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx @@ -9,10 +9,10 @@ import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; import { ALERT_SEVERITY, ALERT_REASON } from '@kbn/rule-data-utils'; import React from 'react'; +import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value'; import { DefaultDraggable } from '../../../../common/components/draggables'; import { TruncatableText } from '../../../../common/components/truncatable_text'; import { Severity } from '../../../components/severity'; -import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns'; import type { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering'; import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 75c074c517758..4e298e50d2a26 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -29,12 +29,12 @@ import { useGlobalFullScreen, useTimelineFullScreen, } from '../../../common/containers/use_full_screen'; -import { isFullScreen } from '../timeline/body/column_headers'; import { inputsActions } from '../../../common/store/actions'; import { Resolver } from '../../../resolver/view'; import { useTimelineDataFilters } from '../../containers/use_timeline_data_filters'; import { timelineSelectors } from '../../store'; import { timelineDefaults } from '../../store/defaults'; +import { isFullScreen } from '../timeline/helpers'; const SESSION_VIEW_FULL_SCREEN = 'sessionViewFullScreen'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx index c26b34dffcaf4..92f9b874f8743 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx @@ -12,7 +12,7 @@ import { TimelineId } from '../../../../../common/types'; import { timelineActions } from '../../../store'; import { TestProviders } from '../../../../common/mock'; import { RowRendererValues } from '../../../../../common/api/timeline'; -import { defaultUdtHeaders } from '../../timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../../timeline/body/column_headers/default_headers'; jest.mock('../../../../common/components/discover_in_timeline/use_discover_in_timeline_context'); jest.mock('../../../../common/hooks/use_selector'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx index 8e08e4b957d64..0d1ed98f0bc99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx @@ -17,7 +17,7 @@ import { TimelineTypeEnum, } from '../../../../common/api/timeline'; import { TestProviders } from '../../../common/mock'; -import { defaultUdtHeaders } from '../timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers'; jest.mock('../../../common/components/discover_in_timeline/use_discover_in_timeline_context'); jest.mock('../../../common/hooks/use_selector'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5da977c30a410..917f1d1bc29db 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -35,7 +35,7 @@ import { mockTemplate as mockSelectedTemplate, } from './__mocks__'; import { resolveTimeline } from '../../containers/api'; -import { defaultUdtHeaders } from '../timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers'; jest.mock('../../../common/hooks/use_experimental_features'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index b3f84a82d4ed0..e9c1d85b9049e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -35,7 +35,10 @@ import { useUpdateTimeline } from './use_update_timeline'; import type { TimelineModel } from '../../store/model'; import { timelineDefaults } from '../../store/defaults'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { + defaultColumnHeaderType, + defaultUdtHeaders, +} from '../timeline/body/column_headers/default_headers'; import type { OpenTimelineResult, TimelineErrorCallback } from './types'; import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; @@ -46,7 +49,6 @@ import { DEFAULT_TO_MOMENT, } from '../../../common/utils/default_date_settings'; import { resolveTimeline } from '../../containers/api'; -import { defaultUdtHeaders } from '../timeline/unified_components/default_headers'; import { timelineActions } from '../../store'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 6afd900185af7..8af06fe910f99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -53,7 +53,7 @@ import { SourcererScopeName } from '../../../sourcerer/store/model'; import { useSourcererDataView } from '../../../sourcerer/containers'; import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction'; import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions'; -import { defaultUdtHeaders } from '../timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../../store/defaults'; interface OwnProps { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 48209711babbf..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,513 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx deleted file mode 100644 index 025d12bc6ddc5..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx +++ /dev/null @@ -1,70 +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 { EuiButtonIcon } from '@elastic/eui'; -import React, { useCallback } from 'react'; - -import type { ColumnHeaderOptions } from '../../../../../../../common/types'; -import type { OnColumnRemoved } from '../../../events'; -import { EventsHeadingExtra, EventsLoading } from '../../../styles'; -import type { Sort } from '../../sort'; - -import * as i18n from '../translations'; - -interface Props { - header: ColumnHeaderOptions; - isLoading: boolean; - onColumnRemoved: OnColumnRemoved; - sort: Sort[]; -} - -/** Given a `header`, returns the `SortDirection` applicable to it */ - -export const CloseButton = React.memo<{ - columnId: string; - onColumnRemoved: OnColumnRemoved; -}>(({ columnId, onColumnRemoved }) => { - const handleClick = useCallback( - (event: React.MouseEvent) => { - // To avoid a re-sorting when you delete a column - event.preventDefault(); - event.stopPropagation(); - onColumnRemoved(columnId); - }, - [columnId, onColumnRemoved] - ); - - return ( - - ); -}); - -CloseButton.displayName = 'CloseButton'; - -export const Actions = React.memo(({ header, onColumnRemoved, sort, isLoading }) => { - return ( - <> - {sort.some((i) => i.columnId === header.id) && isLoading ? ( - - - - ) : ( - - - - )} - - ); -}); - -Actions.displayName = 'Actions'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx deleted file mode 100644 index 2048cefb9bf0f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; -import { EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import type { DraggableChildrenFn } from '@hello-pangea/dnd'; -import { Draggable } from '@hello-pangea/dnd'; -import type { ResizeCallback } from 're-resizable'; -import { Resizable } from 're-resizable'; -import { useDispatch } from 'react-redux'; -import styled from 'styled-components'; -import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; - -import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../constants'; -import { getDraggableFieldId } from '../../../../../common/components/drag_and_drop/helpers'; -import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; -import { TimelineTabs } from '../../../../../../common/types/timeline'; -import { Direction } from '../../../../../../common/search_strategy'; -import type { OnFilterChange } from '../../events'; -import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; -import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; -import type { Sort } from '../sort'; - -import { Header } from './header'; -import { timelineActions } from '../../../../store'; - -import * as i18n from './translations'; - -const ContextMenu = styled(EuiContextMenu)` - width: 115px; - - & .euiContextMenuItem { - font-size: 12px; - padding: 4px 8px; - width: 115px; - } -`; - -const PopoverContainer = styled.div<{ $width: number }>` - & .euiPopover { - padding-right: 8px; - width: ${({ $width }) => $width}px; - } -`; - -const RESIZABLE_ENABLE = { right: true }; - -interface ColumneHeaderProps { - draggableIndex: number; - header: ColumnHeaderOptions; - isDragging: boolean; - onFilterChange?: OnFilterChange; - sort: Sort[]; - tabType: TimelineTabs; - timelineId: string; -} - -const ColumnHeaderComponent: React.FC = ({ - draggableIndex, - header, - timelineId, - isDragging, - onFilterChange, - sort, - tabType, -}) => { - const keyboardHandlerRef = useRef(null); - const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); - const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []); - - const dispatch = useDispatch(); - const resizableSize = useMemo( - () => ({ - width: header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH, - height: 'auto', - }), - [header.initialWidth] - ); - const resizableStyle: { - position: 'absolute' | 'relative'; - } = useMemo( - () => ({ - position: isDragging ? 'absolute' : 'relative', - }), - [isDragging] - ); - const resizableHandleComponent = useMemo( - () => ({ - right: , - }), - [] - ); - const handleResizeStop: ResizeCallback = useCallback( - (e, direction, ref, delta) => { - dispatch( - timelineActions.applyDeltaToColumnWidth({ - columnId: header.id, - delta: delta.width, - id: timelineId, - }) - ); - }, - [dispatch, header.id, timelineId] - ); - const draggableId = useMemo( - () => - getDraggableFieldId({ - contextId: `timeline-column-headers-${tabType}-${timelineId}`, - fieldId: header.id, - }), - [tabType, timelineId, header.id] - ); - - const onColumnSort = useCallback( - (sortDirection: Direction) => { - const columnId = header.id; - const columnType = header.type ?? ''; - const esTypes = header.esTypes ?? []; - const headerIndex = sort.findIndex((col) => col.columnId === columnId); - const newSort = - headerIndex === -1 - ? [ - ...sort, - { - columnId, - columnType, - esTypes, - sortDirection, - }, - ] - : [ - ...sort.slice(0, headerIndex), - { - columnId, - columnType, - esTypes, - sortDirection, - }, - ...sort.slice(headerIndex + 1), - ]; - - dispatch( - timelineActions.updateSort({ - id: timelineId, - sort: newSort, - }) - ); - }, - [dispatch, header, sort, timelineId] - ); - - const handleClosePopOverTrigger = useCallback(() => { - setHoverActionsOwnFocus(false); - restoreFocus(); - }, [restoreFocus]); - - const panels: EuiContextMenuPanelDescriptor[] = useMemo( - () => [ - { - id: 0, - items: [ - { - icon: , - name: i18n.HIDE_COLUMN, - onClick: () => { - dispatch(timelineActions.removeColumn({ id: timelineId, columnId: header.id })); - handleClosePopOverTrigger(); - }, - }, - ...(tabType !== TimelineTabs.eql - ? [ - { - disabled: !header.aggregatable, - icon: , - name: i18n.SORT_AZ, - onClick: () => { - onColumnSort(Direction.asc); - handleClosePopOverTrigger(); - }, - }, - { - disabled: !header.aggregatable, - icon: , - name: i18n.SORT_ZA, - onClick: () => { - onColumnSort(Direction.desc); - handleClosePopOverTrigger(); - }, - }, - ] - : []), - ], - }, - ], - [ - dispatch, - handleClosePopOverTrigger, - header.aggregatable, - header.id, - onColumnSort, - tabType, - timelineId, - ] - ); - - const headerButton = useMemo( - () => ( -
- ), - [header, onFilterChange, sort, timelineId] - ); - - const DraggableContent = useCallback( - (dragProvided) => ( - - - - - - - - - - ), - [handleClosePopOverTrigger, headerButton, header.initialWidth, hoverActionsOwnFocus, panels] - ); - - const onFocus = useCallback(() => { - keyboardHandlerRef.current?.focus(); - }, []); - - const openPopover = useCallback(() => { - setHoverActionsOwnFocus(true); - }, []); - - const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ - closePopover: handleClosePopOverTrigger, - draggableId, - fieldName: header.id, - keyboardHandlerRef, - openPopover, - }); - - const keyDownHandler = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - if (!hoverActionsOwnFocus) { - onKeyDown(keyboardEvent); - } - }, - [hoverActionsOwnFocus, onKeyDown] - ); - - return ( - -
- - {DraggableContent} - -
-
- ); -}; - -export const ColumnHeader = React.memo(ColumnHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts index 56c29462415bd..a249f2ef2a851 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts @@ -6,51 +6,48 @@ */ import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../../common/types'; -import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH, +} from '../constants'; export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; -export const defaultHeaders: ColumnHeaderOptions[] = [ +export const defaultUdtHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, id: '@timestamp', - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + initialWidth: DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH, esTypes: ['date'], type: 'date', }, { columnHeaderType: defaultColumnHeaderType, id: 'message', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH * 2, }, { columnHeaderType: defaultColumnHeaderType, id: 'event.category', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, { columnHeaderType: defaultColumnHeaderType, id: 'event.action', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, { columnHeaderType: defaultColumnHeaderType, id: 'host.name', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, { columnHeaderType: defaultColumnHeaderType, id: 'source.ip', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, { columnHeaderType: defaultColumnHeaderType, id: 'destination.ip', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, { columnHeaderType: defaultColumnHeaderType, id: 'user.name', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap deleted file mode 100644 index ed15a5f635e9f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Filter renders correctly against snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx deleted file mode 100644 index bd6e75efd94d9..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx +++ /dev/null @@ -1,55 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { defaultHeaders } from '../default_headers'; - -import { Filter } from '.'; -import type { ColumnHeaderType } from '../../../../../../../common/types'; - -const textFilter: ColumnHeaderType = 'text-filter'; -const notFiltered: ColumnHeaderType = 'not-filtered'; - -describe('Filter', () => { - test('renders correctly against snapshot', () => { - const textFilterColumnHeader = { - ...defaultHeaders[0], - columnHeaderType: textFilter, - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - describe('rendering', () => { - test('it renders a text filter when the columnHeaderType is "text-filter"', () => { - const textFilterColumnHeader = { - ...defaultHeaders[0], - columnHeaderType: textFilter, - }; - - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="textFilter"]').first().props()).toHaveProperty( - 'placeholder' - ); - }); - - test('it does NOT render a filter when the columnHeaderType is "not-filtered"', () => { - const notFilteredHeader = { - ...defaultHeaders[0], - columnHeaderType: notFiltered, - }; - - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="textFilter"]').exists()).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx deleted file mode 100644 index 3eb2cda8af242..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx +++ /dev/null @@ -1,39 +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 { noop } from 'lodash/fp'; -import React from 'react'; - -import type { ColumnHeaderOptions } from '../../../../../../../common/types'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../../constants'; -import type { OnFilterChange } from '../../../events'; -import { TextFilter } from '../text_filter'; - -interface Props { - header: ColumnHeaderOptions; - onFilterChange?: OnFilterChange; -} - -/** Renders a header's filter, based on the `columnHeaderType` */ -export const Filter = React.memo(({ header, onFilterChange = noop }) => { - switch (header.columnHeaderType) { - case 'text-filter': - return ( - - ); - case 'not-filtered': // fall through - default: - return null; - } -}); - -Filter.displayName = 'Filter'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 0a621f0218337..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header renders correctly against snapshot 1`] = ` - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx deleted file mode 100644 index 24b75c88d7963..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx +++ /dev/null @@ -1,84 +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 { EuiToolTip } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React from 'react'; -import type { ColumnHeaderOptions } from '../../../../../../../common/types/timeline'; - -import { TruncatableText } from '../../../../../../common/components/truncatable_text'; -import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles'; -import type { Sort } from '../../sort'; -import { SortIndicator } from '../../sort/sort_indicator'; -import { HeaderToolTipContent } from '../header_tooltip_content'; -import { getSortDirection, getSortIndex } from './helpers'; -interface HeaderContentProps { - children: React.ReactNode; - header: ColumnHeaderOptions; - isLoading: boolean; - isResizing: boolean; - onClick: () => void; - showSortingCapability: boolean; - sort: Sort[]; -} - -const HeaderContentComponent: React.FC = ({ - children, - header, - isLoading, - isResizing, - onClick, - showSortingCapability, - sort, -}) => ( - - {header.aggregatable && showSortingCapability ? ( - - - } - > - <> - {React.isValidElement(header.display) - ? header.display - : header.displayAsText ?? header.id} - - - - - - - ) : ( - - - } - > - <> - {React.isValidElement(header.display) - ? header.display - : header.displayAsText ?? header.id} - - - - - )} - - {children} - -); - -export const HeaderContent = React.memo(HeaderContentComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts deleted file mode 100644 index e31ed05e55929..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Direction } from '../../../../../../../common/search_strategy'; -import type { - ColumnHeaderOptions, - SortDirection, -} from '../../../../../../../common/types/timeline'; -import type { Sort } from '../../sort'; - -interface GetNewSortDirectionOnClickParams { - clickedHeader: ColumnHeaderOptions; - currentSort: Sort[]; -} - -/** Given a `header`, returns the `SortDirection` applicable to it */ -export const getNewSortDirectionOnClick = ({ - clickedHeader, - currentSort, -}: GetNewSortDirectionOnClickParams): Direction => - currentSort.reduce( - (acc, item) => (clickedHeader.id === item.columnId ? getNextSortDirection(item) : acc), - Direction.desc - ); - -/** Given a current sort direction, it returns the next sort direction */ -export const getNextSortDirection = (currentSort: Sort): Direction => { - switch (currentSort.sortDirection) { - case Direction.desc: - return Direction.asc; - case Direction.asc: - return Direction.desc; - case 'none': - return Direction.desc; - default: - return Direction.desc; - } -}; - -interface GetSortDirectionParams { - header: ColumnHeaderOptions; - sort: Sort[]; -} - -export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection => - sort.reduce( - (acc, item) => (header.id === item.columnId ? item.sortDirection : acc), - 'none' - ); - -export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number => - sort.findIndex((s) => s.columnId === header.id); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx deleted file mode 100644 index abf452b834a1d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx +++ /dev/null @@ -1,364 +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 { mount, shallow } from 'enzyme'; -import React, { type ReactNode } from 'react'; - -import { timelineActions } from '../../../../../store'; -import { TestProviders } from '../../../../../../common/mock'; -import type { Sort } from '../../sort'; -import { CloseButton } from '../actions'; -import { defaultHeaders } from '../default_headers'; - -import { HeaderComponent } from '.'; -import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers'; -import { Direction } from '../../../../../../../common/search_strategy'; -import { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector'; -import type { ColumnHeaderType } from '../../../../../../../common/types'; -import { TimelineId } from '../../../../../../../common/types/timeline'; - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useSelector: jest.fn(), - useDispatch: () => mockDispatch, - }; -}); - -jest.mock('../../../../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn(), - useDeepEqualSelector: jest.fn(), -})); - -const filteredColumnHeader: ColumnHeaderType = 'text-filter'; - -describe('Header', () => { - const columnHeader = defaultHeaders[0]; - const sort: Sort[] = [ - { - columnId: columnHeader.id, - columnType: columnHeader.type ?? '', - esTypes: columnHeader.esTypes ?? [], - sortDirection: Direction.desc, - }, - ]; - const timelineId = TimelineId.test; - - beforeEach(() => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false }); - }); - - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - - - ); - expect(wrapper.find('HeaderComponent').dive()).toMatchSnapshot(); - }); - - describe('rendering', () => { - test('it renders the header text', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() - ).toEqual(columnHeader.id); - }); - - test('it renders the header text alias when displayAsText is provided', () => { - const displayAsText = 'Timestamp'; - const headerWithLabel = { ...columnHeader, displayAsText }; - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() - ).toEqual(displayAsText); - }); - - test('it renders the header as a `ReactNode` when `display` is provided', () => { - const display: React.ReactNode = ( -
- {'The display property renders the column heading as a ReactNode'} -
- ); - const headerWithLabel = { ...columnHeader, display }; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="rendered-via-display"]`).exists()).toBe(true); - }); - - test('it prefers to render `display` instead of `displayAsText` when both are provided', () => { - const displayAsText = 'this text should NOT be rendered'; - const display: React.ReactNode = ( -
{'this text is rendered via display'}
- ); - const headerWithLabel = { ...columnHeader, display, displayAsText }; - const wrapper = mount( - - - - ); - - expect(wrapper.text()).toBe('this text is rendered via display'); - }); - - test('it falls back to rendering header.id when `display` is not a valid React node', () => { - const display = {} as unknown as ReactNode; // a plain object is NOT a `ReactNode` - const headerWithLabel = { ...columnHeader, display }; - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() - ).toEqual(columnHeader.id); - }); - - test('it renders a sort indicator', () => { - const headerSortable = { ...columnHeader, aggregatable: true }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-sort-indicator"]').first().exists()).toEqual( - true - ); - }); - - test('it renders a filter', () => { - const columnWithFilter = { - ...columnHeader, - columnHeaderType: filteredColumnHeader, - }; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="textFilter"]').first().props()).toHaveProperty( - 'placeholder' - ); - }); - }); - - describe('onColumnSorted', () => { - test('it invokes the onColumnSorted callback when the header sort button is clicked', () => { - const headerSortable = { ...columnHeader, aggregatable: true }; - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="header-sort-button"]').first().simulate('click'); - - expect(mockDispatch).toBeCalledWith( - timelineActions.updateSort({ - id: timelineId, - sort: [ - { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - esTypes: columnHeader.esTypes ?? [], - sortDirection: Direction.asc, // (because the previous state was Direction.desc) - }, - ], - }) - ); - }); - - test('it does NOT render the header sort button when aggregatable is false', () => { - const headerSortable = { ...columnHeader, aggregatable: false }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); - }); - - test('it does NOT render the header sort button when aggregatable is missing', () => { - const headerSortable = { ...columnHeader }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); - }); - - test('it does NOT invoke the onColumnSorted callback when the header is clicked and aggregatable is undefined', () => { - const mockOnColumnSorted = jest.fn(); - const headerSortable = { ...columnHeader, aggregatable: undefined }; - const wrapper = mount( - - - - ); - - wrapper.find(`[data-test-subj="header-${columnHeader.id}"]`).first().simulate('click'); - - expect(mockOnColumnSorted).not.toHaveBeenCalled(); - }); - }); - - describe('CloseButton', () => { - test('it invokes the onColumnRemoved callback with the column ID when the close button is clicked', () => { - const mockOnColumnRemoved = jest.fn(); - - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="remove-column"]').first().simulate('click'); - - expect(mockOnColumnRemoved).toBeCalledWith(columnHeader.id); - }); - }); - - describe('getSortDirection', () => { - test('it returns the sort direction when the header id matches the sort column id', () => { - expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort[0].sortDirection); - }); - - test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => { - const nonMatching: Sort[] = [ - { - columnId: 'differentSocks', - columnType: columnHeader.type ?? 'number', - esTypes: columnHeader.esTypes ?? [], - sortDirection: Direction.desc, - }, - ]; - - expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none'); - }); - }); - - describe('getNextSortDirection', () => { - test('it returns "asc" when the current direction is "desc"', () => { - const sortDescending: Sort = { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - esTypes: columnHeader.esTypes ?? [], - sortDirection: Direction.desc, - }; - - expect(getNextSortDirection(sortDescending)).toEqual('asc'); - }); - - test('it returns "desc" when the current direction is "asc"', () => { - const sortAscending: Sort = { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - esTypes: columnHeader.esTypes ?? [], - sortDirection: Direction.asc, - }; - - expect(getNextSortDirection(sortAscending)).toEqual(Direction.desc); - }); - - test('it returns "desc" by default', () => { - const sortNone: Sort = { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - esTypes: columnHeader.esTypes ?? [], - sortDirection: 'none', - }; - - expect(getNextSortDirection(sortNone)).toEqual(Direction.desc); - }); - }); - - describe('getNewSortDirectionOnClick', () => { - test('it returns the expected new sort direction when the header id matches the sort column id', () => { - const sortMatches: Sort[] = [ - { - columnId: columnHeader.id, - columnType: columnHeader.type ?? 'number', - esTypes: columnHeader.esTypes ?? [], - sortDirection: Direction.desc, - }, - ]; - - expect( - getNewSortDirectionOnClick({ - clickedHeader: columnHeader, - currentSort: sortMatches, - }) - ).toEqual(Direction.asc); - }); - - test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => { - const sortDoesNotMatch: Sort[] = [ - { - columnId: 'someOtherColumn', - columnType: columnHeader.type ?? 'number', - esTypes: columnHeader.esTypes ?? [], - sortDirection: 'none', - }, - ]; - - expect( - getNewSortDirectionOnClick({ - clickedHeader: columnHeader, - currentSort: sortDoesNotMatch, - }) - ).toEqual(Direction.desc); - }); - }); - - describe('text truncation styling', () => { - test('truncates the header text with an ellipsis', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).at(1) - ).toHaveStyleRule('text-overflow', 'ellipsis'); - }); - }); - - describe('header tooltip', () => { - test('it has a tooltip to display the properties of the field', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx deleted file mode 100644 index 08af0bf9cbdd1..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx +++ /dev/null @@ -1,118 +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 { noop } from 'lodash/fp'; -import React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; -import { isDataViewFieldSubtypeNested } from '@kbn/es-query'; - -import type { ColumnHeaderOptions } from '../../../../../../../common/types'; -import { - useDeepEqualSelector, - useShallowEqualSelector, -} from '../../../../../../common/hooks/use_selector'; -import { timelineActions, timelineSelectors } from '../../../../../store'; -import type { OnColumnRemoved, OnFilterChange } from '../../../events'; -import type { Sort } from '../../sort'; -import { Actions } from '../actions'; -import { Filter } from '../filter'; -import { getNewSortDirectionOnClick } from './helpers'; -import { HeaderContent } from './header_content'; -import { isEqlOnSelector } from './selectors'; - -interface Props { - header: ColumnHeaderOptions; - onFilterChange?: OnFilterChange; - sort: Sort[]; - timelineId: string; -} - -export const HeaderComponent: React.FC = ({ - header, - onFilterChange = noop, - sort, - timelineId, -}) => { - const dispatch = useDispatch(); - const getIsEqlOn = useMemo(() => isEqlOnSelector(), []); - const isEqlOn = useShallowEqualSelector((state) => getIsEqlOn(state, timelineId)); - - const onColumnSort = useCallback(() => { - const columnId = header.id; - const columnType = header.type ?? ''; - const esTypes = header.esTypes ?? []; - const sortDirection = getNewSortDirectionOnClick({ - clickedHeader: header, - currentSort: sort, - }); - const headerIndex = sort.findIndex((col) => col.columnId === columnId); - let newSort = []; - if (headerIndex === -1) { - newSort = [ - ...sort, - { - columnId, - columnType, - esTypes, - sortDirection, - }, - ]; - } else { - newSort = [ - ...sort.slice(0, headerIndex), - { - columnId, - columnType, - esTypes, - sortDirection, - }, - ...sort.slice(headerIndex + 1), - ]; - } - dispatch( - timelineActions.updateSort({ - id: timelineId, - sort: newSort, - }) - ); - }, [dispatch, header, sort, timelineId]); - - const onColumnRemoved = useCallback( - (columnId) => dispatch(timelineActions.removeColumn({ id: timelineId, columnId })), - [dispatch, timelineId] - ); - - const getManageTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { isLoading } = useDeepEqualSelector( - (state) => getManageTimeline(state, timelineId) || { isLoading: false } - ); - const showSortingCapability = !isEqlOn && !isDataViewFieldSubtypeNested(header); - - return ( - <> - - - - - - - ); -}; - -export const Header = React.memo(HeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/selectors.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/selectors.tsx deleted file mode 100644 index cac232fd6ea34..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/selectors.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createSelector } from 'reselect'; -import { TimelineTabs } from '../../../../../../../common/types/timeline'; -import { selectTimeline } from '../../../../../store/selectors'; - -export const isEqlOnSelector = () => - createSelector(selectTimeline, (timeline) => timeline?.activeTab === TimelineTabs.eql); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 750f3956786a5..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HeaderToolTipContent it renders the expected table content 1`] = ` - -

- - Category - : - - - base - -

-

- - Field - : - - - @timestamp - -

-

- - Type - : - - - - - date - - -

-

- - Description - : - - - Date/time when the event originated. -For log events this is the date/time when the event was generated, and not when it was read. -Required field for all events. - -

-
-`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx deleted file mode 100644 index d2a134f37aba4..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx +++ /dev/null @@ -1,89 +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 { mount, shallow } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React from 'react'; - -import type { ColumnHeaderOptions } from '../../../../../../../common/types'; -import { defaultHeaders } from '../../../../../../common/mock'; -import { HeaderToolTipContent } from '.'; - -describe('HeaderToolTipContent', () => { - let header: ColumnHeaderOptions; - beforeEach(() => { - header = cloneDeep(defaultHeaders[0]); - }); - - test('it renders the category', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="category-value"]').first().text()).toEqual( - header.category - ); - }); - - test('it renders the name of the field', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="field-value"]').first().text()).toEqual(header.id); - }); - - test('it renders the expected icon for the header type', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="type-icon"]').first().props().type).toEqual('clock'); - }); - - test('it renders the type of the field', () => { - const wrapper = mount(); - - expect( - wrapper - .find(`[data-test-subj="type-value-${header.esTypes?.at(0)}"]`) - .first() - .text() - ).toEqual(header.esTypes?.at(0)); - }); - - test('it renders multiple `esTypes`', () => { - const hasMultipleTypes = { ...header, esTypes: ['long', 'date'] }; - - const wrapper = mount(); - - hasMultipleTypes.esTypes.forEach((esType) => { - expect(wrapper.find(`[data-test-subj="type-value-${esType}"]`).first().text()).toEqual( - esType - ); - }); - }); - - test('it renders the description of the field', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="description-value"]').first().text()).toEqual( - header.description - ); - }); - - test('it does NOT render the description column when the field does NOT contain a description', () => { - const noDescription = { - ...header, - description: '', - }; - - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="description"]').exists()).toEqual(false); - }); - - test('it renders the expected table content', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx deleted file mode 100644 index c96f3473a0064..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiIcon, EuiBadge } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import type { ColumnHeaderOptions } from '../../../../../../../common/types'; -import { getIconFromType } from '../../../../../../common/components/event_details/helpers'; -import * as i18n from '../translations'; - -const IconType = styled(EuiIcon)` - margin-right: 3px; - position: relative; - top: -2px; -`; -IconType.displayName = 'IconType'; - -const P = styled.span` - margin-bottom: 5px; -`; -P.displayName = 'P'; - -const ToolTipTableMetadata = styled.span` - margin-right: 5px; - display: block; - font-weight: bold; -`; -ToolTipTableMetadata.displayName = 'ToolTipTableMetadata'; - -const ToolTipTableValue = styled.span` - word-wrap: break-word; -`; -ToolTipTableValue.displayName = 'ToolTipTableValue'; - -export const HeaderToolTipContent = React.memo<{ header: ColumnHeaderOptions }>(({ header }) => ( - <> - {!isEmpty(header.category) && ( -

- - {i18n.CATEGORY} - {':'} - - {header.category} -

- )} -

- - {i18n.FIELD} - {':'} - - {header.id} -

-

- - {i18n.TYPE} - {':'} - - - - {header.esTypes?.map((esType) => ( - - {esType} - - ))} - -

- {!isEmpty(header.description) && ( -

- - {i18n.DESCRIPTION} - {':'} - - - {header.description} - -

- )} - -)); -HeaderToolTipContent.displayName = 'HeaderToolTipContent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts index 8a4f024daac83..816ba731ba4fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts @@ -9,8 +9,7 @@ import { mockBrowserFields } from '../../../../../common/containers/source/mock' import type { BrowserFields } from '../../../../../../common/search_strategy'; import type { ColumnHeaderOptions } from '../../../../../../common/types'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; -import { defaultHeaders } from './default_headers'; -import { defaultUdtHeaders } from '../../unified_components/default_headers'; +import { defaultUdtHeaders } from './default_headers'; import { getColumnWidthFromType, getColumnHeaders, @@ -88,7 +87,7 @@ describe('helpers', () => { }); }); - test('should return the expected metadata in case of unified header', () => { + test('should return the expected metadata in case of default header', () => { const inputHeaders = defaultUdtHeaders; expect(getColumnHeader('@timestamp', inputHeaders)).toEqual({ columnHeaderType: 'not-filtered', @@ -112,7 +111,7 @@ describe('helpers', () => { searchable: true, type: 'date', esTypes: ['date'], - initialWidth: 190, + initialWidth: 215, }, { aggregatable: true, @@ -122,7 +121,6 @@ describe('helpers', () => { searchable: true, type: 'ip', esTypes: ['ip'], - initialWidth: 180, }, { aggregatable: true, @@ -132,10 +130,9 @@ describe('helpers', () => { searchable: true, type: 'ip', esTypes: ['ip'], - initialWidth: 180, }, ]; - const mockHeader = defaultHeaders.filter((h) => + const mockHeader = defaultUdtHeaders.filter((h) => ['@timestamp', 'source.ip', 'destination.ip'].includes(h.id) ); expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx deleted file mode 100644 index c20fcc45ea300..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ /dev/null @@ -1,336 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { defaultHeaders } from './default_headers'; -import { mockBrowserFields } from '../../../../../common/containers/source/mock'; -import type { Sort } from '../sort'; -import { TestProviders } from '../../../../../common/mock/test_providers'; -import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; - -import type { ColumnHeadersComponentProps } from '.'; -import { ColumnHeadersComponent } from '.'; -import { cloneDeep } from 'lodash/fp'; -import { timelineActions } from '../../../../store'; -import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; -import { Direction } from '../../../../../../common/search_strategy'; -import { getDefaultControlColumn } from '../control_columns'; -import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns'; -import type { UseFieldBrowserOptionsProps } from '../../../fields_browser'; -import { mockTriggersActionsUi } from '../../../../../common/mock/mock_triggers_actions_ui_plugin'; -import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; -import { HeaderActions } from '../../../../../common/components/header_actions/header_actions'; -import { getActionsColumnWidth } from '../../../../../common/components/header_actions'; - -jest.mock('../../../../../common/lib/kibana', () => ({ - useKibana: () => ({ - services: { - timelines: mockTimelines, - triggersActionsUi: mockTriggersActionsUi, - }, - }), -})); - -const mockUseFieldBrowserOptions = jest.fn(); -jest.mock('../../../fields_browser', () => ({ - useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), -})); - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); -const timelineId = TimelineId.test; - -describe('ColumnHeaders', () => { - const mount = useMountAppended(); - const ACTION_BUTTON_COUNT = 4; - const actionsColumnWidth = getActionsColumnWidth(ACTION_BUTTON_COUNT); - const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({ - ...x, - headerCellRender: HeaderActions, - })); - const sort: Sort[] = [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, - ]; - const defaultProps: ColumnHeadersComponentProps = { - actionsColumnWidth, - browserFields: mockBrowserFields, - columnHeaders: defaultHeaders, - isSelectAllChecked: false, - onSelectAll: jest.fn, - show: true, - showEventsSelect: false, - showSelectAllCheckbox: false, - sort, - tabType: TimelineTabs.query, - timelineId, - leadingControlColumns, - trailingControlColumns: [], - }; - - describe('rendering', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - - - ); - expect(wrapper.find('ColumnHeadersComponent')).toMatchSnapshot(); - }); - - test('it renders the field browser', () => { - const mockCloseEditor = jest.fn(); - mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => { - editorActionsRef.current = { closeEditor: mockCloseEditor }; - return {}; - }); - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="field-browser"]').first().exists()).toEqual(true); - }); - - test('it renders every column header', () => { - const wrapper = mount( - - - - ); - - defaultHeaders.forEach((h) => { - expect(wrapper.find('[data-test-subj="headers-group"]').first().text()).toContain(h.id); - }); - }); - }); - - describe('#onColumnsSorted', () => { - let mockSort: Sort[] = [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, - { - columnId: 'host.name', - columnType: 'string', - esTypes: [], - sortDirection: Direction.asc, - }, - ]; - let mockDefaultHeaders = cloneDeep( - defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) - ); - - beforeEach(() => { - mockDefaultHeaders = cloneDeep( - defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) - ); - mockSort = [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, - { - columnId: 'host.name', - columnType: 'string', - esTypes: [], - sortDirection: Direction.asc, - }, - ]; - }); - - test('Add column `event.category` as desc sorting', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="header-event.category"] [data-test-subj="header-sort-button"]') - .first() - .simulate('click'); - - expect(mockDispatch).toHaveBeenCalledWith( - timelineActions.updateSort({ - id: timelineId, - sort: [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, - { - columnId: 'host.name', - columnType: 'string', - esTypes: [], - sortDirection: Direction.asc, - }, - { - columnId: 'event.category', - columnType: '', - esTypes: [], - sortDirection: Direction.desc, - }, - ], - }) - ); - }); - - test('Change order of column `@timestamp` from desc to asc without changing index position', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="header-@timestamp"] [data-test-subj="header-sort-button"]') - .first() - .simulate('click'); - expect(mockDispatch).toHaveBeenCalledWith( - timelineActions.updateSort({ - id: timelineId, - sort: [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.asc, - }, - { - columnId: 'host.name', - columnType: 'string', - esTypes: [], - sortDirection: Direction.asc, - }, - ], - }) - ); - }); - - test('Change order of column `host.name` from asc to desc without changing index position', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="header-host.name"] [data-test-subj="header-sort-button"]') - .first() - .simulate('click'); - - expect(mockDispatch).toHaveBeenCalledWith( - timelineActions.updateSort({ - id: timelineId, - sort: [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, - { - columnId: 'host.name', - columnType: '', - esTypes: [], - sortDirection: Direction.desc, - }, - ], - }) - ); - }); - test('Does not render the default leading action column header and renders a custom trailing header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.exists('[data-test-subj="field-browser"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="test-header-action-cell"]')).toBeTruthy(); - }); - }); - - describe('Field Editor', () => { - test('Closes field editor when the timeline is unmounted', () => { - const mockCloseEditor = jest.fn(); - mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => { - editorActionsRef.current = { closeEditor: mockCloseEditor }; - return {}; - }); - - const wrapper = mount( - - - - ); - expect(mockCloseEditor).not.toHaveBeenCalled(); - - wrapper.unmount(); - expect(mockCloseEditor).toHaveBeenCalled(); - }); - - test('Closes field editor when the timeline is closed', () => { - const mockCloseEditor = jest.fn(); - mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => { - editorActionsRef.current = { closeEditor: mockCloseEditor }; - return {}; - }); - - const Proxy = (props: ColumnHeadersComponentProps) => ( - - - - ); - const wrapper = mount(); - expect(mockCloseEditor).not.toHaveBeenCalled(); - - wrapper.setProps({ ...defaultProps, show: false }); - expect(mockCloseEditor).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx deleted file mode 100644 index f343b9af8ed97..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ /dev/null @@ -1,316 +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, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import type { DraggableChildrenFn, DroppableProps } from '@hello-pangea/dnd'; -import { Droppable } from '@hello-pangea/dnd'; - -import { useDispatch } from 'react-redux'; -import type { ControlColumnProps, HeaderActionProps } from '../../../../../../common/types'; -import { removeColumn, upsertColumn } from '../../../../store/actions'; -import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper'; -import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge'; -import type { BrowserFields } from '../../../../../common/containers/source'; -import { - DRAG_TYPE_FIELD, - droppableTimelineColumnsPrefix, -} from '../../../../../common/components/drag_and_drop/helpers'; -import type { ColumnHeaderOptions, TimelineTabs } from '../../../../../../common/types/timeline'; -import type { OnSelectAll } from '../../events'; -import { - EventsTh, - EventsThead, - EventsThGroupData, - EventsTrHeader, - EventsThGroupActions, -} from '../../styles'; -import type { Sort } from '../sort'; -import { ColumnHeader } from './column_header'; - -import { SourcererScopeName } from '../../../../../sourcerer/store/model'; -import type { FieldEditorActions } from '../../../fields_browser'; -import { useFieldBrowserOptions } from '../../../fields_browser'; - -export interface ColumnHeadersComponentProps { - actionsColumnWidth: number; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - isEventViewer?: boolean; - isSelectAllChecked: boolean; - onSelectAll: OnSelectAll; - show: boolean; - showEventsSelect: boolean; - showSelectAllCheckbox: boolean; - sort: Sort[]; - tabType: TimelineTabs; - timelineId: string; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; -} - -interface DraggableContainerProps { - children: React.ReactNode; - onMount: () => void; - onUnmount: () => void; -} - -export const DraggableContainer = React.memo( - ({ children, onMount, onUnmount }) => { - useEffect(() => { - onMount(); - - return () => onUnmount(); - }, [onMount, onUnmount]); - - return <>{children}; - } -); - -DraggableContainer.displayName = 'DraggableContainer'; - -export const isFullScreen = ({ - globalFullScreen, - isActiveTimelines, - timelineFullScreen, -}: { - globalFullScreen: boolean; - isActiveTimelines: boolean; - timelineFullScreen: boolean; -}) => - (isActiveTimelines && timelineFullScreen) || (isActiveTimelines === false && globalFullScreen); - -/** Renders the timeline header columns */ -export const ColumnHeadersComponent = ({ - actionsColumnWidth, - browserFields, - columnHeaders, - isEventViewer = false, - isSelectAllChecked, - onSelectAll, - show, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - leadingControlColumns, - trailingControlColumns, -}: ColumnHeadersComponentProps) => { - const dispatch = useDispatch(); - - const [draggingIndex, setDraggingIndex] = useState(null); - const fieldEditorActionsRef = useRef(null); - - useEffect(() => { - return () => { - if (fieldEditorActionsRef.current) { - // eslint-disable-next-line react-hooks/exhaustive-deps - fieldEditorActionsRef.current.closeEditor(); - } - }; - }, []); - - useEffect(() => { - if (!show && fieldEditorActionsRef.current) { - fieldEditorActionsRef.current.closeEditor(); - } - }, [show]); - - const renderClone: DraggableChildrenFn = useCallback( - (dragProvided, _dragSnapshot, rubric) => { - const index = rubric.source.index; - const header = columnHeaders[index]; - - const onMount = () => setDraggingIndex(index); - const onUnmount = () => setDraggingIndex(null); - - return ( - - - - - - - - ); - }, - [columnHeaders, setDraggingIndex] - ); - - const ColumnHeaderList = useMemo( - () => - columnHeaders.map((header, draggableIndex) => ( - - )), - [columnHeaders, timelineId, draggingIndex, sort, tabType] - ); - - const DroppableContent = useCallback( - (dropProvided, snapshot) => ( - <> - - {ColumnHeaderList} - - - ), - [ColumnHeaderList] - ); - - const leadingHeaderCells = useMemo( - () => - leadingControlColumns ? leadingControlColumns.map((column) => column.headerCellRender) : [], - [leadingControlColumns] - ); - - const trailingHeaderCells = useMemo( - () => - trailingControlColumns ? trailingControlColumns.map((column) => column.headerCellRender) : [], - [trailingControlColumns] - ); - - const fieldBrowserOptions = useFieldBrowserOptions({ - sourcererScope: SourcererScopeName.timeline, - editorActionsRef: fieldEditorActionsRef, - upsertColumn: (column, index) => dispatch(upsertColumn({ column, id: timelineId, index })), - removeColumn: (columnId) => dispatch(removeColumn({ columnId, id: timelineId })), - }); - - const LeadingHeaderActions = useMemo(() => { - return leadingHeaderCells.map( - (Header: React.ComponentType | React.ComponentType | undefined, index) => { - const passedWidth = leadingControlColumns[index] && leadingControlColumns[index].width; - const width = passedWidth ? passedWidth : actionsColumnWidth; - return ( - - {Header && ( -
- )} - - ); - } - ); - }, [ - leadingHeaderCells, - leadingControlColumns, - actionsColumnWidth, - browserFields, - columnHeaders, - fieldBrowserOptions, - isEventViewer, - isSelectAllChecked, - onSelectAll, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - ]); - - const TrailingHeaderActions = useMemo(() => { - return trailingHeaderCells.map( - (Header: React.ComponentType | React.ComponentType | undefined, index) => { - const passedWidth = trailingControlColumns[index] && trailingControlColumns[index].width; - const width = passedWidth ? passedWidth : actionsColumnWidth; - return ( - - {Header && ( -
- )} - - ); - } - ); - }, [ - trailingHeaderCells, - trailingControlColumns, - actionsColumnWidth, - browserFields, - columnHeaders, - fieldBrowserOptions, - isEventViewer, - isSelectAllChecked, - onSelectAll, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - ]); - return ( - - - {LeadingHeaderActions} - - {DroppableContent} - - {TrailingHeaderActions} - - - ); -}; - -export const ColumnHeaders = React.memo(ColumnHeadersComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx deleted file mode 100644 index 4d531e34fa3cd..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { RangePicker } from '.'; -import { Ranges } from './ranges'; - -describe('RangePicker', () => { - describe('rendering', () => { - test('it renders the ranges', () => { - const wrapper = mount(); - - Ranges.forEach((range) => { - expect(wrapper.text()).toContain(range); - }); - }); - - test('it selects the option specified by the "selected" prop', () => { - const selected = '1 Month'; - const wrapper = mount(); - - expect(wrapper.find('select').props().value).toBe(selected); - }); - }); - - describe('#onRangeSelected', () => { - test('it invokes the onRangeSelected callback when a new range is selected', () => { - const oldSelection = '1 Week'; - const newSelection = '1 Day'; - const mockOnRangeSelected = jest.fn(); - - const wrapper = mount( - - ); - - wrapper.find('select').simulate('change', { target: { value: newSelection } }); - - expect(mockOnRangeSelected).toBeCalledWith(newSelection); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx deleted file mode 100644 index 2eca94729aeb7..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx +++ /dev/null @@ -1,51 +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 { EuiSelect } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import type { OnRangeSelected } from '../../../events'; - -import { Ranges } from './ranges'; - -interface Props { - selected: string; - onRangeSelected: OnRangeSelected; -} - -export const rangePickerWidth = 120; - -// TODO: Upgrade Eui library and use EuiSuperSelect -const SelectContainer = styled.div` - cursor: pointer; - width: ${rangePickerWidth}px; -`; - -SelectContainer.displayName = 'SelectContainer'; - -/** Renders a time range picker for the MiniMap (e.g. 1 Day, 1 Week...) */ -export const RangePicker = React.memo(({ selected, onRangeSelected }) => { - const onChange = (event: React.ChangeEvent): void => { - onRangeSelected(event.target.value); - }; - - return ( - - ({ - text: range, - }))} - onChange={onChange} - /> - - ); -}); - -RangePicker.displayName = 'RangePicker'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts deleted file mode 100644 index a174207b2b057..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts +++ /dev/null @@ -1,11 +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 * as i18n from './translations'; - -/** Enables runtime enumeration of valid `Range`s */ -export const Ranges: string[] = [i18n.ONE_DAY, i18n.ONE_WEEK, i18n.ONE_MONTH, i18n.ONE_YEAR]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/translations.ts deleted file mode 100644 index 339c56f92490a..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/translations.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ONE_DAY = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneDay', { - defaultMessage: '1 Day', -}); - -export const ONE_WEEK = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneWeek', { - defaultMessage: '1 Week', -}); - -export const ONE_MONTH = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneMonth', { - defaultMessage: '1 Month', -}); - -export const ONE_YEAR = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneYear', { - defaultMessage: '1 Year', -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap deleted file mode 100644 index fc4bd7bbd6148..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TextFilter rendering renders correctly against snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx deleted file mode 100644 index aae979c19902e..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_PLACEHOLDER, TextFilter } from '.'; - -describe('TextFilter', () => { - describe('rendering', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - describe('placeholder', () => { - test('it renders the default placeholder when no filter is specified, and a placeholder is NOT provided', () => { - const wrapper = mount(); - - expect(wrapper.find(`input[placeholder="${DEFAULT_PLACEHOLDER}"]`).exists()).toEqual(true); - }); - - test('it renders the default placeholder when no filter is specified, a placeholder is provided', () => { - const placeholder = 'Make a jazz noise here'; - const wrapper = mount( - - ); - - expect(wrapper.find(`input[placeholder="${placeholder}"]`).exists()).toEqual(true); - }); - }); - - describe('minWidth', () => { - test('it applies the value of the minwidth prop to the input', () => { - const minWidth = 150; - const wrapper = mount(); - - expect(wrapper.find('input').props()).toHaveProperty('minwidth', `${minWidth}px`); - }); - }); - - describe('value', () => { - test('it renders the value of the filter prop', () => { - const filter = 'out the noise'; - const wrapper = mount(); - - expect(wrapper.find('input').prop('value')).toEqual(filter); - }); - }); - - describe('#onFilterChange', () => { - test('it invokes the onFilterChange callback when the input is updated', () => { - const columnId = 'foo'; - const oldFilter = 'abcdef'; - const newFilter = `${oldFilter}g`; - const onFilterChange = jest.fn(); - - const wrapper = mount( - - ); - - wrapper.find('input').simulate('change', { target: { value: newFilter } }); - expect(onFilterChange).toBeCalledWith({ - columnId, - filter: newFilter, - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx deleted file mode 100644 index d22e2ca40ca40..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFieldText } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import type { OnFilterChange } from '../../../events'; -import type { ColumnId } from '../../column_id'; - -interface Props { - columnId: ColumnId; - filter?: string; - minWidth: number; - onFilterChange?: OnFilterChange; - placeholder?: string; -} - -export const DEFAULT_PLACEHOLDER = 'Filter'; - -const FieldText = styled(EuiFieldText)<{ minwidth: string }>` - min-width: ${(props) => props.minwidth}; -`; - -FieldText.displayName = 'FieldText'; - -/** Renders a text-based column filter */ -export const TextFilter = React.memo( - ({ - columnId, - minWidth, - filter = '', - onFilterChange = noop, - placeholder = DEFAULT_PLACEHOLDER, - }) => { - const onChange = (event: React.ChangeEvent): void => { - onFilterChange({ columnId, filter: event.target.value }); - }; - - return ( - - ); - } -); - -TextFilter.displayName = 'TextFilter'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap deleted file mode 100644 index fd4566ca440e8..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,1037 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Columns it renders the expected columns 1`] = ` - - - - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx deleted file mode 100644 index 67f3c35a3ec13..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ /dev/null @@ -1,93 +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 { shallow } from 'enzyme'; - -import React from 'react'; - -import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; -import { mockTimelineData } from '../../../../../common/mock'; -import { defaultHeaders } from '../column_headers/default_headers'; -import { getDefaultControlColumn } from '../control_columns'; - -import { DataDrivenColumns, getMappedNonEcsValue } from '.'; - -describe('Columns', () => { - const headersSansTimestamp = defaultHeaders.filter((h) => h.id !== '@timestamp'); - const ACTION_BUTTON_COUNT = 4; - const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT); - - test('it renders the expected columns', () => { - const wrapper = shallow( - - ); - - expect(wrapper).toMatchSnapshot(); - }); - - describe('getMappedNonEcsValue', () => { - const existingField = 'Descarte'; - const existingValue = ['IThinkThereforeIAm']; - - test('should return the value if the fieldName is found', () => { - const result = getMappedNonEcsValue({ - data: [{ field: existingField, value: existingValue }], - fieldName: existingField, - }); - - expect(result).toBe(existingValue); - }); - - test('should return undefined if the value cannot be found in the array', () => { - const result = getMappedNonEcsValue({ - data: [{ field: existingField, value: existingValue }], - fieldName: 'nonExistent', - }); - - expect(result).toBeUndefined(); - }); - - test('should return undefined when data is an empty array', () => { - const result = getMappedNonEcsValue({ data: [], fieldName: existingField }); - - expect(result).toBeUndefined(); - }); - - test('should return undefined when data is undefined', () => { - const result = getMappedNonEcsValue({ data: undefined, fieldName: existingField }); - - expect(result).toBeUndefined(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx deleted file mode 100644 index a1d65e69dba58..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ /dev/null @@ -1,472 +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 { EuiScreenReaderOnly } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { getOr } from 'lodash/fp'; -import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; - -import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import type { - SetEventsDeleted, - SetEventsLoading, - ActionProps, - ControlColumnProps, - RowCellRender, -} from '../../../../../../common/types'; -import type { - CellValueElementProps, - ColumnHeaderOptions, - TimelineTabs, -} from '../../../../../../common/types/timeline'; -import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; -import type { OnRowSelected } from '../../events'; -import type { inputsModel } from '../../../../../common/store'; -import { - EventsTd, - EVENTS_TD_CLASS_NAME, - EventsTdContent, - EventsTdGroupData, - EventsTdGroupActions, -} from '../../styles'; - -import { StatefulCell } from './stateful_cell'; -import * as i18n from './translations'; - -interface CellProps { - _id: string; - ariaRowindex: number; - index: number; - header: ColumnHeaderOptions; - data: TimelineNonEcsData[]; - ecsData: Ecs; - hasRowRenderers: boolean; - notesCount: number; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - tabType?: TimelineTabs; - timelineId: string; -} - -interface DataDrivenColumnProps { - id: string; - actionsColumnWidth: number; - ariaRowindex: number; - checked: boolean; - columnHeaders: ColumnHeaderOptions[]; - columnValues: string; - data: TimelineNonEcsData[]; - ecsData: Ecs; - eventIdToNoteIds: Readonly>; - isEventPinned: boolean; - isEventViewer?: boolean; - loadingEventIds: Readonly; - notesCount: number; - onEventDetailsPanelOpened: () => void; - onRowSelected: OnRowSelected; - refetch: inputsModel.Refetch; - onRuleChange?: () => void; - hasRowRenderers: boolean; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - showNotes: boolean; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - tabType?: TimelineTabs; - timelineId: string; - toggleShowNotes: () => void; - trailingControlColumns: ControlColumnProps[]; - leadingControlColumns: ControlColumnProps[]; - setEventsLoading: SetEventsLoading; - setEventsDeleted: SetEventsDeleted; -} - -const SPACE = ' '; - -export const shouldForwardKeyDownEvent = (key: string): boolean => { - switch (key) { - case SPACE: // fall through - case 'Enter': - return true; - default: - return false; - } -}; - -export const onKeyDown = (keyboardEvent: React.KeyboardEvent) => { - const { altKey, ctrlKey, key, metaKey, shiftKey, target, type } = keyboardEvent; - - const targetElement = target as Element; - - // we *only* forward the event to the (child) draggable keyboard wrapper - // if the keyboard event originated from the container (TD) element - if (shouldForwardKeyDownEvent(key) && targetElement.className?.includes(EVENTS_TD_CLASS_NAME)) { - const draggableKeyboardWrapper = targetElement.querySelector( - `.${DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}` - ); - - const newEvent = new KeyboardEvent(type, { - altKey, - bubbles: true, - cancelable: true, - ctrlKey, - key, - metaKey, - shiftKey, - }); - - if (key === ' ') { - // prevent the default behavior of scrolling the table when space is pressed - keyboardEvent.preventDefault(); - } - - draggableKeyboardWrapper?.dispatchEvent(newEvent); - } -}; - -const TgridActionTdCell = ({ - action: Action, - width, - actionsColumnWidth, - ariaRowindex, - columnId, - columnValues, - data, - ecsData, - eventIdToNoteIds, - index, - isEventPinned, - isEventViewer, - eventId, - loadingEventIds, - notesCount, - onEventDetailsPanelOpened, - onRowSelected, - refetch, - rowIndex, - hasRowRenderers, - onRuleChange, - selectedEventIds, - showCheckboxes, - showNotes, - tabType, - timelineId, - toggleShowNotes, - setEventsLoading, - setEventsDeleted, -}: ActionProps & { - columnId: string; - hasRowRenderers: boolean; - actionsColumnWidth: number; - notesCount: number; - selectedEventIds: Readonly>; -}) => { - const displayWidth = width ? width : actionsColumnWidth; - return ( - - - - <> - -

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: index + 2 })}

-
- {Action && ( - - )} - -
- {hasRowRenderers ? ( - -

{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}

-
- ) : null} - - {notesCount ? ( - -

{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}

-
- ) : null} -
-
- ); -}; - -const TgridTdCell = ({ - _id, - ariaRowindex, - index, - header, - data, - ecsData, - hasRowRenderers, - notesCount, - renderCellValue, - tabType, - timelineId, -}: CellProps) => { - const ariaColIndex = index + ARIA_COLUMN_INDEX_OFFSET; - return ( - - - <> - -

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: ariaColIndex })}

-
- - -
- {hasRowRenderers ? ( - -

{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}

-
- ) : null} - - {notesCount ? ( - -

{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}

-
- ) : null} -
- ); -}; - -export const DataDrivenColumns = React.memo( - ({ - ariaRowindex, - actionsColumnWidth, - columnHeaders, - columnValues, - data, - ecsData, - eventIdToNoteIds, - isEventPinned, - isEventViewer, - id: _id, - loadingEventIds, - notesCount, - onEventDetailsPanelOpened, - onRowSelected, - refetch, - hasRowRenderers, - onRuleChange, - renderCellValue, - selectedEventIds, - showCheckboxes, - showNotes, - tabType, - timelineId, - toggleShowNotes, - trailingControlColumns, - leadingControlColumns, - setEventsLoading, - setEventsDeleted, - }) => { - const trailingActionCells = useMemo( - () => - trailingControlColumns ? trailingControlColumns.map((column) => column.rowCellRender) : [], - [trailingControlColumns] - ); - const leadingAndDataColumnCount = useMemo( - () => leadingControlColumns.length + columnHeaders.length, - [leadingControlColumns, columnHeaders] - ); - const TrailingActions = useMemo( - () => - trailingActionCells.map((Action: RowCellRender | undefined, index) => { - return ( - Action && ( - - ) - ); - }), - [ - trailingControlColumns, - _id, - data, - ecsData, - onRowSelected, - isEventPinned, - isEventViewer, - actionsColumnWidth, - ariaRowindex, - columnValues, - eventIdToNoteIds, - hasRowRenderers, - leadingAndDataColumnCount, - loadingEventIds, - notesCount, - onEventDetailsPanelOpened, - onRuleChange, - refetch, - selectedEventIds, - showCheckboxes, - showNotes, - tabType, - timelineId, - toggleShowNotes, - trailingActionCells, - setEventsLoading, - setEventsDeleted, - ] - ); - const ColumnHeaders = useMemo( - () => - columnHeaders.map((header, index) => ( - - )), - [ - _id, - ariaRowindex, - columnHeaders, - data, - ecsData, - hasRowRenderers, - notesCount, - renderCellValue, - tabType, - timelineId, - ] - ); - return ( - - {ColumnHeaders} - {TrailingActions} - - ); - } -); - -DataDrivenColumns.displayName = 'DataDrivenColumns'; - -export const getMappedNonEcsValue = ({ - data, - fieldName, -}: { - data?: TimelineNonEcsData[]; - fieldName: string; -}): string[] | undefined => { - /* - While data _should_ always be defined - There is the potential for race conditions where a component using this function - is still visible in the UI, while the data has since been removed. - To cover all scenarios where this happens we'll check for the presence of data here - */ - if (!data || data.length === 0) return undefined; - const item = data.find((d) => d.field === fieldName); - if (item != null && item.value != null) { - return item.value; - } - return undefined; -}; - -export const useGetMappedNonEcsValue = ({ - data, - fieldName, -}: { - data?: TimelineNonEcsData[]; - fieldName: string; -}): string[] | undefined => { - return useMemo(() => getMappedNonEcsValue({ data, fieldName }), [data, fieldName]); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx deleted file mode 100644 index 32ca51ca62f78..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx +++ /dev/null @@ -1,171 +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 { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React, { useEffect } from 'react'; - -import { defaultHeaders, mockTimelineData } from '../../../../../common/mock'; -import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import type { - ColumnHeaderOptions, - CellValueElementProps, -} from '../../../../../../common/types/timeline'; -import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; - -import { StatefulCell } from './stateful_cell'; -import { useGetMappedNonEcsValue } from '.'; - -/** - * This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface, - * as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid - * - * Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`. - * The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface, - * is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example: - * https://codesandbox.io/s/zhxmo - */ -const RenderCellValue: React.FC = ({ columnId, data, setCellProps }) => { - const value = useGetMappedNonEcsValue({ - data, - fieldName: columnId, - }); - useEffect(() => { - // branching logic that conditionally renders a specific cell green: - if (columnId === defaultHeaders[0].id) { - if (value?.length) { - setCellProps({ - style: { - backgroundColor: 'green', - }, - }); - } - } - }, [columnId, data, setCellProps, value]); - - return
{value}
; -}; - -describe('StatefulCell', () => { - const rowIndex = 123; - const colIndex = 0; - const eventId = '_id-123'; - const linkValues = ['foo', 'bar', '@baz']; - const timelineId = TimelineId.test; - - let header: ColumnHeaderOptions; - let data: TimelineNonEcsData[]; - beforeEach(() => { - data = cloneDeep(mockTimelineData[0].data); - header = cloneDeep(defaultHeaders[0]); - }); - - test('it invokes renderCellValue with the expected arguments when tabType is specified', () => { - const renderCellValue = jest.fn(); - - mount( - - ); - - expect(renderCellValue).toBeCalledWith( - expect.objectContaining({ - columnId: header.id, - eventId, - data, - header, - isExpandable: true, - isExpanded: false, - isDetails: false, - linkValues, - rowIndex, - colIndex, - scopeId: timelineId, - }) - ); - }); - - test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => { - const renderCellValue = jest.fn(); - - mount( - - ); - - expect(renderCellValue).toBeCalledWith( - expect.objectContaining({ - columnId: header.id, - eventId, - data, - header, - isExpandable: true, - isExpanded: false, - isDetails: false, - linkValues, - rowIndex, - colIndex, - scopeId: timelineId, - }) - ); - }); - - test('it renders the React.Node returned by renderCellValue', () => { - const renderCellValue = () =>
; - - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true); - }); - - test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => { - const wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style') - ).toEqual('background-color: green;'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx deleted file mode 100644 index 941f6499fe854..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HTMLAttributes } from 'react'; -import React, { useState } from 'react'; - -import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import type { - ColumnHeaderOptions, - CellValueElementProps, - TimelineTabs, -} from '../../../../../../common/types/timeline'; - -export interface CommonProps { - className?: string; - 'aria-label'?: string; - 'data-test-subj'?: string; -} - -const StatefulCellComponent = ({ - rowIndex, - colIndex, - data, - header, - eventId, - linkValues, - renderCellValue, - tabType, - timelineId, -}: { - rowIndex: number; - colIndex: number; - data: TimelineNonEcsData[]; - header: ColumnHeaderOptions; - eventId: string; - linkValues: string[] | undefined; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - tabType?: TimelineTabs; - timelineId: string; -}) => { - const [cellProps, setCellProps] = useState>({}); - return ( -
- {renderCellValue({ - columnId: header.id, - eventId, - data, - header, - isDraggable: true, - isExpandable: true, - isExpanded: false, - isDetails: false, - isTimeline: true, - linkValues, - rowIndex, - colIndex, - setCellProps, - scopeId: timelineId, - key: tabType != null ? `${timelineId}-${tabType}` : timelineId, - })} -
- ); -}; - -StatefulCellComponent.displayName = 'StatefulCellComponent'; - -export const StatefulCell = React.memo(StatefulCellComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts deleted file mode 100644 index 18bd7a600f7cd..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const YOU_ARE_IN_A_TABLE_CELL = ({ column, row }: { column: number; row: number }) => - i18n.translate('xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly', { - values: { column, row }, - defaultMessage: 'You are in a table cell. row: {row}, column: {column}', - }); - -export const EVENT_HAS_AN_EVENT_RENDERER = (row: number) => - i18n.translate('xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly', { - values: { row }, - defaultMessage: - 'The event in row {row} has an event renderer. Press shift + down arrow to focus it.', - }); - -export const EVENT_HAS_NOTES = ({ notesCount, row }: { notesCount: number; row: number }) => - i18n.translate('xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly', { - values: { notesCount, row }, - defaultMessage: - 'The event in row {row} has {notesCount, plural, =1 {a note} other {{notesCount} notes}}. Press shift + right arrow to focus notes.', - }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx deleted file mode 100644 index 8b49c12eaf73c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount, type ComponentType as EnzymeComponentType } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../../../common/mock'; - -import { EventColumnView } from './event_column_view'; -import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; -import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; -import { TimelineTypeEnum } from '../../../../../../common/api/timeline'; -import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { getDefaultControlColumn } from '../control_columns'; -import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeline_control_columns'; -import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; -import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; -import { getActionsColumnWidth } from '../../../../../common/components/header_actions'; - -jest.mock('../../../../../common/components/header_actions/add_note_icon_item', () => { - return { - AddEventNoteAction: jest.fn(() =>
), - }; -}); - -jest.mock('../../../../../common/hooks/use_experimental_features'); -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; -jest.mock('../../../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn(), - useDeepEqualSelector: jest.fn(), -})); -jest.mock('../../../../../common/components/user_privileges', () => { - return { - useUserPrivileges: () => ({ - listPrivileges: { loading: false, error: undefined, result: undefined }, - detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, - endpointPrivileges: {}, - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, - }), - }; -}); -jest.mock('../../../../../common/components/guided_onboarding_tour/tour_step'); -jest.mock('../../../../../common/lib/kibana', () => { - const originalModule = jest.requireActual('../../../../../common/lib/kibana'); - - return { - ...originalModule, - useKibana: () => ({ - services: { - timelines: { ...mockTimelines }, - data: { - search: jest.fn(), - query: jest.fn(), - }, - application: { - capabilities: { - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - cases: mockCasesContract(), - }, - }), - useNavigateTo: () => ({ - navigateTo: jest.fn(), - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - remove: jest.fn(), - }), - }; -}); - -describe('EventColumnView', () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - (useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineTypeEnum.default); - const ACTION_BUTTON_COUNT = 4; - const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT); - - const props = { - ariaRowindex: 2, - id: 'event-id', - actionsColumnWidth: getActionsColumnWidth(ACTION_BUTTON_COUNT), - associateNote: jest.fn(), - columnHeaders: [], - columnRenderers: [], - data: [ - { - field: 'host.name', - }, - ], - ecsData: { - _id: 'id', - }, - eventIdToNoteIds: {}, - expanded: false, - hasRowRenderers: false, - loading: false, - loadingEventIds: [], - notesCount: 0, - onEventDetailsPanelOpened: jest.fn(), - onRowSelected: jest.fn(), - refetch: jest.fn(), - renderCellValue: DefaultCellRenderer, - selectedEventIds: {}, - showCheckboxes: false, - showNotes: false, - tabType: TimelineTabs.query, - timelineId: TimelineId.active, - toggleShowNotes: jest.fn(), - updateNote: jest.fn(), - isEventPinned: false, - leadingControlColumns, - trailingControlColumns: [], - setEventsLoading: jest.fn(), - setEventsDeleted: jest.fn(), - }; - - test('it does NOT render a notes button when isEventsViewer is true', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - }); - - expect(wrapper.find('[data-test-subj="add-note-button-mock"]').exists()).toBe(false); - }); - - test('it does NOT render a notes button when showNotes is false', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - }); - - expect(wrapper.find('[data-test-subj="add-note-button-mock"]').exists()).toBe(false); - }); - - test('it does NOT render a pin button when isEventViewer is true', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - }); - - expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); - }); - - test('it renders a custom control column in addition to the default control column', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - } - ); - - expect(wrapper.find('[data-test-subj="expand-event"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="test-body-control-column-cell"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx deleted file mode 100644 index e184e27d428ef..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ /dev/null @@ -1,225 +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 } from 'react'; - -import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import type { - ControlColumnProps, - RowCellRender, - SetEventsDeleted, - SetEventsLoading, -} from '../../../../../../common/types'; -import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import type { OnRowSelected } from '../../events'; -import { EventsTrData, EventsTdGroupActions } from '../../styles'; -import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns'; -import type { inputsModel } from '../../../../../common/store'; -import type { - ColumnHeaderOptions, - CellValueElementProps, - TimelineTabs, -} from '../../../../../../common/types/timeline'; - -interface Props { - id: string; - actionsColumnWidth: number; - ariaRowindex: number; - columnHeaders: ColumnHeaderOptions[]; - data: TimelineNonEcsData[]; - ecsData: Ecs; - eventIdToNoteIds: Readonly>; - isEventPinned: boolean; - isEventViewer?: boolean; - loadingEventIds: Readonly; - notesCount: number; - onEventDetailsPanelOpened: () => void; - onRowSelected: OnRowSelected; - refetch: inputsModel.Refetch; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - onRuleChange?: () => void; - hasRowRenderers: boolean; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - showNotes: boolean; - tabType?: TimelineTabs; - timelineId: string; - toggleShowNotes: (eventId?: string) => void; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; - setEventsLoading: SetEventsLoading; - setEventsDeleted: SetEventsDeleted; -} - -export const EventColumnView = React.memo( - ({ - id, - actionsColumnWidth, - ariaRowindex, - columnHeaders, - data, - ecsData, - eventIdToNoteIds, - isEventPinned = false, - isEventViewer = false, - loadingEventIds, - notesCount, - onEventDetailsPanelOpened, - onRowSelected, - refetch, - hasRowRenderers, - onRuleChange, - renderCellValue, - selectedEventIds, - showCheckboxes, - showNotes, - tabType, - timelineId, - toggleShowNotes, - leadingControlColumns, - trailingControlColumns, - setEventsLoading, - setEventsDeleted, - }) => { - // Each action button shall announce itself to screen readers via an `aria-label` - // in the following format: - // "button description, for the event in row {ariaRowindex}, with columns {columnValues}", - // so we combine the column values here: - const columnValues = useMemo( - () => - columnHeaders - .map( - (header) => - getMappedNonEcsValue({ - data, - fieldName: header.id, - }) ?? [] - ) - .join(' '), - [columnHeaders, data] - ); - - const leadingActionCells = useMemo( - () => - leadingControlColumns ? leadingControlColumns.map((column) => column.rowCellRender) : [], - [leadingControlColumns] - ); - const LeadingActions = useMemo( - () => - leadingActionCells.map((Action: RowCellRender | undefined, index) => { - const width = leadingControlColumns[index].width - ? leadingControlColumns[index].width - : actionsColumnWidth; - return ( - - {Action && ( - - )} - - ); - }), - [ - actionsColumnWidth, - ariaRowindex, - columnValues, - data, - ecsData, - eventIdToNoteIds, - id, - isEventPinned, - isEventViewer, - leadingActionCells, - leadingControlColumns, - loadingEventIds, - onEventDetailsPanelOpened, - onRowSelected, - onRuleChange, - refetch, - selectedEventIds, - showCheckboxes, - tabType, - timelineId, - toggleShowNotes, - setEventsLoading, - setEventsDeleted, - showNotes, - ] - ); - return ( - - {LeadingActions} - - - ); - } -); - -EventColumnView.displayName = 'EventColumnView'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx deleted file mode 100644 index 76c28f24b14d6..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { isEmpty } from 'lodash'; - -import type { ControlColumnProps } from '../../../../../../common/types'; -import type { inputsModel } from '../../../../../common/store'; -import type { - TimelineItem, - TimelineNonEcsData, -} from '../../../../../../common/search_strategy/timeline'; -import type { - ColumnHeaderOptions, - CellValueElementProps, - RowRenderer, - TimelineTabs, -} from '../../../../../../common/types/timeline'; -import type { OnRowSelected } from '../../events'; -import { EventsTbody } from '../../styles'; -import { StatefulEvent } from './stateful_event'; -import { eventIsPinned } from '../helpers'; - -/** This offset begins at two, because the header row counts as "row 1", and aria-rowindex starts at "1" */ -const ARIA_ROW_INDEX_OFFSET = 2; - -interface Props { - actionsColumnWidth: number; - columnHeaders: ColumnHeaderOptions[]; - containerRef: React.MutableRefObject; - data: TimelineItem[]; - eventIdToNoteIds: Readonly>; - id: string; - isEventViewer?: boolean; - lastFocusedAriaColindex: number; - loadingEventIds: Readonly; - onRowSelected: OnRowSelected; - pinnedEventIds: Readonly>; - refetch: inputsModel.Refetch; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - onRuleChange?: () => void; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - tabType?: TimelineTabs; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; - onToggleShowNotes?: (eventId?: string) => void; -} - -const EventsComponent: React.FC = ({ - actionsColumnWidth, - columnHeaders, - containerRef, - data, - eventIdToNoteIds, - id, - isEventViewer = false, - lastFocusedAriaColindex, - loadingEventIds, - onRowSelected, - pinnedEventIds, - refetch, - onRuleChange, - renderCellValue, - rowRenderers, - selectedEventIds, - showCheckboxes, - tabType, - leadingControlColumns, - trailingControlColumns, - onToggleShowNotes, -}) => ( - - {data.map((event, i) => ( - - ))} - -); - -export const Events = React.memo(EventsComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx deleted file mode 100644 index 05f7a9d8b8e2b..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { PropsWithChildren } from 'react'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { isEventBuildingBlockType } from '@kbn/securitysolution-data-table'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys'; -import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; -import { useKibana } from '../../../../../common/lib/kibana'; -import type { - ColumnHeaderOptions, - CellValueElementProps, - RowRenderer, - TimelineTabs, -} from '../../../../../../common/types/timeline'; -import type { - TimelineItem, - TimelineNonEcsData, -} from '../../../../../../common/search_strategy/timeline'; -import type { OnRowSelected } from '../../events'; -import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; -import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; -import { getEventType, isEvenEqlSequence } from '../helpers'; -import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; -import { EventColumnView } from './event_column_view'; -import type { inputsModel } from '../../../../../common/store'; -import { appSelectors } from '../../../../../common/store'; -import { timelineActions } from '../../../../store'; -import type { TimelineResultNote } from '../../../open_timeline/types'; -import { getRowRenderer } from '../renderers/get_row_renderer'; -import { StatefulRowRenderer } from './stateful_row_renderer'; -import { NOTES_BUTTON_CLASS_NAME } from '../../properties/helpers'; -import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; -import type { - ControlColumnProps, - SetEventsDeleted, - SetEventsLoading, -} from '../../../../../../common/types'; - -interface Props { - actionsColumnWidth: number; - containerRef: React.MutableRefObject; - columnHeaders: ColumnHeaderOptions[]; - event: TimelineItem; - eventIdToNoteIds: Readonly>; - isEventViewer?: boolean; - lastFocusedAriaColindex: number; - loadingEventIds: Readonly; - onRowSelected: OnRowSelected; - isEventPinned: boolean; - refetch: inputsModel.Refetch; - ariaRowindex: number; - onRuleChange?: () => void; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - tabType?: TimelineTabs; - timelineId: string; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; - onToggleShowNotes?: (eventId?: string) => void; -} - -const emptyNotes: string[] = []; - -const EventsTrSupplementContainerWrapper = React.memo>( - ({ children }) => { - const width = useEventDetailsWidthContext(); - return {children}; - } -); - -EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWrapper'; - -const StatefulEventComponent: React.FC = ({ - actionsColumnWidth, - containerRef, - columnHeaders, - event, - eventIdToNoteIds, - isEventViewer = false, - isEventPinned = false, - lastFocusedAriaColindex, - loadingEventIds, - onRowSelected, - refetch, - renderCellValue, - rowRenderers, - onRuleChange, - ariaRowindex, - selectedEventIds, - showCheckboxes, - tabType, - timelineId, - leadingControlColumns, - trailingControlColumns, - onToggleShowNotes, -}) => { - const { telemetry } = useKibana().services; - const trGroupRef = useRef(null); - const dispatch = useDispatch(); - - const { openFlyout } = useExpandableFlyoutApi(); - - // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created - const [activeStatefulEventContext] = useState({ - timelineID: timelineId, - enableHostDetailsFlyout: true, - enableIpDetailsFlyout: true, - tabType, - }); - - const [, setFocusedNotes] = useState<{ [eventId: string]: boolean }>({}); - - const eventId = event._id; - - const isDetailPanelExpanded: boolean = false; - - const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); - const notesById = useDeepEqualSelector(getNotesByIds); - const noteIds: string[] = eventIdToNoteIds[eventId] || emptyNotes; - - const notes: TimelineResultNote[] = useMemo( - () => - appSelectors.getNotes(notesById, noteIds).map((note) => ({ - savedObjectId: note.saveObjectId, - note: note.note, - noteId: note.id, - updated: (note.lastEdit ?? note.created).getTime(), - updatedBy: note.user, - })), - [notesById, noteIds] - ); - - const hasRowRenderers: boolean = useMemo( - () => getRowRenderer({ data: event.ecs, rowRenderers }) != null, - [event.ecs, rowRenderers] - ); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const indexName = event._index!; - - const onToggleShowNotesHandler = useCallback( - (currentEventId?: string) => { - onToggleShowNotes?.(currentEventId); - setFocusedNotes((prevShowNotes) => { - if (prevShowNotes[eventId]) { - // notes are closing, so focus the notes button on the next tick, after escaping the EuiFocusTrap - setTimeout(() => { - const notesButtonElement = trGroupRef.current?.querySelector( - `.${NOTES_BUTTON_CLASS_NAME}` - ); - notesButtonElement?.focus(); - }, 0); - } - - return { ...prevShowNotes, [eventId]: !prevShowNotes[eventId] }; - }); - }, - [onToggleShowNotes, eventId] - ); - - const handleOnEventDetailPanelOpened = useCallback(() => { - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: eventId, - indexName, - scopeId: timelineId, - }, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: timelineId, - panel: 'right', - }); - }, [eventId, indexName, openFlyout, timelineId, telemetry]); - - const setEventsLoading = useCallback( - ({ eventIds, isLoading }) => { - dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading })); - }, - [dispatch, timelineId] - ); - - const setEventsDeleted = useCallback( - ({ eventIds, isDeleted }) => { - dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted })); - }, - [dispatch, timelineId] - ); - - return ( - - - - - - - - - - - - - - - - ); -}; - -export const StatefulEvent = React.memo(StatefulEventComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx deleted file mode 100644 index 0c365ae42798d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount, type ComponentType as EnzymeComponentType } from 'enzyme'; -import { waitFor } from '@testing-library/react'; - -import { useKibana, useCurrentUser } from '../../../../common/lib/kibana'; -import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; -import { mockBrowserFields } from '../../../../common/containers/source/mock'; -import { Direction } from '../../../../../common/search_strategy'; -import { - defaultHeaders, - mockGlobalState, - mockTimelineData, - createMockStore, - TestProviders, -} from '../../../../common/mock'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; - -import type { Props } from '.'; -import { StatefulBody } from '.'; -import type { Sort } from './sort'; -import { getDefaultControlColumn } from './control_columns'; -import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; -import { defaultRowRenderers } from './renderers'; -import type { State } from '../../../../common/store'; -import type { UseFieldBrowserOptionsProps } from '../../fields_browser'; -import type { - DraggableProvided, - DraggableStateSnapshot, - DroppableProvided, - DroppableStateSnapshot, -} from '@hello-pangea/dnd'; -import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys'; -import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; -import { createExpandableFlyoutApiMock } from '../../../../common/mock/expandable_flyout'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; - -jest.mock('../../../../common/hooks/use_app_toasts'); -jest.mock('../../../../common/components/guided_onboarding_tour/tour_step'); -jest.mock( - '../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions' -); - -jest.mock('../../../../common/hooks/use_upselling', () => ({ - useUpsellingMessage: jest.fn(), -})); - -jest.mock('../../../../common/components/user_privileges', () => { - return { - useUserPrivileges: () => ({ - listPrivileges: { loading: false, error: undefined, result: undefined }, - detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, - endpointPrivileges: {}, - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, - }), - }; -}); - -const mockUseFieldBrowserOptions = jest.fn(); -const mockUseKibana = useKibana as jest.Mock; -const mockUseCurrentUser = useCurrentUser as jest.Mock>>; -const mockCasesContract = jest.requireActual('@kbn/cases-plugin/public/mocks'); -jest.mock('../../fields_browser', () => ({ - useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), -})); - -const useAddToTimeline = () => ({ - beginDrag: jest.fn(), - cancelDrag: jest.fn(), - dragToLocation: jest.fn(), - endDrag: jest.fn(), - hasDraggableLock: jest.fn(), - startDragToTimeline: jest.fn(), -}); - -jest.mock('../../../../common/lib/kibana'); -const mockSort: Sort[] = [ - { - columnId: '@timestamp', - columnType: 'date', - esTypes: ['date'], - sortDirection: Direction.desc, - }, -]; - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); - -const mockOpenFlyout = jest.fn(); -jest.mock('@kbn/expandable-flyout'); - -const mockedTelemetry = createTelemetryServiceMock(); - -jest.mock('../../../../common/components/link_to', () => { - const originalModule = jest.requireActual('../../../../common/components/link_to'); - return { - ...originalModule, - useGetSecuritySolutionUrl: () => - jest.fn(({ deepLinkId }: { deepLinkId: string }) => `/${deepLinkId}`), - useNavigateTo: () => { - return { navigateTo: jest.fn() }; - }, - useAppUrl: () => { - return { getAppUrl: jest.fn() }; - }, - }; -}); - -jest.mock('../../../../common/components/links', () => { - const originalModule = jest.requireActual('../../../../common/components/links'); - return { - ...originalModule, - useGetSecuritySolutionUrl: () => - jest.fn(({ deepLinkId }: { deepLinkId: string }) => `/${deepLinkId}`), - useNavigateTo: () => { - return { navigateTo: jest.fn() }; - }, - useAppUrl: () => { - return { getAppUrl: jest.fn() }; - }, - }; -}); - -// Prevent Resolver from rendering -jest.mock('../../graph_overlay'); - -jest.mock('../../fields_browser/create_field_button', () => ({ - useCreateFieldButton: () => <>, -})); - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - EuiScreenReaderOnly: () => <>, - }; -}); -jest.mock('suricata-sid-db', () => { - return { - db: [], - }; -}); -jest.mock( - '../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions', - () => { - return { - useAddToCaseActions: () => { - return { - addToCaseActionItems: [], - }; - }, - }; - } -); - -jest.mock('@hello-pangea/dnd', () => ({ - Droppable: ({ - children, - }: { - children: (a: DroppableProvided, b: DroppableStateSnapshot) => void; - }) => - children( - { - droppableProps: { - 'data-rfd-droppable-context-id': '123', - 'data-rfd-droppable-id': '123', - }, - innerRef: jest.fn(), - placeholder: null, - }, - { - isDraggingOver: false, - draggingOverWith: null, - draggingFromThisWith: null, - isUsingPlaceholder: false, - } - ), - Draggable: ({ - children, - }: { - children: (a: DraggableProvided, b: DraggableStateSnapshot) => void; - }) => - children( - { - draggableProps: { - 'data-rfd-draggable-context-id': '123', - 'data-rfd-draggable-id': '123', - }, - innerRef: jest.fn(), - dragHandleProps: null, - }, - { - isDragging: false, - isDropAnimating: false, - isClone: false, - dropAnimation: null, - draggingOver: null, - combineWith: null, - combineTargetFor: null, - mode: null, - } - ), - DragDropContext: ({ children }: { children: React.ReactNode }) => children, -})); - -describe('Body', () => { - const getWrapper = async ( - childrenComponent: JSX.Element, - store?: { store: ReturnType } - ) => { - const wrapper = mount(childrenComponent, { - wrappingComponent: TestProviders as EnzymeComponentType<{}>, - wrappingComponentProps: store ?? {}, - }); - await waitFor(() => wrapper.find('[data-test-subj="suricataRefs"]').exists()); - - return wrapper; - }; - const mockRefetch = jest.fn(); - let appToastsMock: jest.Mocked>; - - beforeEach(() => { - jest.mocked(useExpandableFlyoutApi).mockReturnValue({ - ...createExpandableFlyoutApiMock(), - openFlyout: mockOpenFlyout, - }); - - mockUseCurrentUser.mockReturnValue({ username: 'test-username' }); - mockUseKibana.mockReturnValue({ - services: { - application: { - navigateToApp: jest.fn(), - getUrlForApp: jest.fn(), - capabilities: { - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - cases: mockCasesContract.mockCasesContract(), - data: { - search: jest.fn(), - query: jest.fn(), - dataViews: jest.fn(), - }, - uiSettings: { - get: jest.fn(), - }, - savedObjects: { - client: {}, - }, - telemetry: mockedTelemetry, - timelines: { - getLastUpdated: jest.fn(), - getLoadingPanel: jest.fn(), - getFieldBrowser: jest.fn(), - getUseAddToTimeline: () => useAddToTimeline, - }, - }, - useNavigateTo: jest.fn().mockReturnValue({ - navigateTo: jest.fn(), - }), - }); - appToastsMock = useAppToastsMock.create(); - (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); - }); - - const ACTION_BUTTON_COUNT = 4; - - const props: Props = { - activePage: 0, - browserFields: mockBrowserFields, - data: [mockTimelineData[0]], - id: TimelineId.test, - refetch: mockRefetch, - renderCellValue: DefaultCellRenderer, - rowRenderers: defaultRowRenderers, - sort: mockSort, - tabType: TimelineTabs.query, - totalPages: 1, - leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT), - trailingControlColumns: [], - }; - - describe('rendering', () => { - beforeEach(() => { - mockDispatch.mockClear(); - }); - - test('it renders the column headers', async () => { - const wrapper = await getWrapper(); - expect(wrapper.find('[data-test-subj="column-headers"]').first().exists()).toEqual(true); - }); - - test('it renders the scroll container', async () => { - const wrapper = await getWrapper(); - expect(wrapper.find('[data-test-subj="timeline-body"]').first().exists()).toEqual(true); - }); - - test('it renders events', async () => { - const wrapper = await getWrapper(); - expect(wrapper.find('[data-test-subj="events"]').first().exists()).toEqual(true); - }); - test('it renders a tooltip for timestamp', async () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); - const state: State = { - ...mockGlobalState, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - ...mockGlobalState.timeline.timelineById, - [TimelineId.test]: { - ...mockGlobalState.timeline.timelineById[TimelineId.test], - id: TimelineId.test, - columns: headersJustTimestamp, - }, - }, - }, - }; - - const store = createMockStore(state); - const wrapper = await getWrapper(, { store }); - - headersJustTimestamp.forEach(() => { - expect( - wrapper - .find('[data-test-subj="data-driven-columns"]') - .first() - .find('[data-test-subj="localized-date-tool-tip"]') - .exists() - ).toEqual(true); - }); - }); - }); - - describe('event details', () => { - beforeEach(() => { - mockDispatch.mockReset(); - }); - - test('open the expandable flyout to show event details for query tab', async () => { - const wrapper = await getWrapper(); - - wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); - wrapper.update(); - expect(mockDispatch).not.toHaveBeenCalled(); - expect(mockOpenFlyout).toHaveBeenCalledWith({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: '1', - indexName: undefined, - scopeId: 'timeline-test', - }, - }, - }); - }); - - test('open the expandable flyout to show event details for pinned tab', async () => { - const wrapper = await getWrapper(); - - wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); - wrapper.update(); - expect(mockDispatch).not.toHaveBeenCalled(); - expect(mockOpenFlyout).toHaveBeenCalledWith({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: '1', - indexName: undefined, - scopeId: 'timeline-test', - }, - }, - }); - }); - - test('open the expandable flyout to show event details for notes tab', async () => { - const wrapper = await getWrapper(); - - wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); - wrapper.update(); - expect(mockDispatch).not.toHaveBeenCalled(); - expect(mockOpenFlyout).toHaveBeenCalledWith({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: '1', - indexName: undefined, - scopeId: 'timeline-test', - }, - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx deleted file mode 100644 index ab60e061fcdf9..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ /dev/null @@ -1,271 +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 { noop } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { - FIRST_ARIA_INDEX, - ARIA_COLINDEX_ATTRIBUTE, - ARIA_ROWINDEX_ATTRIBUTE, - onKeyDownFocusHandler, -} from '@kbn/timelines-plugin/public'; -import { getActionsColumnWidth } from '../../../../common/components/header_actions'; -import type { ControlColumnProps } from '../../../../../common/types'; -import type { CellValueElementProps } from '../cell_rendering'; -import { DEFAULT_COLUMN_MIN_WIDTH } from './constants'; -import type { RowRenderer, TimelineTabs } from '../../../../../common/types/timeline'; -import { RowRendererCount } from '../../../../../common/api/timeline'; -import type { BrowserFields } from '../../../../common/containers/source'; -import type { TimelineItem } from '../../../../../common/search_strategy/timeline'; -import type { inputsModel, State } from '../../../../common/store'; -import { timelineActions } from '../../../store'; -import type { OnRowSelected, OnSelectAll } from '../events'; -import { getColumnHeaders } from './column_headers/helpers'; -import { getEventIdToDataMapping } from './helpers'; -import type { Sort } from './sort'; -import { plainRowRenderer } from './renderers/plain_row_renderer'; -import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; -import { ColumnHeaders } from './column_headers'; -import { Events } from './events'; -import { useLicense } from '../../../../common/hooks/use_license'; -import { selectTimelineById } from '../../../store/selectors'; - -export interface Props { - activePage: number; - browserFields: BrowserFields; - data: TimelineItem[]; - id: string; - isEventViewer?: boolean; - sort: Sort[]; - refetch: inputsModel.Refetch; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - rowRenderers: RowRenderer[]; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; - tabType: TimelineTabs; - totalPages: number; - onRuleChange?: () => void; - onToggleShowNotes?: (eventId?: string) => void; -} - -/** - * The Body component is used everywhere timeline is used within the security application. It is the highest level component - * that is shared across all implementations of the timeline. - */ -export const StatefulBody = React.memo( - ({ - activePage, - browserFields, - data, - id, - isEventViewer = false, - onRuleChange, - refetch, - renderCellValue, - rowRenderers, - sort, - tabType, - totalPages, - leadingControlColumns = [], - trailingControlColumns = [], - onToggleShowNotes, - }) => { - const dispatch = useDispatch(); - const containerRef = useRef(null); - const { - columns, - eventIdToNoteIds, - excludedRowRendererIds, - isSelectAllChecked, - loadingEventIds, - pinnedEventIds, - selectedEventIds, - show, - queryFields, - selectAll, - } = useSelector((state: State) => selectTimelineById(state, id)); - - const columnHeaders = useMemo( - () => getColumnHeaders(columns, browserFields), - [browserFields, columns] - ); - - const isEnterprisePlus = useLicense().isEnterprise(); - const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5; - - const onRowSelected: OnRowSelected = useCallback( - ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { - dispatch( - timelineActions.setSelected({ - id, - eventIds: getEventIdToDataMapping(data, eventIds, queryFields), - isSelected, - isSelectAllChecked: - isSelected && Object.keys(selectedEventIds).length + 1 === data.length, - }) - ); - }, - [data, dispatch, id, queryFields, selectedEventIds] - ); - - const onSelectAll: OnSelectAll = useCallback( - ({ isSelected }: { isSelected: boolean }) => - isSelected - ? dispatch( - timelineActions.setSelected({ - id, - eventIds: getEventIdToDataMapping( - data, - data.map((event) => event._id), - queryFields - ), - isSelected, - isSelectAllChecked: isSelected, - }) - ) - : dispatch(timelineActions.clearSelected({ id })), - [data, dispatch, id, queryFields] - ); - - // Sync to selectAll so parent components can select all events - useEffect(() => { - if (selectAll && !isSelectAllChecked) { - onSelectAll({ isSelected: true }); - } - }, [isSelectAllChecked, onSelectAll, selectAll]); - - const enabledRowRenderers = useMemo(() => { - if (excludedRowRendererIds && excludedRowRendererIds.length === RowRendererCount) - return [plainRowRenderer]; - - if (!excludedRowRendererIds) return rowRenderers; - - return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); - }, [excludedRowRendererIds, rowRenderers]); - - const actionsColumnWidth = useMemo( - () => getActionsColumnWidth(ACTION_BUTTON_COUNT), - [ACTION_BUTTON_COUNT] - ); - - const columnWidths = useMemo( - () => - columnHeaders.reduce( - (totalWidth, header) => totalWidth + (header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH), - 0 - ), - [columnHeaders] - ); - - const leadingActionColumnsWidth = useMemo(() => { - return leadingControlColumns - ? leadingControlColumns.reduce( - (totalWidth, header) => - header.width ? totalWidth + header.width : totalWidth + actionsColumnWidth, - 0 - ) - : 0; - }, [actionsColumnWidth, leadingControlColumns]); - - const trailingActionColumnsWidth = useMemo(() => { - return trailingControlColumns - ? trailingControlColumns.reduce( - (totalWidth, header) => - header.width ? totalWidth + header.width : totalWidth + actionsColumnWidth, - 0 - ) - : 0; - }, [actionsColumnWidth, trailingControlColumns]); - - const totalWidth = useMemo(() => { - return columnWidths + leadingActionColumnsWidth + trailingActionColumnsWidth; - }, [columnWidths, leadingActionColumnsWidth, trailingActionColumnsWidth]); - - const [lastFocusedAriaColindex] = useState(FIRST_ARIA_INDEX); - - const columnCount = useMemo(() => { - return columnHeaders.length + trailingControlColumns.length + leadingControlColumns.length; - }, [columnHeaders, trailingControlColumns, leadingControlColumns]); - - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - onKeyDownFocusHandler({ - colindexAttribute: ARIA_COLINDEX_ATTRIBUTE, - containerElement: containerRef.current, - event: e, - maxAriaColindex: columnHeaders.length + 1, - maxAriaRowindex: data.length + 1, - onColumnFocused: noop, - rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE, - }); - }, - [columnHeaders.length, containerRef, data.length] - ); - - return ( - <> - - - - - - - - - - ); - } -); - -StatefulBody.displayName = 'StatefulBody'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts deleted file mode 100644 index 36749de01333a..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts +++ /dev/null @@ -1,147 +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 moment from 'moment'; - -import type { MomentUnit } from './date_ranges'; -import { getDateRange, getDates } from './date_ranges'; - -describe('dateRanges', () => { - describe('#getDates', () => { - test('given a unit of "year", it returns the four quarters of the year', () => { - const unit: MomentUnit = 'year'; - const end = moment.utc('Mon, 31 Dec 2018 23:59:59 -0700'); - const current = moment.utc('Mon, 01 Jan 2018 00:00:00 -0700'); - - expect(getDates({ unit, end, current })).toEqual( - [ - '2018-01-01T07:00:00.000Z', - '2018-04-01T07:00:00.000Z', - '2018-07-01T07:00:00.000Z', - '2018-10-01T07:00:00.000Z', - ].map((d) => new Date(d)) - ); - }); - - test('given a unit of "month", it returns all the weeks of the month', () => { - const unit: MomentUnit = 'month'; - const end = moment.utc('Wed, 31 Oct 2018 23:59:59 -0600'); - const current = moment.utc('Mon, 01 Oct 2018 00:00:00 -0600'); - - expect(getDates({ unit, end, current })).toEqual( - [ - '2018-10-01T06:00:00.000Z', - '2018-10-08T06:00:00.000Z', - '2018-10-15T06:00:00.000Z', - '2018-10-22T06:00:00.000Z', - '2018-10-29T06:00:00.000Z', - ].map((d) => new Date(d)) - ); - }); - - test('given a unit of "week", it returns all the days of the week', () => { - const unit: MomentUnit = 'week'; - const end = moment.utc('Sat, 27 Oct 2018 23:59:59 -0600'); - const current = moment.utc('Sun, 21 Oct 2018 00:00:00 -0600'); - - expect(getDates({ unit, end, current })).toEqual( - [ - '2018-10-21T06:00:00.000Z', - '2018-10-22T06:00:00.000Z', - '2018-10-23T06:00:00.000Z', - '2018-10-24T06:00:00.000Z', - '2018-10-25T06:00:00.000Z', - '2018-10-26T06:00:00.000Z', - '2018-10-27T06:00:00.000Z', - ].map((d) => new Date(d)) - ); - }); - - test('given a unit of "day", it returns all the hours of the day', () => { - const unit: MomentUnit = 'day'; - const end = moment.utc('Tue, 23 Oct 2018 23:59:59 -0600'); - const current = moment.utc('Tue, 23 Oct 2018 00:00:00 -0600'); - - expect(getDates({ unit, end, current })).toEqual( - [ - '2018-10-23T06:00:00.000Z', - '2018-10-23T07:00:00.000Z', - '2018-10-23T08:00:00.000Z', - '2018-10-23T09:00:00.000Z', - '2018-10-23T10:00:00.000Z', - '2018-10-23T11:00:00.000Z', - '2018-10-23T12:00:00.000Z', - '2018-10-23T13:00:00.000Z', - '2018-10-23T14:00:00.000Z', - '2018-10-23T15:00:00.000Z', - '2018-10-23T16:00:00.000Z', - '2018-10-23T17:00:00.000Z', - '2018-10-23T18:00:00.000Z', - '2018-10-23T19:00:00.000Z', - '2018-10-23T20:00:00.000Z', - '2018-10-23T21:00:00.000Z', - '2018-10-23T22:00:00.000Z', - '2018-10-23T23:00:00.000Z', - '2018-10-24T00:00:00.000Z', - '2018-10-24T01:00:00.000Z', - '2018-10-24T02:00:00.000Z', - '2018-10-24T03:00:00.000Z', - '2018-10-24T04:00:00.000Z', - '2018-10-24T05:00:00.000Z', - ].map((d) => new Date(d)) - ); - }); - }); - - describe('#getDateRange', () => { - let dateSpy: jest.SpyInstance; - - beforeEach(() => { - dateSpy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2018, 10, 23)).valueOf()); - }); - - afterEach(() => { - dateSpy.mockReset(); - }); - - test('given a unit of "day", it returns all the hours of the day', () => { - const unit: MomentUnit = 'day'; - - const dates = getDateRange(unit); - expect(dates).toEqual( - [ - '2018-11-23T00:00:00.000Z', - '2018-11-23T01:00:00.000Z', - '2018-11-23T02:00:00.000Z', - '2018-11-23T03:00:00.000Z', - '2018-11-23T04:00:00.000Z', - '2018-11-23T05:00:00.000Z', - '2018-11-23T06:00:00.000Z', - '2018-11-23T07:00:00.000Z', - '2018-11-23T08:00:00.000Z', - '2018-11-23T09:00:00.000Z', - '2018-11-23T10:00:00.000Z', - '2018-11-23T11:00:00.000Z', - '2018-11-23T12:00:00.000Z', - '2018-11-23T13:00:00.000Z', - '2018-11-23T14:00:00.000Z', - '2018-11-23T15:00:00.000Z', - '2018-11-23T16:00:00.000Z', - '2018-11-23T17:00:00.000Z', - '2018-11-23T18:00:00.000Z', - '2018-11-23T19:00:00.000Z', - '2018-11-23T20:00:00.000Z', - '2018-11-23T21:00:00.000Z', - '2018-11-23T22:00:00.000Z', - '2018-11-23T23:00:00.000Z', - ].map((d) => new Date(d)) - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.ts deleted file mode 100644 index c715b2004c69c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; - -export type MomentUnit = 'year' | 'month' | 'week' | 'day'; - -export type MomentIncrement = 'quarters' | 'months' | 'weeks' | 'days' | 'hours'; - -export type MomentUnitToIncrement = { [key in MomentUnit]: MomentIncrement }; - -const unitsToIncrements: MomentUnitToIncrement = { - day: 'hours', - month: 'weeks', - week: 'days', - year: 'quarters', -}; - -interface GetDatesParams { - unit: MomentUnit; - end: moment.Moment; - current: moment.Moment; -} - -/** - * A pure function that given a unit (e.g. `'year' | 'month' | 'week'...`) and - * a date range, returns a range of `Date`s with a granularity appropriate - * to the unit. - * - * @example - * test('given a unit of "year", it returns the four quarters of the year', () => { - * const unit: MomentUnit = 'year'; - * const end = moment.utc('Mon, 31 Dec 2018 23:59:59 -0700'); - * const current = moment.utc('Mon, 01 Jan 2018 00:00:00 -0700'); - * - * expect(getDates({ unit, end, current })).toEqual( - * [ - * '2018-01-01T07:00:00.000Z', - * '2018-04-01T06:00:00.000Z', - * '2018-07-01T06:00:00.000Z', - * '2018-10-01T06:00:00.000Z' - * ].map(d => new Date(d)) - * ); - * }); - */ -export const getDates = ({ unit, end, current }: GetDatesParams): Date[] => - current <= end - ? [ - current.toDate(), - ...getDates({ - current: current.clone().add(1, unitsToIncrements[unit]), - end, - unit, - }), - ] - : []; - -/** - * An impure function (it performs IO to get the current `Date`) that, - * given a unit (e.g. `'year' | 'month' | 'week'...`), it - * returns range of `Date`s with a granularity appropriate to the unit. - */ -export function getDateRange(unit: MomentUnit): Date[] { - const current = moment().utc().startOf(unit); - const end = moment().utc().endOf(unit); - - return getDates({ - current, - end, // TODO: this should be relative to `unit` - unit, - }); -} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_udt.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_udt.test.tsx index d731b6e831f9d..c1fe2f3278dd5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_udt.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_udt.test.tsx @@ -6,7 +6,7 @@ */ import { mockTimelineData } from '../../../../../common/mock'; -import { defaultUdtHeaders } from '../../unified_components/default_headers'; +import { defaultUdtHeaders } from '../column_headers/default_headers'; import { getFormattedFields } from './formatted_field_udt'; import type { DataTableRecord } from '@kbn/discover-utils/types'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx index 551ba3c4ac570..ad75ef79aa049 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx @@ -13,13 +13,13 @@ import type { TimelineNonEcsData } from '../../../../../../common/search_strateg import { mockTimelineData } from '../../../../../common/mock'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { getEmptyValue } from '../../../../../common/components/empty_value'; -import { defaultHeaders } from '../column_headers/default_headers'; import { columnRenderers } from '.'; import { getColumnRenderer } from './get_column_renderer'; import { getValues, findItem, deleteItemIdx } from './helpers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { TimelineId } from '../../../../../../common/types/timeline'; +import { defaultUdtHeaders } from '../column_headers/default_headers'; jest.mock('../../../../../common/lib/kibana'); @@ -47,7 +47,7 @@ describe('get_column_renderer', () => { columnName, eventId: _id, values: getValues(columnName, nonSuricata), - field: defaultHeaders[1], + field: defaultUdtHeaders[1], scopeId: TimelineId.test, }); @@ -62,7 +62,7 @@ describe('get_column_renderer', () => { columnName, eventId: _id, values: getValues(columnName, nonSuricata), - field: defaultHeaders[1], + field: defaultUdtHeaders[1], scopeId: TimelineId.test, }); const wrapper = mount( @@ -82,7 +82,7 @@ describe('get_column_renderer', () => { columnName, eventId: _id, values: getValues(columnName, nonSuricata), - field: defaultHeaders[7], + field: defaultUdtHeaders[7], scopeId: TimelineId.test, }); const wrapper = mount( @@ -100,7 +100,7 @@ describe('get_column_renderer', () => { columnName, eventId: _id, values: getValues(columnName, nonSuricata), - field: defaultHeaders[7], + field: defaultUdtHeaders[7], scopeId: TimelineId.test, }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx index a5cd33efdd5c4..3912d7d9ef8ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { mockTimelineData, TestProviders } from '../../../../../common/mock'; -import { defaultColumnHeaderType } from '../column_headers/default_headers'; import { REASON_FIELD_NAME } from './constants'; import { reasonColumnRenderer } from './reason_column_renderer'; import { plainColumnRenderer } from './plain_column_renderer'; @@ -19,6 +18,7 @@ import { RowRendererIdEnum } from '../../../../../../common/api/timeline'; import { render } from '@testing-library/react'; import { cloneDeep } from 'lodash'; import { TableId } from '@kbn/securitysolution-data-table'; +import { defaultColumnHeaderType } from '../column_headers/default_headers'; jest.mock('./plain_column_renderer'); jest.mock('../../../../../common/components/link_to', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap deleted file mode 100644 index 8a7b179da059f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SortIndicator rendering renders correctly against snapshot 1`] = ` - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts deleted file mode 100644 index 96503dcac3812..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SortColumnTimeline } from '../../../../../../common/types/timeline'; - -/** Specifies which column the timeline is sorted on */ -export type Sort = SortColumnTimeline; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx deleted file mode 100644 index 56f98a6795cd1..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; -import { Direction } from '../../../../../../common/search_strategy'; - -import * as i18n from '../translations'; - -import { getDirection, SortIndicator } from './sort_indicator'; - -describe('SortIndicator', () => { - describe('rendering', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the expected sort indicator when direction is ascending', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( - 'sortUp' - ); - }); - - test('it renders the expected sort indicator when direction is descending', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( - 'sortDown' - ); - }); - - test('it renders the expected sort indicator when direction is `none`', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( - 'empty' - ); - }); - }); - - describe('getDirection', () => { - test('it returns the expected symbol when the direction is ascending', () => { - expect(getDirection(Direction.asc)).toEqual('sortUp'); - }); - - test('it returns the expected symbol when the direction is descending', () => { - expect(getDirection(Direction.desc)).toEqual('sortDown'); - }); - - test('it returns the expected symbol (undefined) when the direction is neither ascending, nor descending', () => { - expect(getDirection('none')).toEqual(undefined); - }); - }); - - describe('sort indicator tooltip', () => { - test('it returns the expected tooltip when the direction is ascending', () => { - const wrapper = mount(); - - expect( - wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content - ).toEqual(i18n.SORTED_ASCENDING); - }); - - test('it returns the expected tooltip when the direction is descending', () => { - const wrapper = mount(); - - expect( - wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content - ).toEqual(i18n.SORTED_DESCENDING); - }); - - test('it does NOT render a tooltip when sort direction is `none`', () => { - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx deleted file mode 100644 index 82c25f00c78ab..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx +++ /dev/null @@ -1,68 +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 { EuiIcon, EuiToolTip } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from '../translations'; -import { SortNumber } from './sort_number'; - -import { Direction } from '../../../../../../common/search_strategy'; -import type { SortDirection } from '../../../../../../common/types/timeline'; - -enum SortDirectionIndicatorEnum { - SORT_UP = 'sortUp', - SORT_DOWN = 'sortDown', -} - -export type SortDirectionIndicator = undefined | SortDirectionIndicatorEnum; - -/** Returns the symbol that corresponds to the specified `SortDirection` */ -export const getDirection = (sortDirection: SortDirection): SortDirectionIndicator => { - switch (sortDirection) { - case Direction.asc: - return SortDirectionIndicatorEnum.SORT_UP; - case Direction.desc: - return SortDirectionIndicatorEnum.SORT_DOWN; - case 'none': - return undefined; - default: - throw new Error('Unhandled sort direction'); - } -}; - -interface Props { - sortDirection: SortDirection; - sortNumber: number; -} - -/** Renders a sort indicator */ -export const SortIndicator = React.memo(({ sortDirection, sortNumber }) => { - const direction = getDirection(sortDirection); - - if (direction != null) { - return ( - - <> - - - - - ); - } else { - return ; - } -}); - -SortIndicator.displayName = 'SortIndicator'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx deleted file mode 100644 index 3fdd31eae5c47..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiIcon, EuiNotificationBadge } from '@elastic/eui'; -import React from 'react'; - -interface Props { - sortNumber: number; -} - -export const SortNumber = React.memo(({ sortNumber }) => { - if (sortNumber >= 0) { - return ( - - {sortNumber + 1} - - ); - } else { - return ; - } -}); - -SortNumber.displayName = 'SortNumber'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx index 031604c6a3da6..41cdec6d6d4bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx @@ -9,7 +9,7 @@ import { TimelineTabs } from '../../../../../common/types'; import { DataLoadingState } from '@kbn/unified-data-table'; import React from 'react'; import { UnifiedTimeline } from '../unified_components'; -import { defaultUdtHeaders } from '../unified_components/default_headers'; +import { defaultUdtHeaders } from './column_headers/default_headers'; import type { UnifiedTimelineBodyProps } from './unified_timeline_body'; import { UnifiedTimelineBody } from './unified_timeline_body'; import { render, screen } from '@testing-library/react'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx index fe6b668ed6837..95feab8543617 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useState, useMemo } from 'react'; import { RootDragDropProvider } from '@kbn/dom-drag-drop'; import { StyledTableFlexGroup, StyledUnifiedTableFlexItem } from '../unified_components/styles'; import { UnifiedTimeline } from '../unified_components'; -import { defaultUdtHeaders } from '../unified_components/default_headers'; +import { defaultUdtHeaders } from './column_headers/default_headers'; import type { PaginationInputPaginated, TimelineItem } from '../../../../../common/search_strategy'; export interface UnifiedTimelineBodyProps extends ComponentProps { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx index 9e5006267d32b..ec230139dc95e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -8,7 +8,7 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { useGetMappedNonEcsValue } from '../body/data_driven_columns'; +import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value'; import { columnRenderers } from '../body/renderers'; import { getColumnRenderer } from '../body/renderers/get_column_renderer'; import type { CellValueElementProps } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx index f7dad276cb939..9f187f91dcdff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx @@ -17,6 +17,7 @@ import { buildIsOneOfQueryMatch, buildIsQueryMatch, handleIsOperator, + isFullScreen, isPrimitiveArray, showGlobalFilters, } from './helpers'; @@ -392,3 +393,42 @@ describe('isStringOrNumberArray', () => { }); }); }); + +describe('isFullScreen', () => { + describe('globalFullScreen is false', () => { + it('should return false if isActiveTimelines is false', () => { + const result = isFullScreen({ + globalFullScreen: false, + isActiveTimelines: false, + timelineFullScreen: true, + }); + expect(result).toBe(false); + }); + it('should return false if timelineFullScreen is false', () => { + const result = isFullScreen({ + globalFullScreen: false, + isActiveTimelines: true, + timelineFullScreen: false, + }); + expect(result).toBe(false); + }); + }); + describe('globalFullScreen is true', () => { + it('should return true if isActiveTimelines is true and timelineFullScreen is true', () => { + const result = isFullScreen({ + globalFullScreen: true, + isActiveTimelines: true, + timelineFullScreen: true, + }); + expect(result).toBe(true); + }); + it('should return true if isActiveTimelines is false', () => { + const result = isFullScreen({ + globalFullScreen: true, + isActiveTimelines: false, + timelineFullScreen: false, + }); + expect(result).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx index 04f08f203ec7f..43c4648abb83a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx @@ -282,3 +282,14 @@ export const TIMELINE_FILTER_DROP_AREA = 'timeline-filter-drop-area'; export const getNonDropAreaFilters = (filters: Filter[] = []) => filters.filter((f: Filter) => f.meta.controlledBy !== TIMELINE_FILTER_DROP_AREA); + +export const isFullScreen = ({ + globalFullScreen, + isActiveTimelines, + timelineFullScreen, +}: { + globalFullScreen: boolean; + isActiveTimelines: boolean; + timelineFullScreen: boolean; +}) => + (isActiveTimelines && timelineFullScreen) || (isActiveTimelines === false && globalFullScreen); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index ea0edabdfe7bb..05d15f076f569 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -29,7 +29,7 @@ import { useTimelineFullScreen } from '../../../common/containers/use_full_scree import { EXIT_FULL_SCREEN_CLASS_NAME } from '../../../common/components/exit_full_screen'; import { useResolveConflict } from '../../../common/hooks/use_resolve_conflict'; import { sourcererSelectors } from '../../../common/store'; -import { defaultUdtHeaders } from './unified_components/default_headers'; +import { defaultUdtHeaders } from './body/column_headers/default_headers'; const TimelineTemplateBadge = styled.div` background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 97762de6bcb91..1591c7f8c791b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -10,11 +10,6 @@ import { rgba } from 'polished'; import styled, { createGlobalStyle } from 'styled-components'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import type { TimelineEventsType } from '../../../../common/types/timeline'; - -import { ACTIONS_COLUMN_ARIA_COL_INDEX } from './helpers'; -import { EVENTS_TABLE_ARIA_LABEL } from './translations'; - /** * TIMELINE BODY */ @@ -73,79 +68,6 @@ TimelineBody.displayName = 'TimelineBody'; export const EVENTS_TABLE_CLASS_NAME = 'siemEventsTable'; -interface EventsTableProps { - $activePage: number; - $columnCount: number; - columnWidths: number; - $rowCount: number; - $totalPages: number; -} - -export const EventsTable = styled.div.attrs( - ({ className = '', $columnCount, columnWidths, $activePage, $rowCount, $totalPages }) => ({ - 'aria-label': EVENTS_TABLE_ARIA_LABEL({ activePage: $activePage + 1, totalPages: $totalPages }), - 'aria-colcount': `${$columnCount}`, - 'aria-rowcount': `${$rowCount + 1}`, - className: `siemEventsTable ${className}`, - role: 'grid', - style: { - minWidth: `${columnWidths}px`, - }, - tabindex: '-1', - }) -)` - padding: 3px; -`; - -/* EVENTS HEAD */ - -export const EventsThead = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable__thead ${className}`, - role: 'rowgroup', -}))` - background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; - border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThick} solid - ${({ theme }) => theme.eui.euiColorLightShade}; - position: sticky; - top: 0; - z-index: ${({ theme }) => theme.eui.euiZLevel1}; -`; - -export const EventsTrHeader = styled.div.attrs(({ className }) => ({ - 'aria-rowindex': '1', - className: `siemEventsTable__trHeader ${className}`, - role: 'row', -}))` - display: flex; -`; - -export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ - 'aria-colindex': `${ACTIONS_COLUMN_ARIA_COL_INDEX}`, - className: `siemEventsTable__thGroupActions ${className}`, - role: 'columnheader', - tabIndex: '0', -}))<{ actionsColumnWidth: number; isEventViewer: boolean }>` - display: flex; - flex: 0 0 - ${({ actionsColumnWidth, isEventViewer }) => - `${!isEventViewer ? actionsColumnWidth + 4 : actionsColumnWidth}px`}; - min-width: 0; - padding-left: ${({ isEventViewer }) => - !isEventViewer ? '4px;' : '0;'}; // match timeline event border -`; - -export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable__thGroupData ${className}`, -}))<{ isDragging?: boolean }>` - display: flex; - - > div:hover .siemEventsHeading__handle { - display: ${({ isDragging }) => (isDragging ? 'none' : 'block')}; - opacity: 1; - visibility: visible; - } -`; - export const EventsTh = styled.div.attrs<{ role: string }>( ({ className = '', role = 'columnheader' }) => ({ className: `siemEventsTable__th ${className}`, @@ -197,76 +119,6 @@ export const EventsThContent = styled.div.attrs(({ className = '' }) => ({ } `; -/* EVENTS BODY */ - -export const EventsTbody = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable__tbody ${className}`, - role: 'rowgroup', -}))` - overflow-x: hidden; -`; - -export const EventsTrGroup = styled.div.attrs( - ({ className = '', $ariaRowindex }: { className?: string; $ariaRowindex: number }) => ({ - 'aria-rowindex': `${$ariaRowindex}`, - className: `siemEventsTable__trGroup ${className}`, - role: 'row', - }) -)<{ - className?: string; - eventType: Omit; - isEvenEqlSequence: boolean; - isBuildingBlockType: boolean; - isExpanded: boolean; - showLeftBorder: boolean; -}>` - border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid - ${({ theme }) => theme.eui.euiColorLightShade}; - ${({ theme, eventType, isEvenEqlSequence, showLeftBorder }) => - showLeftBorder - ? `border-left: 4px solid - ${ - eventType === 'raw' - ? theme.eui.euiColorLightShade - : eventType === 'eql' && isEvenEqlSequence - ? theme.eui.euiColorPrimary - : eventType === 'eql' && !isEvenEqlSequence - ? theme.eui.euiColorAccent - : theme.eui.euiColorWarning - }` - : ''}; - ${({ isBuildingBlockType }) => - isBuildingBlockType - ? 'background: repeating-linear-gradient(127deg, rgba(245, 167, 0, 0.2), rgba(245, 167, 0, 0.2) 1px, rgba(245, 167, 0, 0.05) 2px, rgba(245, 167, 0, 0.05) 10px);' - : ''}; - ${({ eventType, isEvenEqlSequence }) => - eventType === 'eql' - ? isEvenEqlSequence - ? 'background: repeating-linear-gradient(127deg, rgba(0, 107, 180, 0.2), rgba(0, 107, 180, 0.2) 1px, rgba(0, 107, 180, 0.05) 2px, rgba(0, 107, 180, 0.05) 10px);' - : 'background: repeating-linear-gradient(127deg, rgba(221, 10, 115, 0.2), rgba(221, 10, 115, 0.2) 1px, rgba(221, 10, 115, 0.05) 2px, rgba(221, 10, 115, 0.05) 10px);' - : ''}; - - &:hover { - background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; - } - - ${({ isExpanded, theme }) => - isExpanded && - ` - background: ${theme.eui.euiTableSelectedColor}; - - &:hover { - ${theme.eui.euiTableHoverSelectedColor} - } - `} -`; - -export const EventsTrData = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable__trData ${className}`, -}))` - display: flex; -`; - const TIMELINE_EVENT_DETAILS_OFFSET = 40; interface WidthProp { @@ -295,57 +147,6 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ } `; -export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({ - 'aria-colindex': `${ACTIONS_COLUMN_ARIA_COL_INDEX}`, - className: `siemEventsTable__tdGroupActions ${className}`, - role: 'gridcell', -}))<{ width: number }>` - align-items: center; - display: flex; - flex: 0 0 ${({ width }) => `${width}px`}; - min-width: 0; -`; - -export const EventsTdGroupData = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable__tdGroupData ${className}`, -}))` - display: flex; -`; -interface EventsTdProps { - $ariaColumnIndex?: number; - width?: number; -} - -export const EVENTS_TD_CLASS_NAME = 'siemEventsTable__td'; - -export const EventsTd = styled.div.attrs( - ({ className = '', $ariaColumnIndex, width }) => { - const common = { - className: `siemEventsTable__td ${className}`, - role: 'gridcell', - style: { - flexBasis: width ? `${width}px` : 'auto', - }, - }; - - return $ariaColumnIndex != null - ? { - ...common, - 'aria-colindex': `${$ariaColumnIndex}`, - } - : common; - } -)` - align-items: center; - display: flex; - flex-shrink: 0; - min-width: 0; - - .siemEventsTable__tdGroupActions &:first-child:last-child { - flex: 1; - } -`; - export const EventsTdContent = styled.div.attrs(({ className }) => ({ className: `siemEventsTable__tdContent ${className != null ? className : ''}`, }))<{ textAlign?: string; width?: number }>` @@ -363,89 +164,9 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({ } `; -/** - * EVENTS HEADING - */ - -export const EventsHeading = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsHeading ${className}`, -}))<{ isLoading: boolean }>` - align-items: center; - display: flex; - - &:hover { - cursor: ${({ isLoading }) => (isLoading ? 'wait' : 'grab')}; - } -`; - -export const EventsHeadingTitleButton = styled.button.attrs(({ className = '' }) => ({ - className: `siemEventsHeading__title siemEventsHeading__title--aggregatable ${className}`, - type: 'button', -}))` - align-items: center; - display: flex; - font-weight: inherit; - min-width: 0; - - &:hover, - &:focus { - color: ${({ theme }) => theme.eui.euiColorPrimary}; - text-decoration: underline; - } - - &:hover { - cursor: pointer; - } - - & > * + * { - margin-left: ${({ theme }) => theme.eui.euiSizeXS}; - } -`; - -export const EventsHeadingTitleSpan = styled.span.attrs(({ className }) => ({ - className: `siemEventsHeading__title siemEventsHeading__title--notAggregatable ${className}`, -}))` - min-width: 0; -`; - -export const EventsHeadingExtra = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsHeading__extra ${className}` as string, -}))` - margin-left: auto; - margin-right: 2px; - - &.siemEventsHeading__extra--close { - opacity: 0; - transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease; - visibility: hidden; - - .siemEventsTable__th:hover & { - opacity: 1; - visibility: visible; - } - } -`; - -export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsHeading__handle ${className}`, -}))` - background-color: ${({ theme }) => theme.eui.euiBorderColor}; - height: 100%; - opacity: 0; - transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease; - visibility: hidden; - width: ${({ theme }) => theme.eui.euiBorderWidthThick}; - - &:hover { - background-color: ${({ theme }) => theme.eui.euiColorPrimary}; - cursor: col-resize; - } -`; - /** * EVENTS LOADING */ - export const EventsLoading = styled(EuiLoadingSpinner)` margin: 0 2px; vertical-align: middle; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx index 7c8949c1b6121..23fb44d04910f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx @@ -35,9 +35,6 @@ jest.mock('../../../../containers/details', () => ({ jest.mock('../../../fields_browser', () => ({ useFieldBrowserOptions: jest.fn(), })); -jest.mock('../../body/events', () => ({ - Events: () => <>, -})); jest.mock('../../../../../sourcerer/containers'); jest.mock('../../../../../sourcerer/containers/use_signal_helpers', () => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.test.tsx index 0df50a8cf47c3..55604cd958c23 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.test.tsx @@ -14,7 +14,7 @@ import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer' import { defaultHeaders, mockTimelineData } from '../../../../../common/mock'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { defaultRowRenderers } from '../../body/renderers'; -import type { Sort } from '../../body/sort'; +import type { SortColumnTimeline as Sort } from '../../../../../../common/types/timeline'; import { TimelineId } from '../../../../../../common/types/timeline'; import { useTimelineEvents } from '../../../../containers'; import { useTimelineEventsDetails } from '../../../../containers/details'; @@ -38,9 +38,6 @@ jest.mock('../../../../containers/details', () => ({ jest.mock('../../../fields_browser', () => ({ useFieldBrowserOptions: jest.fn(), })); -jest.mock('../../body/events', () => ({ - Events: () => <>, -})); jest.mock('../../../../../sourcerer/containers'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index 8f19e90c77a70..e4593f7eec959 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -21,7 +21,6 @@ import { useKibana } from '../../../../../common/lib/kibana'; import { timelineSelectors } from '../../../../store'; import type { Direction } from '../../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../../containers'; -import { defaultHeaders } from '../../body/column_headers/default_headers'; import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { timelineDefaults } from '../../../../store/defaults'; @@ -37,6 +36,7 @@ import { useTimelineControlColumn } from '../shared/use_timeline_control_columns import { LeftPanelNotesTab } from '../../../../../flyout/document_details/left'; import { useNotesInFlyout } from '../../properties/use_notes_in_flyout'; import { NotesFlyout } from '../../properties/notes_flyout'; +import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; interface PinnedFilter { bool: { @@ -111,7 +111,7 @@ export const PinnedTabContentComponent: React.FC = ({ }, [pinnedEventIds]); const timelineQueryFields = useMemo(() => { - const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + const columnsHeader = isEmpty(columns) ? defaultUdtHeaders : columns; const columnFields = columnsHeader.map((c) => c.id); return [...columnFields, ...requiredFieldsForActions]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx index 70afec0d73135..f0a2c06bbffb4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx @@ -30,8 +30,10 @@ import { useDispatch } from 'react-redux'; import type { ExperimentalFeatures } from '../../../../../../common'; import { allowedExperimentalValues } from '../../../../../../common'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { defaultUdtHeaders } from '../../unified_components/default_headers'; -import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; +import { + defaultUdtHeaders, + defaultColumnHeaderType, +} from '../../body/column_headers/default_headers'; import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; import * as timelineActions from '../../../../store/actions'; @@ -52,10 +54,6 @@ jest.mock('../../../fields_browser', () => ({ useFieldBrowserOptions: jest.fn(), })); -jest.mock('../../body/events', () => ({ - Events: () => <>, -})); - jest.mock('../../../../../sourcerer/containers'); jest.mock('../../../../../sourcerer/containers/use_signal_helpers', () => ({ useSignalHelpers: () => ({ signalIndexNeedsInit: false }), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx index eae2eec549dfe..c711208cb6806 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx @@ -23,7 +23,6 @@ import { useKibana } from '../../../../../common/lib/kibana'; import * as i18n from './translations'; import { TimelineTabs } from '../../../../../../common/types/timeline'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; -import { isFullScreen } from '../../body/column_headers'; import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../../../common/constants'; import { FULL_SCREEN } from '../../body/column_headers/translations'; import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations'; @@ -35,6 +34,7 @@ import { useUserPrivileges } from '../../../../../common/components/user_privile import { timelineActions, timelineSelectors } from '../../../../store'; import { timelineDefaults } from '../../../../store/defaults'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; +import { isFullScreen } from '../../helpers'; const FullScreenButtonIcon = styled(EuiButtonIcon)` margin: 4px 0 4px 0; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/layout.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/layout.tsx index aacb38ea2e798..1cac4dc2536f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/layout.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/layout.tsx @@ -5,14 +5,7 @@ * 2.0. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiBadge, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlyoutHeader, EuiBadge } from '@elastic/eui'; import styled from 'styled-components'; export const TabHeaderContainer = styled.div` @@ -33,46 +26,12 @@ export const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` } `; -export const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` - overflow-y: hidden; - flex: 1; - - .euiFlyoutBody__overflow { - overflow: hidden; - mask-image: none; - } - - .euiFlyoutBody__overflowContent { - padding: 0; - height: 100%; - display: flex; - } -`; - -export const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)` - background: none; - &.euiFlyoutFooter { - ${({ theme }) => `padding: ${theme.eui.euiSizeS} 0;`} - } -`; - export const FullWidthFlexGroup = styled(EuiFlexGroup)` margin: 0; width: 100%; overflow: hidden; `; -export const ScrollableFlexItem = styled(EuiFlexItem)` - ${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`} - overflow: hidden; -`; - -export const SourcererFlex = styled(EuiFlexItem)` - align-items: flex-end; -`; - -SourcererFlex.displayName = 'SourcererFlex'; - export const VerticalRule = styled.div` width: 2px; height: 100%; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts index 8bbda9a255a09..926082ff9ed41 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts @@ -8,7 +8,7 @@ import { TestProviders } from '../../../../../common/mock'; import { renderHook } from '@testing-library/react-hooks'; import { useTimelineColumns } from './use_timeline_columns'; -import { defaultUdtHeaders } from '../../unified_components/default_headers'; +import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline/columns'; jest.mock('../../../../../common/hooks/use_experimental_features', () => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx index f42bf47c76423..006c6ba1eb679 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { SourcererScopeName } from '../../../../../sourcerer/store/model'; import { useSourcererDataView } from '../../../../../sourcerer/containers'; import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config'; -import { defaultUdtHeaders } from '../../unified_components/default_headers'; +import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; import type { ColumnHeaderOptions } from '../../../../../../common/types'; import { memoizedGetTimelineColumnHeaders } from './utils'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts index 879e4b140a61a..543c7daaf8679 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts @@ -7,7 +7,7 @@ import type { BrowserFields, ColumnHeaderOptions } from '@kbn/timelines-plugin/common'; import memoizeOne from 'memoize-one'; import type { ControlColumnProps } from '../../../../../../common/types'; -import type { Sort } from '../../body/sort'; +import type { SortColumnTimeline as Sort } from '../../../../../../common/types/timeline'; import type { TimelineItem } from '../../../../../../common/search_strategy'; import type { inputsModel } from '../../../../../common/store'; import { getColumnHeaders } from '../../body/column_headers/helpers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.test.tsx index cfdb2b0d2dbf9..cc8b24710e5af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.test.tsx @@ -12,7 +12,7 @@ import { CustomTimelineDataGridBody } from './custom_timeline_data_grid_body'; import { mockTimelineData, TestProviders } from '../../../../../common/mock'; import type { TimelineItem } from '@kbn/timelines-plugin/common'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { defaultUdtHeaders } from '../default_headers'; +import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; import type { EuiDataGridColumn } from '@elastic/eui'; import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer'; import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx index 78ffadeb37ff8..649817d5f8ef2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx @@ -8,7 +8,6 @@ import { createMockStore, mockTimelineData, TestProviders } from '../../../../../common/mock'; import React from 'react'; import { TimelineDataTable } from '.'; -import { defaultUdtHeaders } from '../default_headers'; import { TimelineId, TimelineTabs } from '../../../../../../common/types'; import { DataLoadingState } from '@kbn/unified-data-table'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; @@ -18,6 +17,7 @@ import { getColumnHeaders } from '../../body/column_headers/helpers'; import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks'; import { timelineActions } from '../../../../store'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; jest.mock('../../../../../sourcerer/containers'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/default_headers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/default_headers.tsx deleted file mode 100644 index a0cf9d6355b9d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/default_headers.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../common/types'; -import { - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH, -} from '../body/constants'; - -export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; - -export const defaultUdtHeaders: ColumnHeaderOptions[] = [ - { - columnHeaderType: defaultColumnHeaderType, - id: '@timestamp', - initialWidth: DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH, - esTypes: ['date'], - type: 'date', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'message', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH * 2, - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'event.category', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'event.action', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'host.name', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'source.ip', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'destination.ip', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'user.name', - }, -]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx index 9703efd8d5bb3..c660893ba379e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx @@ -32,7 +32,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_ex import { TimelineTabs } from '@kbn/securitysolution-data-table'; import { DataLoadingState } from '@kbn/unified-data-table'; import { getColumnHeaders } from '../body/column_headers/helpers'; -import { defaultUdtHeaders } from './default_headers'; +import { defaultUdtHeaders } from '../body/column_headers/default_headers'; import type { ColumnHeaderType } from '../../../../../common/types'; jest.mock('../../../containers', () => ({ @@ -45,10 +45,6 @@ jest.mock('../../fields_browser', () => ({ useFieldBrowserOptions: jest.fn(), })); -jest.mock('../body/events', () => ({ - Events: () => <>, -})); - jest.mock('../../../../sourcerer/containers'); jest.mock('../../../../sourcerer/containers/use_signal_helpers', () => ({ useSignalHelpers: () => ({ signalIndexNeedsInit: false }), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx index 7d89da9002ba8..112886f93ca32 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx @@ -31,7 +31,6 @@ import { withDataView } from '../../../../common/components/with_data_view'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; import type { TimelineItem } from '../../../../../common/search_strategy'; import { useKibana } from '../../../../common/lib/kibana'; -import { defaultHeaders } from '../body/column_headers/default_headers'; import type { ColumnHeaderOptions, OnChangePage, @@ -47,7 +46,7 @@ import { TimelineResizableLayout } from './resizable_layout'; import TimelineDataTable from './data_table'; import { timelineActions } from '../../../store'; import { getFieldsListCreationOptions } from './get_fields_list_creation_options'; -import { defaultUdtHeaders } from './default_headers'; +import { defaultUdtHeaders } from '../body/column_headers/default_headers'; import { getTimelineShowStatusByIdSelector } from '../../../store/selectors'; const TimelineBodyContainer = styled.div.attrs(({ className = '' }) => ({ @@ -291,7 +290,7 @@ const UnifiedTimelineComponent: React.FC = ({ (columnId: string) => { dispatch( timelineActions.upsertColumn({ - column: getColumnHeader(columnId, defaultHeaders), + column: getColumnHeader(columnId, defaultUdtHeaders), id: timelineId, index: 1, }) diff --git a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx index e8e2fe9dbbadf..a4c054371a316 100644 --- a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx @@ -18,7 +18,7 @@ import { appActions } from '../../common/store/app'; import { SourcererScopeName } from '../../sourcerer/store/model'; import { InputsModelId } from '../../common/store/inputs/constants'; import { TestProviders, mockGlobalState } from '../../common/mock'; -import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers'; jest.mock('../../common/components/discover_in_timeline/use_discover_in_timeline_context'); jest.mock('../../common/containers/use_global_time', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 527f372c1a447..2f80e969cab5e 100644 --- a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -19,7 +19,7 @@ import { SourcererScopeName } from '../../sourcerer/store/model'; import { appActions } from '../../common/store/app'; import type { TimeRange } from '../../common/store/inputs/model'; import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context'; -import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers'; import { timelineDefaults } from '../store/defaults'; export interface UseCreateTimelineParams { diff --git a/x-pack/plugins/security_solution/public/timelines/store/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/defaults.ts index dd9b811e144e8..e4fd97cd50534 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/defaults.ts @@ -12,10 +12,9 @@ import { RowRendererIdEnum, } from '../../../common/api/timeline'; -import { defaultHeaders } from '../components/timeline/body/column_headers/default_headers'; import { normalizeTimeRange } from '../../common/utils/normalize_time_range'; import type { SubsetTimelineModel, TimelineModel } from './model'; -import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers'; +import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers'; // normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false); @@ -109,7 +108,7 @@ export const timelineDefaults: SubsetTimelineModel & }; export const getTimelineManageDefaults = (id: string) => ({ - defaultColumns: defaultHeaders, + defaultColumns: defaultUdtHeaders, documentType: '', selectAll: false, id, diff --git a/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts index 4503d1026d7c8..b0067d6d0d9f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts @@ -18,7 +18,10 @@ import type { DataProvidersAnd, } from '../components/timeline/data_providers/data_provider'; import { IS_OPERATOR } from '../components/timeline/data_providers/data_provider'; -import { defaultColumnHeaderType } from '../components/timeline/body/column_headers/default_headers'; +import { + defaultUdtHeaders, + defaultColumnHeaderType, +} from '../components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, RESIZED_COLUMN_MIN_WITH, @@ -50,7 +53,6 @@ import type { TimelineModel } from './model'; import { timelineDefaults } from './defaults'; import type { TimelineById } from './types'; import { Direction } from '../../../common/search_strategy'; -import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers'; jest.mock('../../common/utils/normalize_time_range'); jest.mock('../../common/utils/default_date_settings', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/store/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/helpers.ts index b876465449740..a9566c22a814a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/helpers.ts @@ -10,7 +10,6 @@ import { v4 as uuidv4 } from 'uuid'; import type { Filter } from '@kbn/es-query'; import type { SessionViewConfig } from '../../../common/types'; import type { TimelineNonEcsData } from '../../../common/search_strategy'; -import type { Sort } from '../components/timeline/body/sort'; import type { DataProvider, QueryOperator, @@ -23,15 +22,16 @@ import { TimelineStatusEnum, TimelineTypeEnum, } from '../../../common/api/timeline'; +import { TimelineId } from '../../../common/types/timeline'; import type { ColumnHeaderOptions, TimelineEventsType, SerializedFilterQuery, TimelinePersistInput, SortColumnTimeline, + SortColumnTimeline as Sort, } from '../../../common/types/timeline'; import type { RowRendererId, TimelineType } from '../../../common/api/timeline'; -import { TimelineId } from '../../../common/types/timeline'; import { normalizeTimeRange } from '../../common/utils/normalize_time_range'; import { getTimelineManageDefaults, timelineDefaults } from './defaults'; import type { KqlMode, TimelineModel } from './model'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d7dfcc8ba4072..d3efeff9f821c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -40230,8 +40230,6 @@ "xpack.securitySolution.timeline.descriptionTooltip": "Description", "xpack.securitySolution.timeline.destination": "Destination", "xpack.securitySolution.timeline.EqlQueryBarLabel": "Requête EQL", - "xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly": "L'événement de la ligne {row} possède un outil de rendu d'événement. Appuyez sur Maj + flèche vers le bas pour faire la mise au point dessus.", - "xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly": "L'événement de la ligne {row} possède {notesCount, plural, =1 {une note} other {{notesCount} des notes}}. Appuyez sur Maj + flèche vers la droite pour faire la mise au point sur les notes.", "xpack.securitySolution.timeline.eventRenderersSwitch.title": "Outils de rendu d'événement", "xpack.securitySolution.timeline.eventRenderersSwitch.warning": "L'activation des outils de rendu d'événement peut avoir un impact sur les performances de la table.", "xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "Épingler la sélection", @@ -40287,10 +40285,6 @@ "xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "Cliquer pour synchroniser la plage temporelle des requêtes avec la plage temporelle de la page actuelle.", "xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "Modèle sans titre", "xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "Chronologie sans titre", - "xpack.securitySolution.timeline.rangePicker.oneDay": "1 jour", - "xpack.securitySolution.timeline.rangePicker.oneMonth": "1 mois", - "xpack.securitySolution.timeline.rangePicker.oneWeek": "1 semaine", - "xpack.securitySolution.timeline.rangePicker.oneYear": "1 an", "xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "Retirer des favoris", "xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "Modifications non enregistrées", "xpack.securitySolution.timeline.saveStatus.unsavedLabel": "Non enregistré", @@ -40340,7 +40334,6 @@ "xpack.securitySolution.timeline.userDetails.managed.description": "Les métadonnées de toutes les intégrations de référentiel de ressource sont autorisées dans votre environnement.", "xpack.securitySolution.timeline.userDetails.updatedTime": "Mis à jour le {time}", "xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "Vous êtes dans un outil de rendu d'événement pour la ligne : {row}. Appuyez sur la touche fléchée vers le haut pour quitter et revenir à la ligne en cours, ou sur la touche fléchée vers le bas pour quitter et passer à la ligne suivante.", - "xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "Vous êtes dans une cellule de tableau. Ligne : {row}, colonne : {column}", "xpack.securitySolution.timelineEvents.errorSearchDescription": "Une erreur s'est produite lors de la recherche d'événements de la chronologie", "xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "Impossible d'interroger les données de toutes les chronologies", "xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "Importer", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 656e6f844f0c3..d026751779628 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -39974,8 +39974,6 @@ "xpack.securitySolution.timeline.descriptionTooltip": "説明", "xpack.securitySolution.timeline.destination": "送信先", "xpack.securitySolution.timeline.EqlQueryBarLabel": "EQL クエリ", - "xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly": "行{row}のイベントにはイベントレンダラーがあります。Shiftと下矢印を押すとフォーカスします。", - "xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly": "行{row}のイベントには{notesCount, plural, other {{notesCount}個のメモ}}があります。Shiftと右矢印を押すとメモをフォーカスします。", "xpack.securitySolution.timeline.eventRenderersSwitch.title": "イベントレンダラー", "xpack.securitySolution.timeline.eventRenderersSwitch.warning": "イベントレンダリングを有効化すると、テーブルパフォーマンスに影響する可能性があります。", "xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "選択項目にピン付け", @@ -40031,10 +40029,6 @@ "xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "クリックすると、クエリの時間範囲と現在のページの時間範囲を同期します。", "xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "無題のテンプレート", "xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "無題のタイムライン", - "xpack.securitySolution.timeline.rangePicker.oneDay": "1日", - "xpack.securitySolution.timeline.rangePicker.oneMonth": "1 か月", - "xpack.securitySolution.timeline.rangePicker.oneWeek": "1 週間", - "xpack.securitySolution.timeline.rangePicker.oneYear": "1 年", "xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "お気に入りから削除", "xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "保存されていない変更", "xpack.securitySolution.timeline.saveStatus.unsavedLabel": "未保存", @@ -40084,7 +40078,6 @@ "xpack.securitySolution.timeline.userDetails.managed.description": "環境で有効になっているアセットリポジトリ統合からのメタデータ。", "xpack.securitySolution.timeline.userDetails.updatedTime": "更新日時{time}", "xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "行 {row} のイベントレンダラーを表示しています。上矢印キーを押すと、終了して現在の行に戻ります。下矢印キーを押すと、終了して次の行に進みます。", - "xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "表セルの行 {row}、列 {column} にいます", "xpack.securitySolution.timelineEvents.errorSearchDescription": "タイムラインイベント検索でエラーが発生しました", "xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "すべてのタイムラインデータをクエリできませんでした", "xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "インポート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d3341c98103cb..79c35dff0e021 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -40020,8 +40020,6 @@ "xpack.securitySolution.timeline.descriptionTooltip": "描述", "xpack.securitySolution.timeline.destination": "目标", "xpack.securitySolution.timeline.EqlQueryBarLabel": "EQL 查询", - "xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly": "位于行 {row} 的事件具有事件呈现程序。按 shift + 向下箭头键以对其聚焦。", - "xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly": "位于行 {row} 的事件有{notesCount, plural, =1 {备注} other { {notesCount} 个备注}}。按 shift + 右箭头键以聚焦备注。", "xpack.securitySolution.timeline.eventRenderersSwitch.title": "事件呈现器", "xpack.securitySolution.timeline.eventRenderersSwitch.warning": "启用事件呈现器可能会影响表性能。", "xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "固定所选", @@ -40077,10 +40075,6 @@ "xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "单击以将查询时间范围与当前页面的时间范围进行同步。", "xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "未命名模板", "xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "未命名时间线", - "xpack.securitySolution.timeline.rangePicker.oneDay": "1 天", - "xpack.securitySolution.timeline.rangePicker.oneMonth": "1 个月", - "xpack.securitySolution.timeline.rangePicker.oneWeek": "1 周", - "xpack.securitySolution.timeline.rangePicker.oneYear": "1 年", "xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "从收藏夹中移除", "xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "未保存的更改", "xpack.securitySolution.timeline.saveStatus.unsavedLabel": "未保存", @@ -40130,7 +40124,6 @@ "xpack.securitySolution.timeline.userDetails.managed.description": "在您的环境中启用的任何资产存储库集成中的元数据。", "xpack.securitySolution.timeline.userDetails.updatedTime": "已更新 {time}", "xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "您正处于第 {row} 行的事件呈现器中。按向上箭头键退出并返回当前行,或按向下箭头键退出并前进到下一行。", - "xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "您处在表单元格中。行:{row},列:{column}", "xpack.securitySolution.timelineEvents.errorSearchDescription": "搜索时间线事件时发生错误", "xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "无法查询所有时间线数据", "xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "导入", From 2c4f35290204ac887f2ab239e3a68e855e423768 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 7 Nov 2024 09:41:56 -0600 Subject: [PATCH 26/47] [ci] Remove defend workflows on serverless These shouldn't be running on 8.x --- .../security_solution/defend_workflows.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml b/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml index 104853c27b112..28cc4f2812b5a 100644 --- a/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml +++ b/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml @@ -18,20 +18,3 @@ steps: automatic: - exit_status: '-1' limit: 1 - - - command: .buildkite/scripts/steps/functional/defend_workflows_serverless.sh - label: 'Defend Workflows Cypress Tests on Serverless' - agents: - enableNestedVirtualization: true - localSsds: 1 - localSsdInterface: nvme - machineType: n2-standard-4 - depends_on: - - build - - quick_checks - timeout_in_minutes: 60 - parallelism: 14 - retry: - automatic: - - exit_status: '-1' - limit: 1 From 2fb4a328695c1eca53eef05c8401c3fff2ecad86 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Thu, 7 Nov 2024 19:02:39 +0300 Subject: [PATCH 27/47] [i18n] [8.x] Integrate 8.16.0 Translations (#199149) Integrating latest translations extracted from 8.x branch. Skipping backports from main to target branches since the `i18n_check` might trim unused translations that are still used in different branches. Integration script is ran against each target branch separately. --- src/dev/i18n_tools/bin/run_i18n_check.ts | 2 +- .../remove_outdated_translations.ts | 45 +- .../translations/translations/fr-FR.json | 4870 +++++++++++++---- .../translations/translations/ja-JP.json | 4571 +++++++++++++--- .../translations/translations/zh-CN.json | 4551 ++++++++++++--- 5 files changed, 11589 insertions(+), 2450 deletions(-) diff --git a/src/dev/i18n_tools/bin/run_i18n_check.ts b/src/dev/i18n_tools/bin/run_i18n_check.ts index ff00148ab3012..f25257c3ec51b 100644 --- a/src/dev/i18n_tools/bin/run_i18n_check.ts +++ b/src/dev/i18n_tools/bin/run_i18n_check.ts @@ -110,7 +110,7 @@ run( }, { title: 'Checking Untracked i18n Messages outside defined namespaces', - enabled: (_) => !ignoreUntracked || !!(filterNamespaces && filterNamespaces.length), + enabled: (_) => !ignoreUntracked && !!(filterNamespaces && filterNamespaces.length), task: (context, task) => checkUntrackedNamespacesTask(context, task, { rootPaths }), }, { diff --git a/src/dev/i18n_tools/tasks/validate_translation_files/remove_outdated_translations.ts b/src/dev/i18n_tools/tasks/validate_translation_files/remove_outdated_translations.ts index a4815c17cac91..e4f9aae6b5277 100644 --- a/src/dev/i18n_tools/tasks/validate_translation_files/remove_outdated_translations.ts +++ b/src/dev/i18n_tools/tasks/validate_translation_files/remove_outdated_translations.ts @@ -67,26 +67,31 @@ const removeOutdatedMessages = ( 'outdatedMessages' | 'updatedMessages', Array<[string, string | { message: string }]> > => { - const outdatedMessages: Array<[string, string | { message: string }]> = []; - let updatedMessages = translationMessages; + return translationMessages.reduce( + (acc, [translatedId, translatedMessage]) => { + const messageDescriptor = extractedMessages.find(({ id }) => id === translatedId); + // removed from codebase + if (!messageDescriptor) { + acc.outdatedMessages.push([translatedId, translatedMessage]); + return acc; + } - updatedMessages = translationMessages.filter(([translatedId, translatedMessage]) => { - const messageDescriptor = extractedMessages.find(({ id }) => id === translatedId); - if (!messageDescriptor?.hasValuesObject) { - return true; - } - try { - verifyMessageDescriptor( - typeof translatedMessage === 'string' ? translatedMessage : translatedMessage.message, - messageDescriptor - ); - return true; - } catch (err) { - outdatedMessages.push([translatedId, translatedMessage]); - // failed to verify message against latest descriptor. remove from file. - return false; - } - }); + try { + verifyMessageDescriptor( + typeof translatedMessage === 'string' ? translatedMessage : translatedMessage.message, + messageDescriptor + ); + acc.updatedMessages.push([translatedId, translatedMessage]); + } catch (err) { + // failed to verify message against latest descriptor. remove from file. + acc.outdatedMessages.push([translatedId, translatedMessage]); + } - return { updatedMessages, outdatedMessages }; + return acc; + }, + { updatedMessages: [], outdatedMessages: [] } as Record< + 'outdatedMessages' | 'updatedMessages', + Array<[string, string | { message: string }]> + > + ); }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d3efeff9f821c..03206b7d39659 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -1,5 +1,80 @@ { - "formats": {}, + "formats": { + "number": { + "currency": { + "style": "currency" + }, + "percent": { + "style": "percent" + } + }, + "date": { + "short": { + "month": "numeric", + "day": "numeric", + "year": "2-digit" + }, + "medium": { + "month": "short", + "day": "numeric", + "year": "numeric" + }, + "long": { + "month": "long", + "day": "numeric", + "year": "numeric" + }, + "full": { + "weekday": "long", + "month": "long", + "day": "numeric", + "year": "numeric" + } + }, + "time": { + "short": { + "hour": "numeric", + "minute": "numeric" + }, + "medium": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric" + }, + "long": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + }, + "full": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + } + }, + "relative": { + "years": { + "style": "long" + }, + "months": { + "style": "long" + }, + "days": { + "style": "long" + }, + "hours": { + "style": "long" + }, + "minutes": { + "style": "long" + }, + "seconds": { + "style": "long" + } + } + }, "messages": { "advancedSettings.advancedSettingsLabel": "Paramètres avancés", "advancedSettings.featureCatalogueTitle": "Personnalisez votre expérience Kibana : modifiez le format de date, activez le mode sombre, et bien plus encore.", @@ -8,7 +83,7 @@ "aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.manageSettingsButtonLabel": "Gérer les paramètres", "aiAssistantManagementSelection.aiAssistantSelectionPage.observabilityAi.thisFeatureIsDisabledCallOutLabel": "Cette fonctionnalité est désactivée.", "aiAssistantManagementSelection.aiAssistantSelectionPage.observabilityLabel": "Assistant d'IA Elastic pour Observability", - "aiAssistantManagementSelection.aiAssistantSelectionPage.securityAi.thisFeatureIsDisabledCallOutLabel": "Cette fonctionnalité est désactivée. Elle peut être activée dans Espaces > Fonctionnalités.", + "aiAssistantManagementSelection.aiAssistantSelectionPage.securityAi.thisFeatureIsDisabledCallOutLabel": "Cette fonctionnalité est désactivée. Vous pouvez l'activer à partir de Espaces > Fonctionnalités.", "aiAssistantManagementSelection.aiAssistantSelectionPage.securityAssistant.documentationLinkDescription": "Pour en savoir plus, consultez notre {documentation}.", "aiAssistantManagementSelection.aiAssistantSelectionPage.securityAssistant.manageSettingsButtonLabel": "Gérer les paramètres", "aiAssistantManagementSelection.aiAssistantSelectionPage.securityLabel": "Assistant d'IA Elastic pour Security", @@ -27,6 +102,41 @@ "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "Nulle part", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability": "Partout", "alertingTypes.builtinActionGroups.recovered": "Récupéré", + "alertsGrouping.unit": "{totalCount, plural, =1 {alerte} other {alertes}}", + "alertsUIShared.actiActionsonNotifyWhen.forEachOption": "Pour chaque alerte", + "alertsUIShared.actiActionsonNotifyWhen.summaryOption": "Résumé des alertes", + "alertsUIShared.actionForm.actionGroupRecoveredMessage": "Récupéré", + "alertsUIShared.actionVariables.alertActionGroupLabel": "Groupe d'actions de l'alerte ayant programmé les actions pour la règle.", + "alertsUIShared.actionVariables.alertActionGroupNameLabel": "Nom lisible par l'utilisateur du groupe d'actions de l'alerte ayant programmé les actions pour la règle.", + "alertsUIShared.actionVariables.alertConsecutiveMatchesLabel": "Le nombre de courses consécutives qui remplissent les conditions de la règle.", + "alertsUIShared.actionVariables.alertFlappingLabel": "Indicateur sur l'alerte spécifiant si le statut de l'alerte change fréquemment.", + "alertsUIShared.actionVariables.alertIdLabel": "ID de l'alerte ayant programmé les actions pour la règle.", + "alertsUIShared.actionVariables.alertUuidLabel": "UUID de l'alerte ayant programmé les actions pour la règle.", + "alertsUIShared.actionVariables.allAlertsCountLabel": "Décompte de toutes les alertes.", + "alertsUIShared.actionVariables.allAlertsDataLabel": "Tableau d'objets pour toutes les alertes.", + "alertsUIShared.actionVariables.dateLabel": "Date à laquelle la règle a programmé l'action.", + "alertsUIShared.actionVariables.kibanaBaseUrlLabel": "Valeur server.publicBaseUrl configurée ou chaîne vide si elle n'est pas configurée.", + "alertsUIShared.actionVariables.legacyAlertActionGroupLabel": "Cet élément a été déclassé au profit de {variable}.", + "alertsUIShared.actionVariables.legacyAlertActionGroupNameLabel": "Cet élément a été déclassé au profit de {variable}.", + "alertsUIShared.actionVariables.legacyAlertIdLabel": "Cet élément a été déclassé au profit de {variable}.", + "alertsUIShared.actionVariables.legacyAlertInstanceIdLabel": "Cet élément a été déclassé au profit de {variable}.", + "alertsUIShared.actionVariables.legacyAlertNameLabel": "Cet élément a été déclassé au profit de {variable}.", + "alertsUIShared.actionVariables.legacyParamsLabel": "Cet élément a été déclassé au profit de {variable}.", + "alertsUIShared.actionVariables.legacySpaceIdLabel": "Cet élément a été déclassé au profit de {variable}.", + "alertsUIShared.actionVariables.legacyTagsLabel": "Cet élément a été déclassé au profit de {variable}.", + "alertsUIShared.actionVariables.newAlertsCountLabel": "Décompte des nouvelles alertes.", + "alertsUIShared.actionVariables.newAlertsDataLabel": "Tableau d'objets pour les nouvelles alertes.", + "alertsUIShared.actionVariables.ongoingAlertsCountLabel": "Décompte des alertes en cours.", + "alertsUIShared.actionVariables.ongoingAlertsDataLabel": "Tableau d'objets pour les alertes en cours.", + "alertsUIShared.actionVariables.recoveredAlertsCountLabel": "Décompte des alertes récupérées.", + "alertsUIShared.actionVariables.recoveredAlertsDataLabel": "Tableau d'objets pour les alertes récupérées.", + "alertsUIShared.actionVariables.ruleIdLabel": "ID de la règle.", + "alertsUIShared.actionVariables.ruleNameLabel": "Nom de la règle.", + "alertsUIShared.actionVariables.ruleParamsLabel": "Les paramètres de la règle.", + "alertsUIShared.actionVariables.ruleSpaceIdLabel": "ID d'espace de la règle.", + "alertsUIShared.actionVariables.ruleTagsLabel": "Balises de la règle.", + "alertsUIShared.actionVariables.ruleTypeLabel": "Type de règle.", + "alertsUIShared.actionVariables.ruleUrlLabel": "L'URL d'accès à la règle qui a généré l'alerte. La chaîne sera vide si server.publicBaseUrl n'est pas configuré.", "alertsUIShared.alertFieldsTable.field": "Champ", "alertsUIShared.alertFieldsTable.filter.placeholder": "Filtre par Champ, Valeur ou Description...", "alertsUIShared.alertFieldsTable.value": "Valeur", @@ -34,6 +144,8 @@ "alertsUIShared.alertFilterControls.defaultControlDisplayNames.rule": "Règle", "alertsUIShared.alertFilterControls.defaultControlDisplayNames.status": "Statut", "alertsUIShared.alertFilterControls.defaultControlDisplayNames.tags": "Balises", + "alertsUIShared.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "Ce connecteur est désactivé par la configuration de Kibana.", + "alertsUIShared.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "Ce connecteur requiert une licence {minimumLicenseRequired}.", "alertsUIShared.component.alertsSearchBar.placeholder": "Alertes de recherche (par exemple, kibana.alert.evaluation.threshold > 75)", "alertsUIShared.components.addMessageVariables.addRuleVariableTitle": "Ajouter une variable", "alertsUIShared.components.addMessageVariables.addVariablePopoverButton": "Ajouter une variable", @@ -54,10 +166,11 @@ "alertsUIShared.components.ruleTypeModal.noRuleTypesErrorTitle": "Aucun type de règles trouvé", "alertsUIShared.components.ruleTypeModal.searchPlaceholder": "Recherche", "alertsUIShared.components.ruleTypeModal.title": "Sélectionner le type de règle", + "alertsUIShared.disabledActionsWarningTitle": "Cette règle possède des actions qui sont désactivées", "alertsUIShared.filterGroup.contextMenu.reset": "Réinitialiser les contrôles", "alertsUIShared.filterGroup.contextMenu.resetTooltip": "Réinitialiser les contrôles aux paramètres d'usine", "alertsUIShared.filterGroup.filtersChangedBanner": "Les contrôles de filtre ont changé", - "alertsUIShared.filterGroup.filtersChangedTitle": "Les nouveaux contrôles de filtre de cette page sont différents de ceux que vous avez précédemment enregistrés. Vous pouvez enregistrer les modifications ou les ignorer.\n Si vous quittez cette fenêtre, ces modifications seront automatiquement ignorées", + "alertsUIShared.filterGroup.filtersChangedTitle": "Les nouveaux contrôles de filtre de cette page sont différents de ceux que vous avez précédemment enregistrés. Vous pouvez enregistrer les modifications ou les ignorer. Si vous quittez cette fenêtre, ces modifications seront automatiquement ignorées", "alertsUIShared.filterGroup.groupMenuTitle": "Menu de groupe de filtres", "alertsUIShared.filtersGroup.contextMenu.addControls": "Ajouter des contrôles", "alertsUIShared.filtersGroup.contextMenu.addControls.maxLimit": "Un maximum de 4 contrôles peut être ajouté.", @@ -68,8 +181,24 @@ "alertsUIShared.filtersGroup.discardChanges": "Abandonner les modifications", "alertsUIShared.filtersGroup.pendingChanges": "Enregistrer les modifications en attente", "alertsUIShared.filtersGroup.urlParam.arrayError": "Les paramètres d'URL du filtre de page doivent être un tableau", + "alertsUIShared.healthCheck.actionText": "En savoir plus.", + "alertsUIShared.healthCheck.alertsErrorText": "Pour créer une règle, vous devez activer les plug-ins d'alerting et d'actions.", + "alertsUIShared.healthCheck.alertsErrorTitle": "Vous devez activer Alerting et Actions", + "alertsUIShared.healthCheck.apiKeysAndEncryptionErrorText": "Vous devez activer les clés d'API et configurer une clé de chiffrement pour utiliser Alerting.", + "alertsUIShared.healthCheck.apiKeysDisabledErrorText": "Vous devez activer les clés d'API pour utiliser Alerting.", + "alertsUIShared.healthCheck.apiKeysDisabledErrorTitle": "Configuration supplémentaire requise", + "alertsUIShared.healthCheck.encryptionErrorText": "Vous devez configurer une clé de chiffrement pour utiliser Alerting.", + "alertsUIShared.healthCheck.encryptionErrorTitle": "Configuration supplémentaire requise", + "alertsUIShared.healthCheck.healthCheck.apiKeysAndEncryptionErrorTitle": "Configuration supplémentaire requise", + "alertsUIShared.hooks.useAlertDataView.fetchErrorMessage": "Impossible de charger la vue des données de l'alerte", + "alertsUIShared.hooks.useFindAlertsQuery.unableToFetchAlertsGroupingAggregations": "Impossible de récupérer les agrégations de groupement d'alertes", + "alertsUIShared.hooks.useFindAlertsQuery.unableToFindAlertsQueryMessage": "Impossible de trouver les alertes", "alertsUIShared.hooks.useLoadRuleTypesQuery.unableToLoadRuleTypesMessage": "Impossible de charger les types de règles", "alertsUIShared.hooks.useRuleAADFields.errorMessage": "Impossible de charger les champs d'alerte par type de règle", + "alertsUIShared.licenseCheck.actionTypeDisabledByConfigMessageTitle": "Cette fonctionnalité est désactivée par la configuration de Kibana.", + "alertsUIShared.licenseCheck.actionTypeDisabledByLicenseLinkTitle": "Afficher les options de licence", + "alertsUIShared.licenseCheck.actionTypeDisabledByLicenseMessageDescription": "Pour réactiver cette action, veuillez mettre à niveau votre licence.", + "alertsUIShared.licenseCheck.actionTypeDisabledByLicenseMessageTitle": "Cette fonctionnalité requiert une licence {minimumLicenseRequired}.", "alertsUIShared.maintenanceWindowCallout.fetchError": "La vérification visant à déterminer si les fenêtres de maintenance sont actives a échoué", "alertsUIShared.maintenanceWindowCallout.fetchErrorDescription": "Les notifications de règle sont arrêtées lorsque les fenêtres de maintenance sont en cours d'exécution.", "alertsUIShared.maintenanceWindowCallout.maintenanceWindowActive": "{activeWindowCount, plural, one {Une fenêtre de maintenance est} other {Des fenêtres de maintenance sont}} en cours d'exécution pour des règles de {categories}", @@ -89,20 +218,74 @@ "alertsUIShared.producerDisplayNames.slo": "SLO", "alertsUIShared.producerDisplayNames.stackAlerts": "Alertes de la suite", "alertsUIShared.producerDisplayNames.uptime": "Synthetics et Uptime", - "alertsUIShared.ruleForm.error.belowMinimumAlertDelayText": "Le délai d’alerte doit être supérieur à 1.", + "alertsUIShared.ruleActionsAlertsFilter.ActionAlertsFilterQueryPlaceholder": "Filtrer les alertes à l'aide de la syntaxe KQL", + "alertsUIShared.ruleActionsAlertsFilter.ActionAlertsFilterQueryToggleLabel": "Si l'alerte correspond à une requête", + "alertsUIShared.ruleActionsItem.actionErrorToolTip": "L’action contient des erreurs.", + "alertsUIShared.ruleActionsItem.actionUnableToLoadConnectorTitle": "Créez un connecteur et réessayez. Si vous ne parvenez pas à créer un connecteur, contactez votre administrateur système.", + "alertsUIShared.ruleActionsItem.actionUseAadTemplateFieldsLabel": "Utiliser les champs de modèle de l'index des alertes", + "alertsUIShared.ruleActionsItem.actionWarningsTitle": "1 avertissement", + "alertsUIShared.ruleActionsItem.existingAlertActionTypeEditTitle": "{actionConnectorName}", + "alertsUIShared.ruleActionsItem.runWhenGroupTitle": "Exécuter lorsque {groupName}", + "alertsUIShared.ruleActionsItem.summaryGroupTitle": "Résumé des alertes", + "alertsUIShared.ruleActionsNotifyWhen.actionFrequencyLabel": "Fréquence d'action", + "alertsUIShared.ruleActionsNotifyWhen.frequencyNotifyWhen.label": "Exécuter chaque", + "alertsUIShared.ruleActionsNotifyWhen.notifyWhenThrottleWarning": "Les intervalles d'action personnalisés ne peuvent pas être plus courts que l'intervalle de vérification de la règle", + "alertsUIShared.ruleActionsNotifyWhen.onActionGroupChange.description": "Actions exécutées si le statut de l'alerte change.", + "alertsUIShared.ruleActionsNotifyWhen.onActionGroupChange.display": "Lors de changements de statut", + "alertsUIShared.ruleActionsNotifyWhen.onActionGroupChange.label": "Lors de changements de statut", + "alertsUIShared.ruleActionsNotifyWhen.onActiveAlert.description": "Les actions sont exécutées si les conditions de règle sont remplies.", + "alertsUIShared.ruleActionsNotifyWhen.onActiveAlert.display": "Selon les intervalles de vérification", + "alertsUIShared.ruleActionsNotifyWhen.onActiveAlert.label": "Selon les intervalles de vérification", + "alertsUIShared.ruleActionsNotifyWhen.onThrottleInterval.description": "Les actions sont exécutées si les conditions de règle sont remplies.", + "alertsUIShared.ruleActionsNotifyWhen.onThrottleInterval.display": "Selon des intervalles d'action personnalisés", + "alertsUIShared.ruleActionsNotifyWhen.onThrottleInterval.label": "Selon des intervalles d'action personnalisés", + "alertsUIShared.ruleActionsNotifyWhen.summaryOrRulePerSelectRoleDescription": "Sélection du type de fréquence d'action", + "alertsUIShared.ruleActionsSetting.actionGroupNotSupported": "{actionGroupName} (non pris en charge actuellement)", + "alertsUIShared.ruleActionsSetting.actionGroupRunWhen": "Exécuter quand", + "alertsUIShared.ruleActionsSystemActionsItem.deleteActionAriaLabel": "supprimer l'action", + "alertsUIShared.ruleForm.actionsForm.publicBaseUrl": "server.publicBaseUrl n'est pas défini. Les URL générées seront relatives ou vides.", + "alertsUIShared.ruleForm.actionsForm.requiredFilterQuery": "Une requête personnalisée est requise.", + "alertsUIShared.ruleForm.actionTypeModalEmptyText": "Essayez une autre recherche ou modifiez vos paramètres de filtrage.", + "alertsUIShared.ruleForm.actionTypeModalEmptyTitle": "Aucun connecteur trouvé", + "alertsUIShared.ruleForm.actionTypeModalFilterAll": "Tous", + "alertsUIShared.ruleForm.actionTypeModalTitle": "Sélectionner un connecteur", + "alertsUIShared.ruleForm.circuitBreakerHideFullErrorText": "Masquer l'erreur en intégralité", + "alertsUIShared.ruleForm.circuitBreakerSeeFullErrorText": "Afficher l'erreur en intégralité", + "alertsUIShared.ruleForm.confirmRuleSaveCancelButtonText": "Annuler", + "alertsUIShared.ruleForm.confirmRuleSaveConfirmButtonText": "Enregistrer la règle", + "alertsUIShared.ruleForm.confirmRuleSaveMessageText": "Vous pouvez ajouter une action à tout moment.", + "alertsUIShared.ruleForm.confirmRuleSaveTitle": "Enregistrer la règle ne contenant aucune action ?", + "alertsUIShared.ruleForm.createErrorText": "Impossible de créer une règle.", + "alertsUIShared.ruleForm.createSuccessText": "Création de la règle \"{ruleName}\" effectuée", + "alertsUIShared.ruleForm.editErrorText": "Impossible de mettre à jour la règle.", + "alertsUIShared.ruleForm.editSuccessText": "Mise à jour de \"{ruleName}\" effectuée", + "alertsUIShared.ruleForm.error.belowMinimumAlertDelayText": "Le délai d'alerte doit être supérieur ou égal à 1.", "alertsUIShared.ruleForm.error.belowMinimumText": "L'intervalle doit être au minimum de {minimum}.", "alertsUIShared.ruleForm.error.requiredConsumerText": "La portée est requise.", "alertsUIShared.ruleForm.error.requiredIntervalText": "L'intervalle de vérification est requis.", "alertsUIShared.ruleForm.error.requiredNameText": "Le nom est requis.", "alertsUIShared.ruleForm.error.requiredRuleTypeIdText": "Le type de règle est requis.", "alertsUIShared.ruleForm.intervalWarningText": "Des intervalles inférieurs à {minimum} ne sont pas recommandés pour des raisons de performances.", + "alertsUIShared.ruleForm.modalSearchClearFiltersText": "Effacer les filtres", + "alertsUIShared.ruleForm.modalSearchPlaceholder": "Recherche", + "alertsUIShared.ruleForm.returnTitle": "Renvoyer", + "alertsUIShared.ruleForm.routeParamsErrorText": "Une erreur s'est produite lors du chargement du formulaire de la règle. Veuillez vérifier que le chemin est correct.", + "alertsUIShared.ruleForm.routeParamsErrorTitle": "Impossible de charger le formulaire de règle", "alertsUIShared.ruleForm.ruleActions.addActionText": "Ajouter une action", + "alertsUIShared.ruleForm.ruleActionsAlertsFilterTimeframeTimezoneLabel": "Fuseau horaire", + "alertsUIShared.ruleForm.ruleActionsAlertsFilterTimeframeToggleLabel": "Si l'alerte est générée pendant l'intervalle de temps", + "alertsUIShared.ruleForm.ruleActionsAlertsFilterTimeframeWeekdays": "Jours de la semaine", + "alertsUIShared.ruleForm.ruleActionsNoPermissionDescription": "Pour modifier les règles, vous devez avoir un accès en lecture aux actions et aux connecteurs.", + "alertsUIShared.ruleForm.ruleActionsNoPermissionTitle": "Privilèges manquants pour les actions et les connecteurs", + "alertsUIShared.ruleForm.ruleActionsTitle": "Actions", "alertsUIShared.ruleForm.ruleAlertDelay.alertDelayHelpText": "Une alerte n'est déclenchée que si le nombre spécifié d'exécutions consécutives remplit les conditions de la règle.", "alertsUIShared.ruleForm.ruleAlertDelay.alertDelayTitlePrefix": "Après une alerte", "alertsUIShared.ruleForm.ruleAlertDelay.alertDelayTitleSuffix": "correspondances consécutives", "alertsUIShared.ruleForm.ruleDefinition.advancedOptionsTitle": "Options avancées", "alertsUIShared.ruleForm.ruleDefinition.alertDelayDescription": "Définir le nombre d’exécutions consécutives pour lesquelles cette règle doit répondre aux conditions d'alerte avant qu'une alerte ne se déclenche", "alertsUIShared.ruleForm.ruleDefinition.alertDelayTitle": "Délai d'alerte", + "alertsUIShared.ruleForm.ruleDefinition.alertFlappingDetectionDescription": "Détectez les alertes qui passent rapidement de l'état actif à l'état récupéré et réduisez le bruit non souhaité de ces alertes instables", + "alertsUIShared.ruleForm.ruleDefinition.alertFlappingDetectionTitle": "Détection de bagotement d'alerte", "alertsUIShared.ruleForm.ruleDefinition.docLinkTitle": "Afficher la documentation", "alertsUIShared.ruleForm.ruleDefinition.loadingRuleTypeParamsTitle": "Chargement des paramètres de types de règles", "alertsUIShared.ruleForm.ruleDefinition.scheduleDescriptionText": "Définir la fréquence de vérification des conditions de l'alerte", @@ -110,10 +293,18 @@ "alertsUIShared.ruleForm.ruleDefinition.scheduleTooltipText": "Les vérifications sont mises en file d'attente ; elles seront exécutées au plus près de la valeur définie, en fonction de la capacité.", "alertsUIShared.ruleForm.ruleDefinition.scopeDescriptionText": "Sélectionnez les applications auxquelles associer le privilège de rôle correspondant", "alertsUIShared.ruleForm.ruleDefinition.scopeTitle": "Portée de la règle", + "alertsUIShared.ruleForm.ruleDefinitionTitle": "Définition de la règle", "alertsUIShared.ruleForm.ruleDetails.description": "Définissez un nom et des balises pour votre règle.", + "alertsUIShared.ruleForm.ruleDetails.ruleNameInputButtonAriaLabel": "Enregistrer le nom de la règle", "alertsUIShared.ruleForm.ruleDetails.ruleNameInputTitle": "Nom de règle", "alertsUIShared.ruleForm.ruleDetails.ruleTagsInputTitle": "Balises", + "alertsUIShared.ruleForm.ruleDetails.ruleTagsPlaceholder": "Ajouter des balises", "alertsUIShared.ruleForm.ruleDetails.title": "Nom et balises de la règle", + "alertsUIShared.ruleForm.ruleDetailsTitle": "Détails de la règle", + "alertsUIShared.ruleForm.ruleFormCancelModalCancel": "Annuler", + "alertsUIShared.ruleForm.ruleFormCancelModalConfirm": "Abandonner les modifications", + "alertsUIShared.ruleForm.ruleFormCancelModalDescription": "Vous ne pouvez pas récupérer de modifications non enregistrées.", + "alertsUIShared.ruleForm.ruleFormCancelModalTitle": "Abandonner les modifications non enregistrées apportées à la règle ?", "alertsUIShared.ruleForm.ruleFormConsumerSelection.apm": "APM et expérience utilisateur", "alertsUIShared.ruleForm.ruleFormConsumerSelection.consumerSelectComboBoxTitle": "Sélectionner une portée", "alertsUIShared.ruleForm.ruleFormConsumerSelection.consumerSelectTitle": "Visibilité du rôle", @@ -122,7 +313,53 @@ "alertsUIShared.ruleForm.ruleFormConsumerSelection.slo": "SLO", "alertsUIShared.ruleForm.ruleFormConsumerSelection.stackAlerts": "Règles de la Suite Elastic", "alertsUIShared.ruleForm.ruleFormConsumerSelection.uptime": "Synthetics et Uptime", + "alertsUIShared.ruleForm.ruleNotFoundErrorText": "Une erreur s'est produite lors du chargement de la règle. Veuillez vous assurer que la règle existe et que vous y avez accès.", + "alertsUIShared.ruleForm.ruleNotFoundErrorTitle": "Impossible de charger la règle", + "alertsUIShared.ruleForm.rulePage.ruleNameAriaLabelText": "Modifier le nom de la règle", + "alertsUIShared.ruleForm.rulePageFooter.cancelText": "Annuler", + "alertsUIShared.ruleForm.rulePageFooter.createText": "Créer une règle", + "alertsUIShared.ruleForm.rulePageFooter.saveText": "Enregistrer la règle", + "alertsUIShared.ruleForm.rulePageFooter.showRequestText": "Afficher la requête", "alertsUIShared.ruleForm.ruleSchedule.scheduleTitlePrefix": "Chaque", + "alertsUIShared.ruleForm.ruleTypeNotFoundErrorText": "Une erreur s'est produite lors du chargement du type de règle. Veuillez vous assurer que vous avez accès au type de règle sélectionné.", + "alertsUIShared.ruleForm.ruleTypeNotFoundErrorTitle": "Impossible de charger le type de règle", + "alertsUIShared.ruleForm.showRequestModal.headerTitle": "{requestType} requête de règle d'alerte", + "alertsUIShared.ruleForm.showRequestModal.headerTitleCreate": "Créer", + "alertsUIShared.ruleForm.showRequestModal.headerTitleEdit": "Modifier", + "alertsUIShared.ruleForm.showRequestModal.somethingWentWrongDescription": "Désolé, un problème est survenu.", + "alertsUIShared.ruleForm.showRequestModal.subheadingTitle": "La requête Kibana va {requestType} cette règle.", + "alertsUIShared.ruleForm.showRequestModal.subheadingTitleCreate": "créer", + "alertsUIShared.ruleForm.showRequestModal.subheadingTitleEdit": "modifier", + "alertsUIShared.ruleSettingsFlappingForm.flappingExternalLinkLabel": "Qu'est-ce que c'est ?", + "alertsUIShared.ruleSettingsFlappingForm.flappingLabel": "Détection des éléments instables", + "alertsUIShared.ruleSettingsFlappingForm.flappingOffContentRules": "Règles", + "alertsUIShared.ruleSettingsFlappingForm.flappingOffContentSettings": "Paramètres", + "alertsUIShared.ruleSettingsFlappingForm.flappingOffPopoverContent": "Accédez à {rules} > {settings} pour activer la détection d'instabilité pour l'ensemble des règles d'un espace. Vous pouvez ensuite personnaliser la période d'analyse et les valeurs seuils de chaque règle.", + "alertsUIShared.ruleSettingsFlappingForm.flappingOverrideConfiguration": "Personnaliser la configuration", + "alertsUIShared.ruleSettingsFlappingForm.offLabel": "DÉSACTIVÉ", + "alertsUIShared.ruleSettingsFlappingForm.onLabel": "ACTIVÉ", + "alertsUIShared.ruleSettingsFlappingForm.overrideLabel": "Personnalisé", + "alertsUIShared.ruleSettingsFlappingInputsProps.lookBackWindowHelp": "Nombre minimal d'exécutions pour lesquelles le seuil doit être atteint.", + "alertsUIShared.ruleSettingsFlappingInputsProps.lookBackWindowLabel": "Fenêtre d'historique d'exécution de la règle", + "alertsUIShared.ruleSettingsFlappingInputsProps.statusChangeThresholdHelp": "Nombre minimal de fois où une alerte doit changer d'état dans la fenêtre d'historique.", + "alertsUIShared.ruleSettingsFlappingInputsProps.statusChangeThresholdLabel": "Seuil de modification du statut d'alerte", + "alertsUIShared.ruleSettingsFlappingMessage.flappingOffMessage": "La détection de bagotement d'alerte est désactivée. Les alertes seront générées en fonction de l'intervalle de la règle, ce qui peut entraîner des volumes d'alertes plus importants.", + "alertsUIShared.ruleSettingsFlappingMessage.flappingSettingsDescription": "Cette règle détecte qu'une alerte est instable si son statut change au moins {statusChangeThreshold} au cours des derniers/dernières {lookBackWindow}.", + "alertsUIShared.ruleSettingsFlappingMessage.lookBackWindowLabelRuleRuns": "{amount, number} règle {amount, plural, one {exécute} other {exécutent}}", + "alertsUIShared.ruleSettingsFlappingMessage.spaceFlappingSettingsDescription": "L'ensemble des règles (de cet espace) détecte qu'une alerte est instable lorsque son statut change au moins {statusChangeThreshold} au cours des derniers/dernières {lookBackWindow}.", + "alertsUIShared.ruleSettingsFlappingMessage.statusChangeThresholdTimes": "{amount, number} {amount, plural, other {fois}}", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingOffContentRules": "Règles", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingOffContentSettings": "Paramètres", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopover1": "Lorsque la {flappingDetection} est activée, les alertes qui passent rapidement de l'état actif à l'état récupéré sont identifiées comme \"instables\" et les notifications sont réduites.", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopover2": "Le statut {alertStatus} définit une période (nombre minimum d'exécutions) utilisée dans l'algorithme de détection.", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopover3": "Le paramètre {lookBack} indique le nombre minimum de fois que les alertes doivent changer d'état au cours de la période seuil pour être considérées comme des alertes instables.", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopover4": "Accédez à {rules} > {settings} pour activer la détection d'instabilité pour l'ensemble des règles d'un espace. Vous pouvez ensuite personnaliser la période d'analyse et les valeurs seuils de chaque règle.", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverAlertStatus": "seuil de modification du statut d'alerte", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverFlappingDetection": "détection des éléments instables", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverLookBack": "fenêtre d'historique d'exécution de la règle", + "alertsUIShared.ruleSettingsFlappingTitleTooltip.tooltipTitle": "Détection de bagotement d'alerte", + "alertsUIShared.technicalPreviewBadgeDescription": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera de corriger tout problème, mais les fonctionnalités des versions d'évaluation technique ne sont pas soumises aux SLA de support des fonctionnalités officielles en disponibilité générale.", + "alertsUIShared.technicalPreviewBadgeLabel": "Version d'évaluation technique", "alertsUIShared.timeUnits.dayLabel": "{timeValue, plural, one {jour} other {jours}}", "alertsUIShared.timeUnits.hourLabel": "{timeValue, plural, one {heure} other {heures}}", "alertsUIShared.timeUnits.minuteLabel": "{timeValue, plural, one {minute} other {minutes}}", @@ -143,6 +380,11 @@ "autocomplete.seeDocumentation": "Consultez la documentation", "autocomplete.selectField": "Veuillez d'abord sélectionner un champ...", "autocomplete.showValueListModal": "Afficher la liste de valeurs", + "avcBanner.body": "Elastic Security passe avec brio le test de protection contre les malwares réalisé par AV-Comparatives", + "avcBanner.readTheBlog.link": "Lire le blog", + "avcBanner.title": "Protection à 100 % sans aucun faux positif.", + "bfetch.advancedSettings.disableBfetchCompressionDeprecation": "Ce paramètre est déclassé et sera supprimé dans la version 9.0 de Kibana.", + "bfetch.advancedSettings.disableBfetchDeprecation": "Ce paramètre est déclassé et sera supprimé dans la version 9.0 de Kibana.", "bfetch.disableBfetch": "Désactiver la mise en lots de requêtes", "bfetch.disableBfetchCompression": "Désactiver la compression par lots", "bfetch.disableBfetchCompressionDesc": "Vous pouvez désactiver la compression par lots. Cela permet de déboguer des requêtes individuelles, mais augmente la taille des réponses.", @@ -246,7 +488,7 @@ "cloud.deploymentDetails.modal.closeButtonLabel": "Fermer", "cloud.deploymentDetails.modal.learnMoreButtonLabel": "En savoir plus", "coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "Cette couleur sera automatiquement affectée au premier terme qui ne correspond pas à toutes les autres affectations", - "coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "Affecté automatiquement", + "coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "Terme d'affectation automatique", "coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "Supprimer cette affectation", "coloring.colorMapping.assignments.duplicateCategoryWarning": "Une autre couleur a déjà été affectée à cette catégorie. Seule la première affectation correspondante sera utilisée.", "coloring.colorMapping.colorChangesModal.categoricalModeDescription": "Basculer en mode de catégorie conduira à l'abandon de toutes vos modifications de couleurs personnalisées", @@ -277,7 +519,7 @@ "coloring.colorMapping.container.mapCurrentValuesButtonLabel": "Ajouter tous les termes non affectés", "coloring.colorMapping.container.mappingAssignmentHeader": "Affectations de couleurs", "coloring.colorMapping.container.mapValueButtonLabel": "Ajouter tous les termes non affectés", - "coloring.colorMapping.container.mapValuesPromptDescription.mapValuesPromptDetail": "Ajoutez de nouvelles affectations pour commencer à associer des termes de vos données à des couleurs spécifiques.", + "coloring.colorMapping.container.mapValuesPromptDescription.mapValuesPromptDetail": "Ajoutez une nouvelle affectation pour associer manuellement des termes à des couleurs spécifiées.", "coloring.colorMapping.container.OpenAdditionalActionsButtonLabel": "Ouvrir les actions d'affectation supplémentaires", "coloring.colorMapping.container.unassignedTermsMode.ReuseColorsLabel": "Palette de couleurs", "coloring.colorMapping.container.unassignedTermsMode.ReuseGradientLabel": "Gradient", @@ -324,10 +566,14 @@ "console.autocompleteSuggestions.endpointLabel": "point de terminaison", "console.autocompleteSuggestions.methodLabel": "méthode", "console.autocompleteSuggestions.paramLabel": "param", + "console.closeFullscreenButton": "Fermer l'affichage pleine page", "console.consoleDisplayName": "Console", "console.consoleMenu.copyAsCurlFailedMessage": "Impossible de copier la requête en tant que cURL", "console.consoleMenu.copyAsCurlMessage": "Requête copiée en tant que cURL", - "console.deprecations.enabled.manualStepOneMessage": "Ouvrez le fichier de configuration kibana.yml.", + "console.consoleMenu.copyAsFailedMessage": "{requestsCount, plural, one {Une requête n'a pas pu être copiée} other {Plusieurs requêtes n'ont pas pu être copiées}} dans le presse-papiers", + "console.consoleMenu.copyAsSuccessMessage": "{requestsCount, plural, one {Une requête copiée} other {Plusieurs requêtes copiées}} dans le presse-papiers en tant que {language}", + "console.consoleMenu.missingDocumentationPage": "La page de documentation n'est pas encore disponible pour cette API.", + "console.deprecations.enabled.manualStepOneMessage": "Ouvrir le fichier de configuration kibana.yml.", "console.deprecations.enabled.manualStepTwoMessage": "Remplacez le paramètre \"console.enabled\" par \"console.ui.enabled\".", "console.deprecations.enabledMessage": "Pour empêcher les utilisateurs d'accéder à l'interface utilisateur de la console, utilisez le paramètre \"console.ui.enabled\" au lieu de \"console.enabled\".", "console.deprecations.enabledTitle": "Le paramètre \"console.enabled\" est déclassé", @@ -343,12 +589,39 @@ "console.deprecations.proxyFilterTitle": "Le paramètre \"console.proxyFilter\" est déclassé", "console.devToolsDescription": "Plutôt que l’interface cURL, utilisez une interface JSON pour exploiter vos données dans la console.", "console.devToolsTitle": "Interagir avec l'API Elasticsearch", + "console.editor.adjustPanelSizeAriaLabel": "Utilisez les flèches gauche et droite pour ajuster la taille des panneaux", + "console.editor.clearConsoleInputButton": "Effacer cette entrée", + "console.editor.clearConsoleOutputButton": "Effacer cette sortie", "console.embeddableConsole.customScreenReaderAnnouncement": "Il y a un nouveau repère de région nommé {landmarkHeading} avec des commandes de niveau de page à la fin du document.", - "console.embeddableConsole.landmarkHeading": "Console développeur", + "console.embeddableConsole.landmarkHeading": "Console développeur. Appuyez sur Entrée pour modifier. Une fois terminé, appuyez sur Échap pour arrêter la modification.", "console.embeddableConsole.title": "Console", - "console.historyPage.applyHistoryButtonLabel": "Appliquer", - "console.historyPage.clearHistoryButtonLabel": "Effacer", + "console.exportButton": "Exporter les requêtes", + "console.exportButtonTooltipLabel": "Exporter toutes les requêtes de la console vers un fichier TXT", + "console.helpButtonTooltipContent": "Aide", + "console.helpPopover.aboutConsoleButtonAriaLabel": "À propos du lien de la console", + "console.helpPopover.aboutConsoleLabel": "À propos de la console", + "console.helpPopover.aboutQueryDSLButtonAriaLabel": "À propos du lien QueryDSL", + "console.helpPopover.aboutQueryDSLLabel": "À propos de Query DSL", + "console.helpPopover.description": "La console est une interface utilisateur interactive qui vous permet d'appeler les API Elasticsearch et Kibana et d'afficher leurs réponses. Avec la syntaxe Query DSL et REST API, recherchez vos données, gérez les paramètres et bien plus encore.", + "console.helpPopover.rerunTourButtonAriaLabel": "Bouton de réexécution de la présentation des fonctionnalités", + "console.helpPopover.rerunTourLabel": "Réexécution de la présentation des fonctionnalités", + "console.helpPopover.title": "Console Elastic", + "console.historyPage.addAndRunButtonLabel": "Ajouter et exécuter", + "console.historyPage.applyHistoryButtonLabel": "Ajouter", + "console.historyPage.clearHistoryButtonLabel": "Effacer l'ensemble de l'historique", + "console.historyPage.emptyPromptBody": "Ce panneau d'historique affichera toutes les requêtes passées que vous avez exécutées, cela vous permettra de les examiner et de les réutiliser.", + "console.historyPage.emptyPromptFooterLabel": "Envie d'en savoir plus ?", + "console.historyPage.emptyPromptFooterLink": "Lire la documentation de la console", + "console.historyPage.emptyPromptTitle": "Aucune requête pour le moment", + "console.historyPage.monaco.noHistoryTextMessage": "# Aucun historique à afficher", + "console.historyPage.pageDescription": "Revoyez et réutilisez vos requêtes antérieures", "console.historyPage.pageTitle": "Historique", + "console.importButtonLabel": "Importer les requêtes", + "console.importButtonTooltipLabel": "Importer des requêtes dans l'éditeur depuis un fichier", + "console.importConfirmModal.body": "L'importation de ce fichier remplacera toutes les requêtes actuelles dans l'éditeur.", + "console.importConfirmModal.cancelButton": "Annuler", + "console.importConfirmModal.confirmButton": "Importer et remplacer", + "console.importConfirmModal.title": "Importer et remplacer les requêtes ?", "console.keyboardCommandActionLabel.autoIndent": "Appliquer les indentations", "console.keyboardCommandActionLabel.moveToLine": "Déplacer le curseur sur une ligne", "console.keyboardCommandActionLabel.moveToNextRequestEdge": "Accéder au début ou à la fin de la requête suivante", @@ -358,34 +631,130 @@ "console.loadingError.buttonLabel": "Recharger la console", "console.loadingError.message": "Essayez de recharger pour obtenir les données les plus récentes.", "console.loadingError.title": "Impossible de charger la console", + "console.monaco.loadFromDataUnrecognizedUrlErrorMessage": "Seules les URL avec le domaine Elastic (www.elastic.co) peuvent être chargées dans la console.", + "console.monaco.loadFromDataUriErrorMessage": "Impossible de charger les données du paramètre de requête load_from dans l'URL", + "console.monaco.outputTextarea": "Outils de développement de la console - Sortie", + "console.monaco.requestOptions.autoIndentButtonLabel": "Retrait automatique", + "console.monaco.requestOptions.copyAsUrlButtonLabel": "Copier en tant que", + "console.monaco.requestOptions.openDocumentationButtonLabel": "Ouvrir la référence d'API", + "console.monaco.sendRequestButtonTooltipAriaLabel": "Cliquer pour envoyer la requête", + "console.monaco.sendRequestButtonTooltipContent": "Cliquer pour envoyer la requête", "console.notification.clearHistory": "Effacer l'historique", "console.notification.disableSavingToHistory": "Désactiver l'enregistrement", + "console.notification.error.failedToReadFile": "Impossible de lire le fichier sélectionné.", + "console.notification.error.fileImportNoContent": "Le fichier sélectionné ne semble pas avoir de contenu. Sélectionnez un autre fichier.", + "console.notification.error.fileTooBigMessage": "La taille du fichier dépasse la limite de 2 Mo.", + "console.notification.fileImportedSuccessfully": "Le fichier sélectionné a été importé avec succès.", + "console.notification.monaco.error.couldNotSaveRequestTitle": "Impossible d'enregistrer la requête dans l'historique de la console.", + "console.notification.monaco.error.historyQuotaReachedMessage": "L'historique des requêtes est arrivé à saturation. Effacez l'historique de la console ou désactivez l'enregistrement de nouvelles requêtes.", + "console.notification.monaco.error.nonSupportedRequest": "La requête sélectionnée n'est pas valide.", + "console.notification.monaco.error.noRequestSelectedTitle": "Aucune requête sélectionnée. Sélectionnez une requête en positionnant le curseur dessus.", + "console.notification.monaco.error.unknownErrorTitle": "Erreur de requête inconnue", + "console.openFullscreenButton": "Ouvrir cette console en affichage pleine page", + "console.outputEmptyState.description": "Lorsque vous exécutez une requête dans le panneau de saisie, la réponse de sortie s'affichera ici.", + "console.outputEmptyState.docsLink": "Lire la documentation de la Console", + "console.outputEmptyState.learnMore": "Envie d'en savoir plus ?", + "console.outputEmptyState.title": "Entrer une nouvelle requête", + "console.outputPanel.copyOutputButtonTooltipAriaLabel": "Cliquez pour copier dans le presse-papier", + "console.outputPanel.copyOutputButtonTooltipContent": "Cliquez pour copier dans le presse-papier", + "console.outputPanel.copyOutputToast": "Sortie sélectionnée copiée dans le presse-papiers", + "console.outputPanel.copyOutputToastFailedMessage": "Impossible de copier la sortie sélectionnée dans le presse-papiers", "console.pageHeading": "Console", "console.requestInProgressBadgeText": "Requête en cours", "console.requestOptions.autoIndentButtonLabel": "Appliquer les indentations", "console.requestOptions.copyAsUrlButtonLabel": "Copier la commande cURL", "console.requestOptions.openDocumentationButtonLabel": "Afficher la documentation", "console.requestOptionsButtonAriaLabel": "Options de requête", + "console.requestPanel.contextMenu.defaultSelectedLanguage": "Définir par défaut", + "console.requestPanel.contextMenu.languageSelectorModalCancel": "Annuler", + "console.requestPanel.contextMenu.languageSelectorModalCopy": "Copier le code", + "console.requestPanel.contextMenu.languageSelectorModalTitle": "Sélectionner une langue", "console.requestTimeElapasedBadgeTooltipContent": "Temps écoulé", + "console.settingsPage.autocompleteRefreshSettingsDescription": "La console actualise les suggestions de saisie semi-automatique en interrogeant Elasticsearch. Utilisez des actualisations moins fréquentes pour réduire les coûts de bande passante.", + "console.settingsPage.autocompleteRefreshSettingsLabel": "Actualisation de la saisie semi-automatique", + "console.settingsPage.autocompleteSettingsLabel": "Saisie semi-automatique", "console.settingsPage.dataStreamsLabelText": "Flux de données", - "console.settingsPage.enableAccessibilityOverlayLabel": "Activer la superposition d’accessibilité", - "console.settingsPage.enableKeyboardShortcutsLabel": "Activer les raccourcis clavier", + "console.settingsPage.displaySettingsLabel": "Affichage", + "console.settingsPage.enableAccessibilityOverlayLabel": "Superposition d’accessibilité", + "console.settingsPage.enableKeyboardShortcutsLabel": "Raccourcis clavier", "console.settingsPage.fieldsLabelText": "Champs", "console.settingsPage.fontSizeLabel": "Taille de la police", + "console.settingsPage.generalSettingsLabel": "Paramètres généraux", "console.settingsPage.indicesAndAliasesLabelText": "Index et alias", + "console.settingsPage.manualRefreshLabel": "Actualiser manuellement les suggestions de saisie semi-automatique", + "console.settingsPage.offLabel": "Désactivé", + "console.settingsPage.onLabel": "Activé", + "console.settingsPage.pageDescription": "Personnalisez la console en fonction de votre workflow.", "console.settingsPage.pageTitle": "Paramètres de la console", - "console.settingsPage.refreshButtonLabel": "Actualiser les suggestions de saisie semi-automatique", + "console.settingsPage.refreshButtonLabel": "Actualiser", "console.settingsPage.refreshingDataLabel": "Fréquence d'actualisation", "console.settingsPage.refreshInterval.everyHourTimeInterval": "Toutes les heures", "console.settingsPage.refreshInterval.everyNMinutesTimeInterval": "Toutes les {value} {value, plural, one {minute} other {minutes}}", "console.settingsPage.refreshInterval.onceTimeInterval": "Une fois, au chargement de la console", "console.settingsPage.saveRequestsToHistoryLabel": "Enregistrer les requêtes dans l'historique", "console.settingsPage.templatesLabelText": "Modèles", - "console.settingsPage.tripleQuotesMessage": "Utiliser des guillemets triples dans la sortie", + "console.settingsPage.tripleQuotesMessage": "Guillemets triples dans la sortie", + "console.settingsPage.wrapLongLinesLabel": "Formater les longues lignes", + "console.shortcutKeys.keyAltOption": "Alt/Option", + "console.shortcutKeys.keyCtrlCmd": "Ctrl/Cmd", + "console.shortcutKeys.keyDownArrow": "Flèche vers le bas", + "console.shortcutKeys.keyEnter": "Entrée", + "console.shortcutKeys.keyEsc": "Échap", + "console.shortcutKeys.keyI": "I", + "console.shortcutKeys.keyL": "L", + "console.shortcutKeys.keyO": "O", + "console.shortcutKeys.keyOption": "Option", + "console.shortcutKeys.keyShift": "Déplacer", + "console.shortcutKeys.keySlash": "/", + "console.shortcutKeys.keySpace": "Espace", + "console.shortcutKeys.keyTab": "Onglet", + "console.shortcutKeys.keyUpArrow": "Flèche vers le haut", + "console.shortcuts.alternativeKeysOrDivider": "ou", + "console.shortcuts.autocompleteShortcutsSubtitle": "Raccourcis du menu de saisie semi-automatique", + "console.shortcuts.navigationShortcutsSubtitle": "Raccourcis de navigation", + "console.shortcuts.requestShortcutsSubtitle": "Demander des raccourcis", + "console.shortcutsButtonAriaLabel": "Raccourcis clavier", + "console.topNav.configTabDescription": "Config", + "console.topNav.configTabLabel": "Config", "console.topNav.historyTabDescription": "Historique", "console.topNav.historyTabLabel": "Historique", - "console.variablesPage.addButtonLabel": "Ajouter", + "console.topNav.shellTabDescription": "Shell", + "console.topNav.shellTabLabel": "Shell", + "console.tour.completeTourButton": "Terminé", + "console.tour.configStepContent": "Ajustez les paramètres de votre console et créez des variables pour personnaliser votre workflow.", + "console.tour.configStepTitle": "Personnaliser votre boîte à outils", + "console.tour.editorStepContent": "Saisissez une requête dans ce volet d'entrée et consultez la réponse dans le volet de sortie adjacent. Pour en savoir plus, consultez la {queryDslDocs}.", + "console.tour.editorStepTitle": "Lancez-vous dans les requêtes", + "console.tour.filesStepContent": "Exportez facilement vos requêtes de console vers un fichier ou importez celles que vous avez enregistrées précédemment.", + "console.tour.filesStepTitle": "Gérer les fichiers de la console", + "console.tour.historyStepContent": "Le panneau d'historique conserve une trace de vos requêtes antérieures, ce qui facilite leur révision et leur réexécution.", + "console.tour.historyStepTitle": "Consulter les requêtes antérieures", + "console.tour.nextStepButton": "Suivant", + "console.tour.shellStepContent": "La console est une interface utilisateur interactive qui vous permet d'appeler les API Elasticsearch et Kibana et d'afficher leurs réponses. Utilisez la syntaxe Query DSL pour rechercher vos données, gérer les paramètres et bien plus encore.", + "console.tour.shellStepTitle": "Bienvenue dans la Console", + "console.tour.skipTourButton": "Ignorer la visite", + "console.variablesButton": "Variables", + "console.variablesPage.addButtonLabel": "Ajouter une variable", + "console.variablesPage.addNew.cancelButton": "Annuler", + "console.variablesPage.addNew.submitButton": "Enregistrer les modifications", + "console.variablesPage.addNewVariableTitle": "Ajouter une nouvelle variable", + "console.variablesPage.deleteModal.cancelButtonText": "Annuler", + "console.variablesPage.deleteModal.confirmButtonText": "Supprimer la variable", + "console.variablesPage.deleteModal.description": "La suppression d'une variable est une opération irréversible.", + "console.variablesPage.deleteModal.title": "Voulez-vous vraiment continuer ?", + "console.variablesPage.editVariableForm.title": "Modifier la variable", + "console.variablesPage.form.namePlaceholderLabel": "exampleName", + "console.variablesPage.form.valueFieldLabel": "Valeur", + "console.variablesPage.form.valuePlaceholderLabel": "exampleValue", + "console.variablesPage.form.valueRequiredLabel": "La valeur est requise", + "console.variablesPage.form.variableNameFieldLabel": "Nom de la variable", + "console.variablesPage.form.variableNameInvalidLabel": "Seuls les lettres, les chiffres et les traits de soulignement sont autorisés", + "console.variablesPage.form.variableNameRequiredLabel": "Ceci est un champ requis", + "console.variablesPage.pageDescription": "Définissez des paramètres fictifs réutilisables pour les valeurs dynamiques dans vos requêtes.", "console.variablesPage.pageTitle": "Variables", + "console.variablesPage.table.noItemsMessage": "Aucune variable n'a encore été ajoutée", + "console.variablesPage.variablesTable.columns.deleteButton": "Supprimer {variable}", + "console.variablesPage.variablesTable.columns.editButton": "Modifier {variable}", "console.variablesPage.variablesTable.columns.valueHeader": "Valeur", "console.variablesPage.variablesTable.columns.variableHeader": "Nom de la variable", "contentManagement.contentEditor.activity.createdByLabelText": "Créé par", @@ -402,8 +771,25 @@ "contentManagement.contentEditor.metadataForm.readOnlyToolTip": "Veuillez contacter votre administrateur pour modifier ces détails", "contentManagement.contentEditor.metadataForm.tagsLabel": "Balises", "contentManagement.contentEditor.saveButtonLabel": "Mettre à jour {entityName}", + "contentManagement.contentEditor.viewsStats.noViewsTip": "Les vues sont comptées chaque fois qu'un utilisateur ouvre un tableau de bord", + "contentManagement.contentEditor.viewsStats.noViewsTipAriaLabel": "Informations supplémentaires", + "contentManagement.contentEditor.viewsStats.viewsLabel": "Vues", + "contentManagement.contentEditor.viewsStats.viewsLastNDaysLabel": "Vues ({n} derniers jours)", + "contentManagement.contentEditor.viewsStats.weekOfLabel": "Semaine du {date}", + "contentManagement.favorites.addFavoriteError": "Erreur lors de l'ajout aux Éléments avec étoiles", + "contentManagement.favorites.defaultEntityName": "élément", + "contentManagement.favorites.defaultEntityNamePlural": "éléments", + "contentManagement.favorites.favoriteButtonLabel": "Ajouter aux Éléments avec étoiles", + "contentManagement.favorites.noFavoritesIllustrationAlt": "Aucune illustration d'élément avec étoiles", + "contentManagement.favorites.noFavoritesMessageBody": "Suivez vos {entityNamePlural} les plus importants en les ajoutant à votre liste **En vedette**. Cliquez sur l'**icône étoile** **{starIcon}** située à côté d'un nom {entityName} afin qu'il s'affiche dans cet onglet.", + "contentManagement.favorites.noFavoritesMessageHeading": "Vous n'avez ajouté d'étoile à aucun {entityNamePlural}", + "contentManagement.favorites.noMatchingFavoritesMessageHeading": "Aucun {entityNamePlural} comportant une étoile ne correspond à votre recherche", + "contentManagement.favorites.removeFavoriteError": "Erreur lors de la suppression des Éléments avec étoiles", + "contentManagement.favorites.unfavoriteButtonLabel": "Supprimer des Éléments avec étoiles", "contentManagement.inspector.metadataForm.unableToSaveDangerMessage": "Impossible d'enregistrer {entityName}", "contentManagement.tableList.actionsDisabledLabel": "Actions désactivées pour cet élément", + "contentManagement.tableList.contentEditor.activityLabel": "Activité", + "contentManagement.tableList.contentEditor.activityLabelHelpText": "Les données liées à l'activité sont générées automatiquement et ne peuvent pas être mises à jour.", "contentManagement.tableList.createdByColumnTitle": "Créateur", "contentManagement.tableList.lastUpdatedColumnTitle": "Dernière mise à jour", "contentManagement.tableList.listing.createNewItemButtonLabel": "Créer {entityName}", @@ -428,6 +814,10 @@ "contentManagement.tableList.listing.tableSortSelect.headerLabel": "Trier par", "contentManagement.tableList.listing.tableSortSelect.nameAscLabel": "Nom A-Z", "contentManagement.tableList.listing.tableSortSelect.nameDescLabel": "Nom Z-A", + "contentManagement.tableList.listing.tableSortSelect.recentlyAccessedLabel": "Récemment consulté", + "contentManagement.tableList.listing.tableSortSelect.recentlyAccessedTip": "Les informations récemment consultées sont stockées localement dans votre navigateur et ne sont visibles que par vous.", + "contentManagement.tableList.listing.tableSortSelect.recentlyAccessedTipAriaLabel": "Informations supplémentaires", + "contentManagement.tableList.listing.tableSortSelect.sortingOptionsAriaLabel": "Options de tri", "contentManagement.tableList.listing.tableSortSelect.updatedAtAscLabel": "Mise à jour la moins récente", "contentManagement.tableList.listing.tableSortSelect.updatedAtDescLabel": "Mise à jour récente", "contentManagement.tableList.listing.unableToDeleteDangerMessage": "Impossible de supprimer la/le/les {entityName}(s)", @@ -438,6 +828,8 @@ "contentManagement.tableList.listing.userFilter.noCreators": "Aucun créateur", "contentManagement.tableList.mainColumnName": "Nom, description, balises", "contentManagement.tableList.managedItemNoEdit": "Elastic gère cet objet. Clonez-le pour effectuer des modifications.", + "contentManagement.tableList.tabsFilter.allTabLabel": "Tous", + "contentManagement.tableList.tabsFilter.favoriteTabLabel": "Éléments avec étoiles", "contentManagement.tableList.tagBadge.buttonLabel": "Bouton de balise {tagName}.", "contentManagement.tableList.tagFilterPanel.clearSelectionButtonLabelLabel": "Effacer la sélection", "contentManagement.tableList.tagFilterPanel.doneButtonLabel": "Terminé", @@ -447,12 +839,17 @@ "contentManagement.userProfiles.managedAvatarTip.avatarLabel": "Géré", "contentManagement.userProfiles.managedAvatarTip.avatarTooltip": "Cette entité {entityName} est créée et gérée par Elastic. Clonez-le pour effectuer des modifications.", "contentManagement.userProfiles.managedAvatarTip.defaultEntityName": "objet", - "contentManagement.userProfiles.noCreatorTip": "Les créateurs sont assignés lors de la création des objets (version 8.14 et versions ultérieures)", - "contentManagement.userProfiles.noUpdaterTip": "Le champ Mis à jour par est défini lors de la mise à jour des objets (version 8.14 et versions ultérieures)", + "contentManagement.userProfiles.noCreatorTip": "Les créateurs sont assignés lors de la création des objets", + "contentManagement.userProfiles.noUpdaterTip": "Le champ Mis à jour par est défini lors de la mise à jour des objets", + "controls.blockingError": "Une erreur s'est produite lors du chargement de ce contrôle.", + "controls.controlFactoryRegistry.factoryAlreadyExistsError": "Une usine de contrôle pour le type : {key} est déjà enregistrée.", + "controls.controlFactoryRegistry.factoryNotFoundError": "Aucune usine de contrôle n'a été trouvée pour le type : {key}", "controls.controlGroup.ariaActions.moveControlButtonAction": "Déplacer le contrôle {controlTitle}", + "controls.controlGroup.displayName": "Contrôles", "controls.controlGroup.floatingActions.clearTitle": "Effacer", "controls.controlGroup.floatingActions.editTitle": "Modifier", "controls.controlGroup.floatingActions.removeTitle": "Supprimer", + "controls.controlGroup.manageControl": "Modifier les paramètres du contrôle", "controls.controlGroup.manageControl.cancelTitle": "Annuler", "controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "Paramètres personnalisés pour votre contrôle {controlType}.", "controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "Paramètres {controlType}", @@ -461,7 +858,9 @@ "controls.controlGroup.manageControl.dataSource.controlTypeErrorMessage.rangeSlider": "Les curseurs ne sont compatibles qu'avec les champs de numéros.", "controls.controlGroup.manageControl.dataSource.controlTypErrorMessage.noField": "Sélectionnez d'abord un champ.", "controls.controlGroup.manageControl.dataSource.controlTypesTitle": "Type de contrôle", + "controls.controlGroup.manageControl.dataSource.dataViewListErrorTitle": "Erreur lors du chargement des vues de données", "controls.controlGroup.manageControl.dataSource.dataViewTitle": "Vue de données", + "controls.controlGroup.manageControl.dataSource.fieldListErrorTitle": "Erreur lors du chargement de la liste des champs", "controls.controlGroup.manageControl.dataSource.fieldTitle": "Champ", "controls.controlGroup.manageControl.dataSource.formGroupDescription": "Sélectionnez la vue de données et le champ pour lesquels vous voulez créer un contrôle.", "controls.controlGroup.manageControl.dataSource.formGroupTitle": "Source de données", @@ -505,13 +904,14 @@ "controls.controlGroup.management.showApplySelections.tooltip": "Si cette option est désactivée, les sélections de contrôle ne seront appliquées qu'après avoir cliqué sur \"Appliquer\".", "controls.controlGroup.management.validate.title": "Valider les sélections utilisateur", "controls.controlGroup.management.validate.tooltip": "Mettez en évidence les sélections de contrôle qui n'aboutissent à aucune donnée.", + "controls.dataControl.fieldNotFound": "Impossible de localiser le champ : {fieldName}", "controls.frame.error.message": "Une erreur s'est produite. Voir plus", - "controls.optionsList.control.dateSeparator": "; ", + "controls.optionsList.control.dateSeparator": ";", "controls.optionsList.control.excludeExists": "NE PAS", "controls.optionsList.control.invalidSelectionWarningLabel": "{invalidSelectionCount} {invalidSelectionCount, plural, one {sélection ne renvoie} other {sélections ne renvoient}} aucun résultat.", "controls.optionsList.control.negate": "NON", "controls.optionsList.control.placeholder": "N'importe lequel", - "controls.optionsList.control.separator": ", ", + "controls.optionsList.control.separator": ",", "controls.optionsList.controlAndPopover.exists": "{negate, plural, one {Existe} other {Existent}}", "controls.optionsList.displayName": "Liste des options", "controls.optionsList.editor.additionalSettingsTitle": "Paramètres supplémentaires", @@ -549,6 +949,7 @@ "controls.optionsList.popover.loadingMore": "Chargement d'options supplémentaires...", "controls.optionsList.popover.prefixSearchPlaceholder": "Commence par...", "controls.optionsList.popover.selectedOptionsTitle": "Afficher uniquement les options sélectionnées", + "controls.optionsList.popover.selectionError": "Une erreur s'est produite lors de la création de votre sélection", "controls.optionsList.popover.selectionsEmpty": "Vous n'avez pas de sélections", "controls.optionsList.popover.sortBy.alphabetical": "Par ordre alphabétique", "controls.optionsList.popover.sortBy.date": "Par date", @@ -565,6 +966,7 @@ "controls.rangeSlider.control.invalidSelectionWarningLabel": "La plage sélectionnée n'a donné aucun résultat.", "controls.rangeSlider.editor.stepSizeTitle": "Taille de l'étape", "controls.rangeSlider.popover.noAvailableDataHelpText": "Il n'y a aucune donnée à afficher. Ajustez la plage temporelle et les filtres.", + "controls.rangeSliderControl.displayName": "Curseur de plage", "controls.timeSlider.nextLabel": "Fenêtre temporelle suivante", "controls.timeSlider.pauseLabel": "Pause", "controls.timeSlider.playButtonTooltip.disabled": "\"Appliquer automatiquement les sélections\" est désactivé dans les paramètres du contrôle.", @@ -572,11 +974,13 @@ "controls.timeSlider.previousLabel": "Fenêtre temporelle précédente", "controls.timeSlider.settings.pinStart": "Épingler le début", "controls.timeSlider.settings.unpinStart": "Désépingler le début", + "controls.timesliderControl.displayName": "Curseur temporel", "core.application.appContainer.loadingAriaLabel": "Chargement de l'application", "core.application.appContainer.plainSpinner.loadingAriaLabel": "Chargement de l'application", "core.application.appNotFound.pageDescription": "Aucune application détectée pour cette URL. Revenez en arrière ou sélectionnez une application dans le menu.", "core.application.appNotFound.title": "Application introuvable", "core.application.appRenderError.defaultTitle": "Erreur d'application", + "core.chrome.euiDevProviderWarning": "Les composants Kibana doivent être intégrés dans un fournisseur React Context pour obtenir l'ensemble des fonctionnalités et une prise en charge adéquate des thèmes. Voir {link}.", "core.chrome.legacyBrowserWarning": "Votre navigateur ne satisfait pas aux exigences de sécurité de Kibana.", "core.deprecations.deprecations.fetchFailed.manualStepOneMessage": "Vérifiez le message d'erreur dans les logs de serveur Kibana.", "core.deprecations.deprecations.fetchFailedMessage": "Impossible d'extraire les informations de déclassement pour le plug-in {domainId}.", @@ -620,7 +1024,9 @@ "core.euiCodeBlockCopy.copy": "Copier", "core.euiCodeBlockFullScreen.fullscreenCollapse": "Réduire", "core.euiCodeBlockFullScreen.fullscreenExpand": "Développer", + "core.euiCollapsedItemActions.allActions": "Toutes les actions, ligne {index}", "core.euiCollapsedItemActions.allActionsDisabled": "Les actions individuelles sont désactivées lorsque plusieurs lignes sont sélectionnées.", + "core.euiCollapsedItemActions.allActionsTooltip": "Toutes les actions", "core.euiCollapsedNavButton.ariaLabelButtonIcon": "{title}, menu de navigation rapide", "core.euiCollapsibleNavBeta.ariaLabel": "Menu du site", "core.euiCollapsibleNavButton.ariaLabelClose": "Fermer la navigation", @@ -642,6 +1048,7 @@ "core.euiColumnActions.moveLeft": "Déplacer vers la gauche", "core.euiColumnActions.moveRight": "Déplacer vers la droite", "core.euiColumnActions.sort": "Trier {schemaLabel}", + "core.euiColumnActions.unsort": "Ne pas trier {schemaLabel}", "core.euiColumnSelector.button": "Colonnes", "core.euiColumnSelector.dragHandleAriaLabel": "Faire glisser la poignée", "core.euiColumnSelector.hideAll": "Tout masquer", @@ -674,9 +1081,11 @@ "core.euiDataGrid.screenReaderNotice": "Cette cellule contient du contenu interactif.", "core.euiDataGridCell.expansionEnterPrompt": "Appuyez sur Entrée pour développer cette cellule.", "core.euiDataGridCell.focusTrapEnterPrompt": "Appuyez sur Entrée pour interagir avec le contenu de cette cellule.", + "core.euiDataGridCell.focusTrapExitPrompt": "Vous avez quitté le contenu de la cellule.", "core.euiDataGridCell.position": "{columnName}, colonne {columnIndex}, ligne {rowIndex}", "core.euiDataGridCellActions.expandButtonTitle": "Cliquez ou appuyez sur Entrée pour interagir avec le contenu de la cellule.", - "core.euiDataGridHeaderCell.actionsButtonAriaLabel": "{title}. Cliquez pour afficher les actions d'en-tête de colonne", + "core.euiDataGridHeaderCell.actionsButtonAriaLabel": "{title}. Cliquez pour afficher les actions d'en-tête de colonne.", + "core.euiDataGridHeaderCell.actionsEnterKeyInstructions": "Appuyez sur la touche Entrée pour afficher les actions de cette colonne", "core.euiDataGridHeaderCell.actionsPopoverScreenReaderText": "Pour naviguer dans la liste des actions de la colonne, appuyez sur la touche Tab ou sur les flèches vers le haut et vers le bas.", "core.euiDataGridHeaderCell.sortedByAscendingFirst": "Trié par {columnId}, ordre croissant", "core.euiDataGridHeaderCell.sortedByAscendingMultiple": ", puis par {columnId}, ordre croissant", @@ -709,19 +1118,23 @@ "core.euiDatePopoverContent.startDateLabel": "Date de début", "core.euiDisplaySelector.buttonText": "Options d'affichage", "core.euiDisplaySelector.densityLabel": "Densité", + "core.euiDisplaySelector.labelAuto": "Ajustement automatique", "core.euiDisplaySelector.labelCompact": "Compact", "core.euiDisplaySelector.labelExpanded": "Étendu", "core.euiDisplaySelector.labelNormal": "Normal", "core.euiDisplaySelector.resetButtonText": "Réinitialiser à la valeur par défaut", - "core.euiDisplaySelector.rowHeightLabel": "Sous-lignes par ligne", + "core.euiDisplaySelector.rowHeightLabel": "Hauteur de la ligne", "core.euiDualRange.sliderScreenReaderInstructions": "Vous êtes dans un curseur de plage personnalisé. Utilisez les flèches vers le haut et vers le bas pour modifier la valeur minimale. Appuyez sur Tabulation pour interagir avec la valeur maximale.", "core.euiErrorBoundary.error": "Erreur", - "core.euiExternalLinkIcon.newTarget.screenReaderOnlyText": "(s’ouvre dans un nouvel onglet ou une nouvelle fenêtre)", + "core.euiExternalLinkIcon.externalTarget.screenReaderOnlyText": "(externe)", + "core.euiExternalLinkIcon.newTarget.screenReaderOnlyText": "(externe, s'ouvre dans un nouvel onglet ou une nouvelle fenêtre)", "core.euiFieldPassword.maskPassword": "Masquer le mot de passe", "core.euiFieldPassword.showPassword": "Afficher le mot de passe en texte brut. Remarque : votre mot de passe sera visible à l'écran.", + "core.euiFieldSearch.clearSearchButtonLabel": "Effacer la recherche", "core.euiFilePicker.filesSelected": "{fileCount} fichiers sélectionnés", "core.euiFilePicker.promptText": "Sélectionner ou glisser-déposer un fichier", "core.euiFilePicker.removeSelected": "Supprimer", + "core.euiFilePicker.removeSelectedAriaLabel": "Supprimer les fichiers sélectionnés", "core.euiFilterButton.filterBadgeActiveAriaLabel": "{count} filtres actifs", "core.euiFilterButton.filterBadgeAvailableAriaLabel": "{count} filtres disponibles", "core.euiFlyout.screenReaderFixedHeaders": "Vous pouvez quand même continuer à parcourir les en-têtes de page à l'aide de la touche Tabulation en plus de la boîte de dialogue.", @@ -852,6 +1265,10 @@ "core.euiRecentlyUsed.legend": "Plages de dates récemment utilisées", "core.euiRefreshInterval.fullDescriptionOff": "L'actualisation est désactivée, intervalle défini sur {optionValue} {optionText}.", "core.euiRefreshInterval.fullDescriptionOn": "L'actualisation est activée, intervalle défini sur {optionValue} {optionText}.", + "core.euiRefreshInterval.toggleAriaLabel": "Basculer sur l'actualisation", + "core.euiRefreshInterval.toggleLabel": "Actualiser toutes les", + "core.euiRefreshInterval.unitsAriaLabel": "Unités d'intervalle d'actualisation", + "core.euiRefreshInterval.valueAriaLabel": "Valeur d'intervalle d'actualisation", "core.euiRelativeTab.dateInputError": "Doit être une plage valide", "core.euiRelativeTab.fullDescription": "L'unité peut être modifiée. Elle est actuellement définie sur {unit}.", "core.euiRelativeTab.numberInputError": "Doit être >= 0.", @@ -973,6 +1390,8 @@ "core.notifications.errorToast.closeModal": "Fermer", "core.notifications.globalToast.ariaLabel": "Liste de messages de notification", "core.notifications.unableUpdateUISettingNotificationMessageTitle": "Impossible de mettre à jour le paramètre de l'interface utilisateur", + "core.overlays.confirm.cancelButton": "Annuler", + "core.overlays.confirm.okButton": "Confirmer", "core.savedObjects.deprecations.unknownTypes.manualSteps.1": "Activez les plug-ins désactivés, puis redémarrez Kibana.", "core.savedObjects.deprecations.unknownTypes.manualSteps.2": "Si aucun plug-in n'est désactivé ou si leur activation ne résout pas le problème, supprimez les documents.", "core.savedObjects.deprecations.unknownTypes.message": "{objectCount, plural, one {# objet} other {# objets}} de type inconnu {objectCount, plural, one {a été trouvé} other {ont été trouvés}} dans les indices du système Kibana. La mise à niveau avec des types savedObject inconnus n'est plus compatible. Pour assurer la réussite des mises à niveau à l'avenir, réactivez les plug-ins ou supprimez ces documents dans les indices de Kibana", @@ -1012,7 +1431,7 @@ "core.ui_settings.params.darkMode.options.disabled": "Désactivé", "core.ui_settings.params.darkMode.options.enabled": "Activé", "core.ui_settings.params.darkMode.options.system": "Synchroniser avec le système", - "core.ui_settings.params.darkModeText": "Le thème de l'interface utilisateur que l'interface utilisateur de Kibana doit utiliser. La valeur \"activé\" ou \"désactivé\" permet d'activer ou de désactiver le thème sombre. Définissez sur \"système\" pour que le thème de l'interface utilisateur de Kibana suive le thème du système. Vous devez actualiser la page pour que ce paramètre s’applique.", + "core.ui_settings.params.darkModeText": "Le thème de l'interface utilisateur que l'interface utilisateur de Kibana doit utiliser. Réglez l'option sur \"Activé\" pour activer le thème sombre ou sur \"Désactivé\" pour le désactiver. Définissez l'option sur \"Synchroniser avec le système\" pour que le thème de l'interface utilisateur de Kibana suive le thème du système. Vous devez recharger la page pour que ce paramètre s'applique.", "core.ui_settings.params.darkModeTitle": "Mode sombre", "core.ui_settings.params.dateFormat.dayOfWeekText": "Le premier jour de la semaine", "core.ui_settings.params.dateFormat.dayOfWeekTitle": "Jour de la semaine", @@ -1034,15 +1453,15 @@ "core.ui_settings.params.hideAnnouncements": "Masquer les annonces", "core.ui_settings.params.hideAnnouncementsText": "Arrêtez d’afficher les messages et les visites guidées qui mettent en avant les nouvelles fonctionnalités.", "core.ui_settings.params.notifications.banner.markdownLinkText": "Markdown pris en charge", - "core.ui_settings.params.notifications.bannerLifetimeText": "La durée en millisecondes durant laquelle une notification de bannière s'affiche à l'écran. ", + "core.ui_settings.params.notifications.bannerLifetimeText": "La durée en millisecondes durant laquelle une notification de bannière s'affiche à l'écran.", "core.ui_settings.params.notifications.bannerLifetimeTitle": "Durée des notifications de bannière", "core.ui_settings.params.notifications.bannerText": "Une bannière personnalisée à des fins de notification temporaire de l’ensemble des utilisateurs. {markdownLink}.", "core.ui_settings.params.notifications.bannerTitle": "Notification de bannière personnalisée", - "core.ui_settings.params.notifications.errorLifetimeText": "La durée en millisecondes durant laquelle une notification d'erreur s'affiche à l'écran. ", + "core.ui_settings.params.notifications.errorLifetimeText": "La durée en millisecondes durant laquelle une notification d'erreur s'affiche à l'écran.", "core.ui_settings.params.notifications.errorLifetimeTitle": "Durée des notifications d'erreur", - "core.ui_settings.params.notifications.infoLifetimeText": "La durée en millisecondes durant laquelle une notification d'information s'affiche à l'écran. ", + "core.ui_settings.params.notifications.infoLifetimeText": "La durée en millisecondes durant laquelle une notification d'information s'affiche à l'écran.", "core.ui_settings.params.notifications.infoLifetimeTitle": "Durée des notifications d'information", - "core.ui_settings.params.notifications.warningLifetimeText": "La durée en millisecondes durant laquelle une notification d'avertissement s'affiche à l'écran. ", + "core.ui_settings.params.notifications.warningLifetimeText": "La durée en millisecondes durant laquelle une notification d'avertissement s'affiche à l'écran.", "core.ui_settings.params.notifications.warningLifetimeTitle": "Durée des notifications d'avertissement", "core.ui_settings.params.storeUrlText": "L'URL peut parfois devenir trop longue pour être gérée par certains navigateurs. Pour pallier ce problème, nous testons actuellement le stockage de certaines parties de l'URL dans le stockage de session. N’hésitez pas à nous faire part de vos commentaires.", "core.ui_settings.params.storeUrlTitle": "Stocker les URL dans le stockage de session", @@ -1089,7 +1508,9 @@ "core.ui.primaryNav.cloud.linkToProject": "Gérer le projet", "core.ui.primaryNav.cloud.projectLabel": "Projet", "core.ui.primaryNav.goToHome.ariaLabel": "Accéder à la page d’accueil", + "core.ui.primaryNav.header.toggleNavAriaLabel": "Activer/Désactiver la navigation principale", "core.ui.primaryNav.pinnedLinksAriaLabel": "Liens épinglés", + "core.ui.primaryNav.project.toggleNavAriaLabel": "Activer/Désactiver la navigation principale", "core.ui.primaryNav.screenReaderLabel": "Principale", "core.ui.primaryNavSection.screenReaderLabel": "Liens de navigation principale, {category}", "core.ui.publicBaseUrlWarning.configRecommendedDescription": "Dans un environnement de production, il est recommandé de configurer {configKey}.", @@ -1185,7 +1606,7 @@ "customIntegrationsPackage.create.configureIntegrationDescription.helper": "Elastic crée une intégration pour rationaliser la connexion de vos données de log dans la Suite Elastic.", "customIntegrationsPackage.create.dataset.helper": "Tout en minuscules, 100 caractères maximum, les caractères spéciaux seront remplacés par \"_\".", "customIntegrationsPackage.create.dataset.name": "Nom de l’ensemble de données", - "customIntegrationsPackage.create.dataset.name.tooltip": "Le nom de l'ensemble de données associé à cette intégration. Ceci fera partie du nom du flux de données Elasticsearch ", + "customIntegrationsPackage.create.dataset.name.tooltip": "Le nom de l'ensemble de données associé à cette intégration. Ceci fera partie du nom du flux de données Elasticsearch", "customIntegrationsPackage.create.dataset.name.tooltipPrefixMessage": "Ce nom aura pour préfixe {prefixValue}, par ex. {prefixedDatasetName}", "customIntegrationsPackage.create.dataset.placeholder": "Nommez votre intégration de l'ensemble de données", "customIntegrationsPackage.create.errorCallout.authorization.description": "Cet utilisateur ne dispose pas d'autorisations pour créer une intégration.", @@ -1228,6 +1649,7 @@ "dashboard.editingToolbar.controlsButtonTitle": "Contrôles", "dashboard.editingToolbar.editControlGroupButtonTitle": "Paramètres", "dashboard.editingToolbar.onlyOneTimeSliderControlMsg": "Le groupe de contrôle contient déjà un contrôle de curseur temporel.", + "dashboard.editorMenu.addPanelFlyout.searchLabelText": "champ de recherche pour les panneaux", "dashboard.editorMenu.deprecatedTag": "Déclassé", "dashboard.embeddableApi.showSettings.flyout.applyButtonTitle": "Appliquer", "dashboard.embeddableApi.showSettings.flyout.cancelButtonTitle": "Annuler", @@ -1241,6 +1663,7 @@ "dashboard.embeddableApi.showSettings.flyout.form.panelTitleInputAriaLabel": "Modifier le titre du tableau de bord", "dashboard.embeddableApi.showSettings.flyout.form.storeTimeWithDashboardFormRowHelpText": "Le filtre temporel est défini sur l’option sélectionnée chaque fois que ce tableau de bord est chargé.", "dashboard.embeddableApi.showSettings.flyout.form.storeTimeWithDashboardFormRowLabel": "Enregistrer la plage temporelle avec le tableau de bord", + "dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchHelp": "Valide uniquement pour les palettes {default} et {compatibility}", "dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel": "Synchroniser les palettes de couleur de tous les panneaux", "dashboard.embeddableApi.showSettings.flyout.form.syncCursorBetweenPanelsSwitchLabel": "Synchroniser le curseur de tous les panneaux", "dashboard.embeddableApi.showSettings.flyout.form.syncTooltipsBetweenPanelsSwitchLabel": "Synchroniser les infobulles de tous les panneaux", @@ -1291,8 +1714,14 @@ "dashboard.listing.unsaved.unsavedChangesTitle": "Vous avez des modifications non enregistrées dans le {dash} suivant :", "dashboard.loadingError.dashboardGridErrorMessage": "Impossible de charger le tableau de bord : {message}", "dashboard.loadURLError.PanelTooOld": "Impossible de charger les panneaux à partir d'une URL créée dans une version antérieure à 7.3", + "dashboard.managedContentBadge.ariaLabel": "Elastic gère ce tableau de bord. Dupliquez-le pour apporter des modifications.", + "dashboard.managedContentPopoverButton": "Elastic gère ce tableau de bord. {Duplicate}-le pour y apporter des modifications.", + "dashboard.managedContentPopoverButtonText": "Dupliquer", + "dashboard.managedContentPopoverFooterText": "Cliquez ici pour dupliquer ce tableau de bord", "dashboard.noMatchRoute.bannerText": "L'application de tableau de bord ne reconnaît pas ce chemin : {route}.", "dashboard.noMatchRoute.bannerTitleText": "Page introuvable", + "dashboard.palettes.defaultPaletteLabel": "Par défaut", + "dashboard.palettes.kibanaPaletteLabel": "Compatibilité", "dashboard.panel.AddToLibrary": "Enregistrer dans la bibliothèque", "dashboard.panel.addToLibrary.errorMessage": "Une erreur s'est produite lors de l'ajout du panneau {panelTitle} à la bibliothèque", "dashboard.panel.addToLibrary.successMessage": "Le panneau {panelTitle} a été ajouté à la bibliothèque", @@ -1326,7 +1755,7 @@ "dashboard.renderer.404Body": "Désolé, le tableau de bord que vous recherchez est introuvable. Elle a peut-être été retirée ou renommée, ou peut-être qu'elle n'a jamais existé.", "dashboard.renderer.404Title": "Tableau de bord introuvable", "dashboard.resetChangesConfirmModal.confirmButtonLabel": "Réinitialiser le tableau de bord", - "dashboard.resetChangesConfirmModal.resetChangesDescription": "Ce tableau de bord va revenir à son dernier état d'enregistrement. Vous risquez de perdre les modifications apportées aux filtres et aux requêtes.", + "dashboard.resetChangesConfirmModal.resetChangesDescription": "Ce tableau de bord va revenir à son dernier état d'enregistrement. Vous risquez de perdre les modifications apportées aux filtres et aux requêtes.", "dashboard.resetChangesConfirmModal.resetChangesTitle": "Réinitialiser le tableau de bord ?", "dashboard.savedDashboard.newDashboardTitle": "Nouveau tableau de bord", "dashboard.share.defaultDashboardTitle": "Tableau de bord [{date}]", @@ -1335,6 +1764,7 @@ "dashboard.solutionToolbar.addPanelButtonLabel": "Créer une visualisation", "dashboard.solutionToolbar.addPanelFlyout.cancelButtonText": "Fermer", "dashboard.solutionToolbar.addPanelFlyout.headingText": "Ajouter un panneau", + "dashboard.solutionToolbar.addPanelFlyout.loadingErrorDescription": "Une erreur est survenue lors du chargement des panneaux du tableau de bord disponibles pour la sélection", "dashboard.solutionToolbar.addPanelFlyout.noResultsDescription": "Aucun type de panneaux trouvé", "dashboard.solutionToolbar.editorMenuButtonLabel": "Ajouter un panneau", "dashboard.solutionToolbar.quickCreateButtonGroupLegend": "Raccourcis vers les types de visualisation populaires", @@ -1365,7 +1795,7 @@ "dashboard.topNave.viewModeInteractiveSaveButtonAriaLabel": "dupliquer", "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "Créer une copie du tableau de bord", "dashboard.unsavedChangesBadge": "Modifications non enregistrées", - "dashboard.unsavedChangesBadgeToolTipContent": " Vous avez des modifications non enregistrées dans ce tableau de bord. Pour supprimer cette étiquette, enregistrez le tableau de bord.", + "dashboard.unsavedChangesBadgeToolTipContent": "Vous avez des modifications non enregistrées dans ce tableau de bord. Pour supprimer cette étiquette, enregistrez le tableau de bord.", "dashboard.viewmodeBackup.error": "Une erreur s'est produite lors de la sauvegarde du mode d'affichage : {message}", "data.advancedSettings.autocompleteIgnoreTimerange": "Utiliser la plage temporelle", "data.advancedSettings.autocompleteIgnoreTimerangeText": "Désactivez cette propriété pour obtenir des suggestions de saisie semi-automatique depuis l’intégralité de l’ensemble de données plutôt que depuis la plage temporelle définie. {learnMoreLink}", @@ -1383,7 +1813,7 @@ "data.advancedSettings.courier.requestPreferenceCustom": "Personnalisée", "data.advancedSettings.courier.requestPreferenceNone": "Aucune", "data.advancedSettings.courier.requestPreferenceSessionId": "ID session", - "data.advancedSettings.courier.requestPreferenceText": "Permet de définir quelles partitions doivent gérer les requêtes de recherche.\n
    \n
  • {sessionId} : limite les opérations pour exécuter toutes les requêtes de recherche sur les mêmes partitions.\n Cela a l'avantage de réutiliser les caches de partition pour toutes les requêtes.
  • \n
  • {custom} : permet de définir une valeur de préférence.\n Utilisez courier:customRequestPreference pour personnaliser votre valeur de préférence.
  • \n
  • {none} : permet de ne pas définir de préférence.\n Cela peut permettre de meilleures performances, car les requêtes peuvent être réparties entre toutes les copies de partition.\n Cependant, les résultats peuvent être incohérents, les différentes partitions pouvant se trouver dans différents états d'actualisation.
  • \n
", + "data.advancedSettings.courier.requestPreferenceText": "Permet de définir quelles partitions doivent gérer les requêtes de recherche.
  • {sessionId} : limite les opérations pour exécuter toutes les requêtes de recherche sur les mêmes partitions. Cela a l'avantage de réutiliser les caches de partition pour toutes les requêtes.
  • {custom} : permet de définir une valeur de préférence. Utilisez courier:customRequestPreference pour personnaliser votre valeur de préférence.
  • {none} : permet de ne pas définir de préférence. Cela peut permettre de meilleures performances, car les requêtes peuvent être réparties entre toutes les copies de partition. Cependant, les résultats peuvent être incohérents, les différentes partitions pouvant se trouver dans différents états d'actualisation.
", "data.advancedSettings.courier.requestPreferenceTitle": "Préférence de requête", "data.advancedSettings.defaultIndexText": "Utilisé par Discover et Visualisations lorsqu'une vue de données n'est pas définie.", "data.advancedSettings.defaultIndexTitle": "Vue de données par défaut", @@ -1391,7 +1821,7 @@ "data.advancedSettings.docTableHighlightTitle": "Mettre les résultats en surbrillance", "data.advancedSettings.histogram.barTargetText": "Tente de générer ce nombre de compartiments lorsque l’intervalle \"auto\" est utilisé dans des histogrammes numériques et de date.", "data.advancedSettings.histogram.barTargetTitle": "Nombre de compartiments cible", - "data.advancedSettings.histogram.maxBarsText": "\n Limite la densité des histogrammes numériques et de date dans tout Kibana\n pour de meilleures performances à l’aide d’une requête de test. Si la requête de test génère trop de compartiments,\n l'intervalle entre les compartiments est augmenté. Ce paramètre s'applique séparément\n pour chaque agrégation d'histogrammes et ne s'applique pas aux autres types d'agrégations.\n Pour identifier la valeur maximale de ce paramètre, divisez la valeur \"search.max_buckets\" d'Elasticsearch\n par le nombre maximal d'agrégations dans chaque visualisation.\n ", + "data.advancedSettings.histogram.maxBarsText": "Avec une requête de test, limite la densité des histogrammes de dates et de nombres sur Kibana pour garantir de meilleures performances. Si la requête de test génère trop de compartiments, l'intervalle entre les compartiments est augmenté. Ce paramètre s'applique séparément pour chaque agrégation d'histogrammes et ne s'applique pas aux autres types d'agrégations. Pour trouver la valeur maximale de ce paramètre, divisez la valeur \"search.max_buckets\" d'Elasticsearch par le nombre maximal d'agrégations dans chaque visualisation.", "data.advancedSettings.histogram.maxBarsTitle": "Nombre maximal de compartiments", "data.advancedSettings.historyLimitText": "Le nombre de valeurs les plus récentes qui s’affichent pour les champs associés à un historique (par exemple, les entrées de requête).", "data.advancedSettings.historyLimitTitle": "Limite d'historique", @@ -1526,7 +1956,7 @@ "data.search.aggs.buckets.dateHistogram.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", "data.search.aggs.buckets.dateHistogram.dropPartials.help": "Spécifie l'utilisation ou non de drop_partials pour cette agrégation.", "data.search.aggs.buckets.dateHistogram.enabled.help": "Spécifie si cette agrégation doit être activée.", - "data.search.aggs.buckets.dateHistogram.extendedBounds.help": "Avec le paramètre extended_bounds, il est désormais possible de \"forcer\" l'agrégation d'histogrammes à démarrer la conception des compartiments sur une valeur minimale spécifique et à continuer jusqu'à une valeur maximale. ", + "data.search.aggs.buckets.dateHistogram.extendedBounds.help": "Avec le paramètre extended_bounds, il est désormais possible de \"forcer\" l'agrégation d'histogrammes à démarrer la conception des compartiments sur une valeur minimale spécifique et à continuer jusqu'à une valeur maximale.", "data.search.aggs.buckets.dateHistogram.extendToTimeRange.help": "Définit automatiquement les limites étendues sur la plage temporelle appliquée actuellement. Est ignoré si extended_bounds est défini", "data.search.aggs.buckets.dateHistogram.field.help": "Champ à utiliser pour cette agrégation", "data.search.aggs.buckets.dateHistogram.format.help": "Format à utiliser pour cette agrégation", @@ -1583,7 +2013,7 @@ "data.search.aggs.buckets.histogram.autoExtendBounds.help": "Définissez cette option comme vraie pour étendre les limites au domaine des données. Cela permet de s’assurer que chaque compartiment d'intervalle compris dans ces limites créera une ligne de tableau distincte.", "data.search.aggs.buckets.histogram.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", "data.search.aggs.buckets.histogram.enabled.help": "Spécifie si cette agrégation doit être activée.", - "data.search.aggs.buckets.histogram.extendedBounds.help": "Avec le paramètre extended_bounds, il est désormais possible de \"forcer\" l'agrégation d'histogrammes à démarrer la conception des compartiments sur une valeur minimale spécifique et à continuer jusqu'à une valeur maximale. ", + "data.search.aggs.buckets.histogram.extendedBounds.help": "Avec le paramètre extended_bounds, il est désormais possible de \"forcer\" l'agrégation d'histogrammes à démarrer la conception des compartiments sur une valeur minimale spécifique et à continuer jusqu'à une valeur maximale.", "data.search.aggs.buckets.histogram.field.help": "Champ à utiliser pour cette agrégation", "data.search.aggs.buckets.histogram.hasExtendedBounds.help": "Spécifie l'utilisation ou non de has_extended_bounds pour cette agrégation.", "data.search.aggs.buckets.histogram.id.help": "ID pour cette agrégation", @@ -2216,6 +2646,8 @@ "data.searchSessionIndicator.canceledTooltipText": "La session de recherche s'est arrêtée", "data.searchSessionIndicator.canceledWhenText": "Arrêtée {when}", "data.searchSessionIndicator.continueInBackgroundButtonText": "Enregistrer la session", + "data.searchSessionIndicator.deprecationWarning.textParagraphOne": "Les sessions de recherche sont obsolètes et seront supprimées dans une future version.", + "data.searchSessionIndicator.deprecationWarning.title": "Déclassé dans la version 8.15.0", "data.searchSessionIndicator.disabledDueToDisabledGloballyMessage": "Vous ne disposez pas d'autorisations pour gérer les sessions de recherche", "data.searchSessionIndicator.disabledDueToTimeoutMessage": "Les résultats de la session de recherche ont expiré.", "data.searchSessionIndicator.loadingInTheBackgroundDescriptionText": "Vous pouvez retourner aux résultats terminés à partir de la page Gestion", @@ -2313,6 +2745,7 @@ "discover.advancedSettings.disableDocumentExplorerDescription": "Désactivez cette option pour utiliser le nouveau {documentExplorerDocs} au lieu de la vue classique. l'explorateur de documents offre un meilleur tri des données, des colonnes redimensionnables et une vue en plein écran.", "discover.advancedSettings.discover.disableDocumentExplorerDeprecation": "Ce paramètre est déclassé et sera supprimé dans la version 9.0 de Kibana.", "discover.advancedSettings.discover.fieldStatisticsLinkText": "Vue des statistiques de champ", + "discover.advancedSettings.discover.maxCellHeightDeprecation": "Ce paramètre est déclassé et sera supprimé dans la version 9.0 de Kibana.", "discover.advancedSettings.discover.modifyColumnsOnSwitchText": "Supprimez les colonnes qui ne sont pas disponibles dans la nouvelle vue de données.", "discover.advancedSettings.discover.modifyColumnsOnSwitchTitle": "Modifier les colonnes en cas de changement des vues de données", "discover.advancedSettings.discover.multiFieldsLinkText": "champs multiples", @@ -2371,6 +2804,13 @@ "discover.context.unableToLoadDocumentDescription": "Impossible de charger les documents", "discover.contextViewRoute.errorMessage": "Aucune donnée correspondante pour l'ID {dataViewId}", "discover.contextViewRoute.errorTitle": "Une erreur s'est produite", + "discover.customControl.degradedDocArialLabel": "Accéder aux documents dégradés", + "discover.customControl.degradedDocDisabled": "La détection des champs de documents dégradés est désactivée pour cette recherche. Cliquez pour ajouter une {directive} à votre requête ES|QL.", + "discover.customControl.degradedDocNotPresent": "Tous les champs de ce document ont été analysés correctement", + "discover.customControl.degradedDocPresent": "Ce document n'a pas pu être analysé correctement. Tous les champs n'ont pas été remplis correctement.", + "discover.customControl.stacktrace.available": "Traces d'appel disponibles", + "discover.customControl.stacktrace.notAvailable": "Traces d'appel indisponibles", + "discover.customControl.stacktraceArialLabel": "Accès aux traces d'appel disponibles", "discover.discoverBreadcrumbTitle": "Discover", "discover.discoverDefaultSearchSessionName": "Discover", "discover.discoverDescription": "Explorez vos données de manière interactive en interrogeant et en filtrant des documents bruts.", @@ -2417,18 +2857,22 @@ "discover.docTable.tableRow.viewSingleDocumentLinkText": "Afficher un seul document", "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "Afficher les documents alentour", "discover.documentsAriaLabel": "Documents", + "discover.docViews.logsOverview.title": "Aperçu du log", "discover.docViews.table.scoreSortWarningTooltip": "Filtrez sur _score pour pouvoir récupérer les valeurs correspondantes.", "discover.dropZoneTableLabel": "Abandonner la zone pour ajouter un champ en tant que colonne dans la table", "discover.embeddable.inspectorRequestDataTitle": "Données", "discover.embeddable.inspectorRequestDescription": "Cette requête interroge Elasticsearch afin de récupérer les données pour la recherche.", + "discover.embeddable.search.dataViewError": "Vue de données {indexPatternId} manquante", "discover.embeddable.search.displayName": "rechercher", + "discover.errorCalloutESQLReferenceButtonLabel": "Ouvrir la référence ES|QL", "discover.errorCalloutShowErrorMessage": "Afficher les détails", "discover.esqlMode.selectedColumnsCallout": "Affichage de {selectedColumnsNumber} champs sur {esqlQueryColumnsNumber}. Ajoutez-en d’autres depuis la liste des champs disponibles.", - "discover.esqlToDataViewTransitionModal.closeButtonLabel": "Basculer sans sauvegarder", - "discover.esqlToDataViewTransitionModal.dismissButtonLabel": "Ne plus afficher cet avertissement", + "discover.esqlToDataViewTransitionModal.closeButtonLabel": "Annuler et changer", + "discover.esqlToDataViewTransitionModal.dismissButtonLabel": "Ne plus me demander", + "discover.esqlToDataViewTransitionModal.feedbackLink": "Soumettre des commentaires ES|QL", "discover.esqlToDataViewTransitionModal.saveButtonLabel": "Sauvegarder et basculer", - "discover.esqlToDataViewTransitionModal.title": "Votre requête sera supprimée", - "discover.esqlToDataviewTransitionModalBody": "Modifier la vue de données supprime la requête ES|QL en cours. Sauvegardez cette recherche pour ne pas perdre de travail.", + "discover.esqlToDataViewTransitionModal.title": "Modifications non enregistrées", + "discover.esqlToDataviewTransitionModalBody": "Un changement de vue de données supprime la requête ES|QL en cours. Sauvegardez cette recherche pour éviter de perdre votre travail.", "discover.fieldChooser.availableFieldsTooltip": "Champs disponibles pour l'affichage dans le tableau.", "discover.fieldChooser.discoverField.addFieldTooltip": "Ajouter le champ en tant que colonne", "discover.fieldChooser.discoverField.removeFieldTooltip": "Supprimer le champ du tableau", @@ -2458,6 +2902,7 @@ "discover.loadingDocuments": "Chargement des documents", "discover.loadingResults": "Chargement des résultats", "discover.localMenu.alertsDescription": "Alertes", + "discover.localMenu.esqlTooltipLabel": "ES|QL est le nouveau langage de requête canalisé puissant d'Elastic.", "discover.localMenu.fallbackReportTitle": "Recherche Discover sans titre", "discover.localMenu.inspectTitle": "Inspecter", "discover.localMenu.localMenu.alertsTitle": "Alertes", @@ -2472,7 +2917,21 @@ "discover.localMenu.saveTitle": "Enregistrer", "discover.localMenu.shareSearchDescription": "Partager la recherche", "discover.localMenu.shareTitle": "Partager", + "discover.localMenu.switchToClassicTitle": "Basculer vers le classique", + "discover.localMenu.switchToClassicTooltipLabel": "Passez à la syntaxe KQL ou Lucene.", + "discover.localMenu.tryESQLTitle": "Essayer ES|QL", + "discover.logLevelLabels.alert": "Alerte", + "discover.logLevelLabels.critical": "Critique", + "discover.logLevelLabels.debug": "Déboguer", + "discover.logLevelLabels.emergency": "Urgence", + "discover.logLevelLabels.error": "Erreur", + "discover.logLevelLabels.fatal": "Fatal", + "discover.logLevelLabels.info": "Infos", + "discover.logLevelLabels.notice": "Notification", + "discover.logLevelLabels.trace": "Trace", + "discover.logLevelLabels.warning": "Avertissement", "discover.logs.dataTable.header.popover.content": "Contenu", + "discover.logs.dataTable.header.popover.json": "JSON", "discover.logs.dataTable.header.popover.resource": "Ressource", "discover.logs.flyoutDetail.value.hover.filterFor": "Filtrer sur cette {value}", "discover.logs.flyoutDetail.value.hover.filterOut": "Exclure cette {value}", @@ -2557,7 +3016,7 @@ "discover.viewAlert.alertRuleFetchErrorTitle": "Erreur lors de la récupération de la règle d'alerte", "discover.viewAlert.dataViewErrorText": "Échec de la vue des données de la règle d'alerte avec l'ID {alertId}.", "discover.viewAlert.dataViewErrorTitle": "Erreur lors de la récupération de la vue de données", - "discover.viewAlert.documentsMayVaryInfoDescription": "Les documents affichés peuvent différer de ceux ayant déclenché l'alerte.\n Des documents ont peut-être été ajoutés ou supprimés.", + "discover.viewAlert.documentsMayVaryInfoDescription": "Les documents affichés peuvent différer de ceux ayant déclenché l'alerte. Des documents ont peut-être été ajoutés ou supprimés.", "discover.viewAlert.documentsMayVaryInfoTitle": "Les documents affichés peuvent varier", "discover.viewAlert.searchSourceErrorTitle": "Erreur lors de la récupération de la source de recherche", "discover.viewModes.document.label": "Documents", @@ -2565,7 +3024,7 @@ "discover.viewModes.patternAnalysis.label": "Modèles {patternCount}", "domDragDrop.announce.cancelled": "Mouvement annulé. {label} revenu à sa position initiale", "domDragDrop.announce.cancelledItem": "Mouvement annulé. {label} revenu au groupe {groupLabel} à la position {position}", - "domDragDrop.announce.combine.short": " Maintenir la touche Contrôle enfoncée pour combiner", + "domDragDrop.announce.combine.short": "Maintenir la touche Contrôle enfoncée pour combiner", "domDragDrop.announce.dropped.combineCompatible": "Combinaisons de {label} dans le {groupLabel} vers {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}", "domDragDrop.announce.dropped.combineIncompatible": "Conversion de {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} et combinaison avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}", "domDragDrop.announce.dropped.duplicated": "{label} dupliqué dans le groupe {groupLabel} à la position {position} dans le calque {layerNumber}", @@ -2579,7 +3038,7 @@ "domDragDrop.announce.dropped.swapIncompatible": "Conversion de {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} dans le calque {layerNumber} et permuté avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}", "domDragDrop.announce.droppedDefault": "Ajout de {label} dans le groupe {dropGroupLabel} à la position {position} dans le calque {dropLayerNumber}", "domDragDrop.announce.droppedNoPosition": "{label} ajouté à {dropLabel}", - "domDragDrop.announce.duplicate.short": " Maintenez la touche Alt ou Option enfoncée pour dupliquer.", + "domDragDrop.announce.duplicate.short": "Maintenez la touche Alt ou Option enfoncée pour dupliquer.", "domDragDrop.announce.duplicated.combine": "Combinaison de {dropLabel} avec {label} dans {groupLabel} à la position {position} dans le calque {dropLayerNumber}", "domDragDrop.announce.duplicated.replace": "Remplacement de {dropLabel} par {label} dans {groupLabel} à la position {position} dans le calque {dropLayerNumber}", "domDragDrop.announce.duplicated.replaceDuplicateCompatible": "Remplacement de {dropLabel} par une copie de {label} dans {groupLabel} à la position {position} dans le calque {dropLayerNumber}", @@ -2608,7 +3067,7 @@ "domDragDrop.announce.selectedTarget.replaceMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} dans le calque {layerNumber} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}. Appuyez sur la barre d'espace ou sur Entrée pour remplacer {dropLabel} par {label}.{duplicateCopy}{swapCopy}{combineCopy}", "domDragDrop.announce.selectedTarget.swapCompatible": "Permutation de {label} dans le groupe {groupLabel} à la position {position} dans le calque {layerNumber} et de {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}. Maintenir la touche Maj enfoncée tout en appuyant sur la barre d'espace ou sur Entrée pour permuter", "domDragDrop.announce.selectedTarget.swapIncompatible": "Conversion de {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} dans le calque {layerNumber} et permutation avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}. Maintenir la touche Maj enfoncée tout en appuyant sur la barre d'espace ou sur Entrée pour permuter", - "domDragDrop.announce.swap.short": " Maintenez la touche Maj enfoncée pour permuter.", + "domDragDrop.announce.swap.short": "Maintenez la touche Maj enfoncée pour permuter.", "domDragDrop.dropTargets.altOption": "Alt/Option", "domDragDrop.dropTargets.combine": "Combiner", "domDragDrop.dropTargets.control": "Contrôler", @@ -2652,6 +3111,37 @@ "embeddableApi.selectRangeTrigger.title": "Sélection de la plage", "embeddableApi.valueClickTrigger.description": "Un point de données cliquable sur la visualisation", "embeddableApi.valueClickTrigger.title": "Clic unique", + "embeddableExamples.dataTable.ariaLabel": "Tableau de données", + "embeddableExamples.dataTable.noDataViewError": "Au moins une vue de données est requise pour utiliser l'exemple de table de données.", + "embeddableExamples.euiMarkdownEditor.displayNameAriaLabel": "Markdown EUI", + "embeddableExamples.euiMarkdownEditor.embeddableAriaLabel": "Éditeur de markdown du tableau de bord", + "embeddableExamples.savedbook.addBookAction.displayName": "Livre", + "embeddableExamples.savedbook.editBook.displayName": "livre", + "embeddableExamples.savedBook.editor.addToLibrary": "Enregistrer dans la bibliothèque", + "embeddableExamples.savedBook.editor.authorLabel": "Auteur", + "embeddableExamples.savedBook.editor.cancel": "Abandonner les modifications", + "embeddableExamples.savedBook.editor.create": "Créer un livre", + "embeddableExamples.savedBook.editor.editTitle": "Modifier un livre", + "embeddableExamples.savedBook.editor.newTitle": "Créer un nouveau livre", + "embeddableExamples.savedBook.editor.pagesLabel": "Nombre de pages", + "embeddableExamples.savedBook.editor.save": "Conserver les modifications", + "embeddableExamples.savedBook.editor.synopsisLabel": "Synopsis", + "embeddableExamples.savedBook.editor.titleLabel": "Titre", + "embeddableExamples.savedBook.libraryCallout": "Enregistré dans la bibliothèque", + "embeddableExamples.savedBook.noLibraryCallout": "Non enregistré dans la bibliothèque", + "embeddableExamples.savedBook.numberOfPages": "{numberOfPages} pages", + "embeddableExamples.search.dataViewName": "{dataViewName}", + "embeddableExamples.search.noDataViewError": "Veuillez installer une vue de données pour visualiser cet exemple", + "embeddableExamples.search.result": "{count, plural, one {document trouvé} other {documents trouvés}}", + "embeddableExamples.unifiedFieldList.displayName": "Liste des champs", + "embeddableExamples.unifiedFieldList.noDefaultDataViewErrorMessage": "La liste de champs doit être utilisée avec au moins une vue de données présente", + "embeddableExamples.unifiedFieldList.selectDataViewMessage": "Veuillez sélectionner une vue de données", + "esql.advancedSettings.enableESQLDescription": "Ce paramètre active ES|QL dans Kibana. En le désactivant, vous cacherez l'interface utilisateur ES|QL de diverses applications. Cependant, les utilisateurs pourront accéder aux recherches enregistrées ES|QL, en plus des visualisations, etc.", + "esql.advancedSettings.enableESQLTitle": "Activer ES|QL", + "esql.triggers.updateEsqlQueryTrigger": "Mettre à jour la requête ES|QL", + "esql.triggers.updateEsqlQueryTriggerDescription": "Mettre à jour la requête ES|QL en utilisant une nouvelle requête", + "esql.updateESQLQueryLabel": "Mettre à jour la requête ES|QL dans l’éditeur", + "esqlDataGrid.openInDiscoverLabel": "Ouvrir dans Discover", "esqlEditor.query.aborted": "La demande a été annulée", "esqlEditor.query.cancel": "Annuler", "esqlEditor.query.collapseLabel": "Réduire", @@ -2662,6 +3152,8 @@ "esqlEditor.query.expandLabel": "Développer", "esqlEditor.query.feedback": "Commentaires", "esqlEditor.query.hideQueriesLabel": "Masquer les recherches récentes", + "esqlEditor.query.limitInfo": "LIMITE : {limit} lignes", + "esqlEditor.query.limitInfoReduced": "LIMITE : {limit}", "esqlEditor.query.lineCount": "{count} {count, plural, one {ligne} other {lignes}}", "esqlEditor.query.lineNumber": "Ligne {lineNumber}", "esqlEditor.query.querieshistory.error": "La requête a échouée", @@ -2670,10 +3162,12 @@ "esqlEditor.query.querieshistoryRun": "Exécuter la requête", "esqlEditor.query.querieshistoryTable": "Tableau d'historique des recherches", "esqlEditor.query.recentQueriesColumnLabel": "Recherches récentes", + "esqlEditor.query.refreshLabel": "Actualiser", "esqlEditor.query.runQuery": "Exécuter la requête", "esqlEditor.query.showQueriesLabel": "Afficher les recherches récentes", "esqlEditor.query.submitFeedback": "Soumettre un commentaire", "esqlEditor.query.timeRanColumnLabel": "Temps exécuté", + "esqlEditor.query.timestampDetected": "{detectedTimestamp} trouvé", "esqlEditor.query.timestampNotDetected": "@timestamp non trouvé", "esqlEditor.query.warningCount": "{count} {count, plural, one {avertissement} other {avertissements}}", "esqlEditor.query.warningsTitle": "Avertissements", @@ -2793,6 +3287,7 @@ "eventAnnotation.rangeAnnotation.args.time": "Horodatage de l'annotation", "eventAnnotation.rangeAnnotation.description": "Configurer l'annotation manuelle", "eventAnnotationCommon.manualAnnotation.defaultAnnotationLabel": "Événement", + "eventAnnotationCommon.manualAnnotation.defaultRangeAnnotationLabel": "Plage d'événements", "eventAnnotationComponents.eventAnnotationGroup.metadata.name": "Groupes d’annotations", "eventAnnotationComponents.eventAnnotationGroup.savedObjectFinder.emptyCTA": "Créer un calque d’annotations", "eventAnnotationComponents.eventAnnotationGroup.savedObjectFinder.emptyPromptDescription": "Il n’y a actuellement aucune annotation disponible à sélectionner depuis la bibliothèque. Créez un nouveau calque pour ajouter des annotations.", @@ -2912,12 +3407,23 @@ "exceptionList-components.exceptions.exceptionItem.card.metaDetailsBy": "par", "exceptionList-components.exceptions.exceptionItem.card.showCommentsLabel": "Afficher {comments, plural, =1 {commentaire} other {commentaires}} ({comments})", "exceptionList-components.exceptions.exceptionItem.card.updatedLabel": "Mis à jour", + "exceptionList-components.partialCodeSignatureCallout.body": "Veuillez vérifier les valeurs des champs, car vos critères de filtrage peuvent être incomplets. Nous recommandons d'inclure à la fois le nom du signataire et le statut de confiance (en utilisant l'opérateur « AND ») pour éviter d'éventuelles failles de sécurité.", + "exceptionList-components.partialCodeSignatureCallout.title": "Veuillez examiner vos entrées", "exceptionList-components.wildcardWithWrongOperatorCallout.body": "L'utilisation de \"*\" ou de \"?\" dans la valeur avec l'opérateur \"is\" peut rendre l'entrée inefficace. Remplacez {operator} par \"{matches}\" pour que les caractères génériques s'exécutent correctement.", "exceptionList-components.wildcardWithWrongOperatorCallout.changeTheOperator": "Changer d'opérateur", "exceptionList-components.wildcardWithWrongOperatorCallout.matches": "correspond à", "exceptionList-components.wildcardWithWrongOperatorCallout.title": "Veuillez examiner vos entrées", "expandableFlyout.previewSection.backButton": "Retour", "expandableFlyout.previewSection.closeButton": "Fermer", + "expandableFlyout.renderMenu.flyoutResizeButton": "Réinitialiser la taille", + "expandableFlyout.renderMenu.flyoutResizeTitle": "Taille du menu volant", + "expandableFlyout.settingsMenu.flyoutTypeTitle": "Type de menu volant", + "expandableFlyout.settingsMenu.overlayMode": "Superposer", + "expandableFlyout.settingsMenu.overlayTooltip": "Affiche le menu volant sur la page", + "expandableFlyout.settingsMenu.popoverButton": "Paramètres du menu volant", + "expandableFlyout.settingsMenu.popoverTitle": "Paramètres du menu volant", + "expandableFlyout.settingsMenu.pushMode": "Déploiement", + "expandableFlyout.settingsMenu.pushTooltip": "Affiche le menu volant à côté de la page", "expressionError.errorComponent.description": "Échec de l'expression avec le message :", "expressionError.errorComponent.title": "Oups ! Échec de l'expression", "expressionError.renderer.debug.displayName": "Déboguer", @@ -3027,6 +3533,7 @@ "expressionMetricVis.function.dimension.timeField": "Champ temporel", "expressionMetricVis.function.help": "Visualisation de l'indicateur", "expressionMetricVis.function.icon.help": "Fournit une icône de visualisation statique.", + "expressionMetricVis.function.iconAlign.help": "L'alignement de l'icône.", "expressionMetricVis.function.inspectorTableId.help": "ID pour le tableau de l'inspecteur", "expressionMetricVis.function.max.help.": "La dimension contenant la valeur maximale.", "expressionMetricVis.function.metric.help": "L’indicateur principal.", @@ -3037,7 +3544,10 @@ "expressionMetricVis.function.secondaryMetric.help": "L’indicateur secondaire (affiché au-dessus de l’indicateur principal).", "expressionMetricVis.function.secondaryPrefix.help": "Texte facultatif à afficher avant secondaryMetric.", "expressionMetricVis.function.subtitle.help": "Le sous-titre pour un indicateur unique. Remplacé si breakdownBy est spécifié.", + "expressionMetricVis.function.titlesTextAlign.help": "L'alignement du titre et du sous-titre.", "expressionMetricVis.function.trendline.help": "Configuration de la courbe de tendance facultative", + "expressionMetricVis.function.valueFontSize.help": "La taille de la police de valeur.", + "expressionMetricVis.function.valuesTextAlign.help": "L'alignement des indicateurs primaires et secondaires.", "expressionMetricVis.trendA11yDescription": "Graphique linéaire affichant la tendance de l'indicateur principal sur la durée.", "expressionMetricVis.trendA11yTitle": "{dataTitle} sur la durée.", "expressionMetricVis.trendline.function.breakdownBy.help": "La dimension contenant les étiquettes des sous-catégories.", @@ -3834,7 +4344,7 @@ "home.tutorials.ciscoLogs.longDescription": "Il s'agit d'un module pour les logs de dispositifs réseau Cisco (ASA, FTD, IOS, Nexus). Il inclut les ensembles de fichiers suivants pour la réception des logs par le biais de Syslog ou d'un ficher. [En savoir plus]({learnMoreLink}).", "home.tutorials.ciscoLogs.nameTitle": "Logs Cisco", "home.tutorials.ciscoLogs.shortDescription": "Collectez et analysez les logs à partir des périphériques réseau Cisco avec Filebeat.", - "home.tutorials.cloudwatchLogs.longDescription": "Collectez les logs Cloudwatch en déployant Functionbeat à des fins d'exécution en tant que fonction AWS Lambda. [En savoir plus]({learnMoreLink}).", + "home.tutorials.cloudwatchLogs.longDescription": "Collectez les logs Cloudwatch en déployant Functionbeat à des fins d'exécution en tant que fonction AWS Lambda.", "home.tutorials.cloudwatchLogs.nameTitle": "Logs Cloudwatch AWS", "home.tutorials.cloudwatchLogs.shortDescription": "Collectez et analysez les logs à partir d'AWS Cloudwatch avec Functionbeat.", "home.tutorials.cockroachdbMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs CockroachDB", @@ -3852,16 +4362,16 @@ "home.tutorials.common.auditbeatCloudInstructions.config.rpmTitle": "Modifier la configuration", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", "home.tutorials.common.auditbeatCloudInstructions.config.windowsTitle": "Modifier la configuration", - "home.tutorials.common.auditbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.auditbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.auditbeatInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.auditbeatInstructions.config.debTitle": "Modifier la configuration", - "home.tutorials.common.auditbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.auditbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.auditbeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.auditbeatInstructions.config.osxTitle": "Modifier la configuration", - "home.tutorials.common.auditbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.auditbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.auditbeatInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.auditbeatInstructions.config.rpmTitle": "Modifier la configuration", - "home.tutorials.common.auditbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.auditbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.auditbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.auditbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.auditbeatInstructions.install.debTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({linkUrl}).", @@ -3873,7 +4383,7 @@ "home.tutorials.common.auditbeatInstructions.install.rpmTextPre": "Vous utilisez Auditbeat pour la première fois ? Consultez le [guide de démarrage rapide]({linkUrl}).", "home.tutorials.common.auditbeatInstructions.install.rpmTitle": "Télécharger et installer Auditbeat", "home.tutorials.common.auditbeatInstructions.install.windowsTextPost": "Modifiez les paramètres sous {propertyName} dans le fichier {auditbeatPath} afin de pointer vers votre installation Elasticsearch.", - "home.tutorials.common.auditbeatInstructions.install.windowsTextPre": "Vous utilisez Auditbeat pour la première fois ? Consultez le [guide de démarrage rapide]({guideLinkUrl}).\n 1. Téléchargez le fichier .zip Auditbeat pour Windows via la page [Télécharger]({auditbeatLinkUrl}).\n 2. Extrayez le contenu du fichier compressé sous {folderPath}.\n 3. Renommez le répertoire `{directoryName}` en `Auditbeat`.\n 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell.\n 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Auditbeat en tant que service Windows.", + "home.tutorials.common.auditbeatInstructions.install.windowsTextPre": "Vous utilisez Auditbeat pour la première fois ? Consultez le [guide de démarrage rapide]({guideLinkUrl}). 1. Téléchargez le fichier .zip Auditbeat pour Windows via la page [Télécharger]({auditbeatLinkUrl}). 2. Extrayez le contenu du fichier compressé sous {folderPath}. 3. Renommez le répertoire `{directoryName}` en `Auditbeat`. 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell. 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Auditbeat en tant que service Windows.", "home.tutorials.common.auditbeatInstructions.install.windowsTitle": "Télécharger et installer Auditbeat", "home.tutorials.common.auditbeatInstructions.start.debTextPre": "La commande `setup` charge les tableaux de bord Kibana. Si les tableaux de bord sont déjà configurés, omettez cette commande.", "home.tutorials.common.auditbeatInstructions.start.debTitle": "Lancer Auditbeat", @@ -3909,16 +4419,16 @@ "home.tutorials.common.filebeatEnableInstructions.windowsTextPost": "Modifiez les paramètres dans le fichier `modules.d/{moduleName}.yml`. Vous devez activer au moins un ensemble de fichiers.", "home.tutorials.common.filebeatEnableInstructions.windowsTextPre": "Dans le dossier {path}, exécutez la commande suivante :", "home.tutorials.common.filebeatEnableInstructions.windowsTitle": "Activer et configurer le module {moduleName}", - "home.tutorials.common.filebeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.filebeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.filebeatInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.filebeatInstructions.config.debTitle": "Modifier la configuration", - "home.tutorials.common.filebeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.filebeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.filebeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.filebeatInstructions.config.osxTitle": "Modifier la configuration", - "home.tutorials.common.filebeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.filebeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.filebeatInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.filebeatInstructions.config.rpmTitle": "Modifier la configuration", - "home.tutorials.common.filebeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.filebeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.filebeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.filebeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.filebeatInstructions.install.debTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({linkUrl}).", @@ -3930,7 +4440,7 @@ "home.tutorials.common.filebeatInstructions.install.rpmTextPre": "Vous utilisez Filebeat pour la première fois ? Consultez le [guide de démarrage rapide]({linkUrl}).", "home.tutorials.common.filebeatInstructions.install.rpmTitle": "Télécharger et installer Filebeat", "home.tutorials.common.filebeatInstructions.install.windowsTextPost": "Modifiez les paramètres sous {propertyName} dans le fichier {filebeatPath} afin de pointer vers votre installation Elasticsearch.", - "home.tutorials.common.filebeatInstructions.install.windowsTextPre": "Vous utilisez Filebeat pour la première fois ? Consultez le [guide de démarrage rapide]({guideLinkUrl}).\n 1. Téléchargez le fichier .zip Filebeat pour Windows via la page [Télécharger]({filebeatLinkUrl}).\n 2. Extrayez le contenu du fichier compressé sous {folderPath}.\n 3. Renommez le répertoire `{directoryName}` en `Filebeat`.\n 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell.\n 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Filebeat en tant que service Windows.", + "home.tutorials.common.filebeatInstructions.install.windowsTextPre": "Vous utilisez Filebeat pour la première fois ? Consultez le [guide de démarrage rapide]({guideLinkUrl}). 1. Téléchargez le fichier .zip Filebeat pour Windows via la page [Télécharger]({filebeatLinkUrl}). 2. Extrayez le contenu du fichier compressé sous {folderPath}. 3. Renommez le répertoire `{directoryName}` en `Filebeat`. 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell. 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Filebeat en tant que service Windows.", "home.tutorials.common.filebeatInstructions.install.windowsTitle": "Télécharger et installer Filebeat", "home.tutorials.common.filebeatInstructions.start.debTextPre": "La commande `setup` charge les tableaux de bord Kibana. Si les tableaux de bord sont déjà configurés, omettez cette commande.", "home.tutorials.common.filebeatInstructions.start.debTitle": "Lancer Filebeat", @@ -3959,10 +4469,10 @@ "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTitle": "Configurer le groupe de logs Cloudwatch", "home.tutorials.common.functionbeatEnableOnPremInstructionsOSXLinux.textPre": "Modifiez les paramètres dans le fichier `functionbeat.yml`.", "home.tutorials.common.functionbeatEnableOnPremInstructionsWindows.textPre": "Modifiez les paramètres dans le fichier {path}.", - "home.tutorials.common.functionbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.functionbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.functionbeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.functionbeatInstructions.config.osxTitle": "Configurer le cluster Elastic", - "home.tutorials.common.functionbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.functionbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.functionbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.functionbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.functionbeatInstructions.deploy.osxTextPre": "Ceci permet d'installer Functionbeat en tant que fonction Lambda. La commande `setup` vérifie la configuration d'Elasticsearch et charge le modèle d'indexation Kibana. L'omission de cette commande est normalement sans risque.", @@ -3973,7 +4483,7 @@ "home.tutorials.common.functionbeatInstructions.install.linuxTitle": "Télécharger et installer Functionbeat", "home.tutorials.common.functionbeatInstructions.install.osxTextPre": "Vous utilisez Functionbeat pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", "home.tutorials.common.functionbeatInstructions.install.osxTitle": "Télécharger et installer Functionbeat", - "home.tutorials.common.functionbeatInstructions.install.windowsTextPre": "Vous utilisez Functionbeat pour la première fois ? Consultez le [guide de démarrage rapide]({functionbeatLink}).\n 1. Téléchargez le fichier .zip Functionbeat pour Windows via la page [Télécharger]({elasticLink}).\n 2. Extrayez le contenu du fichier compressé sous {folderPath}.\n 3. Renommez le répertoire `{directoryName}` en `Functionbeat`.\n 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell.\n 5. Depuis l'invite PowerShell, accédez au répertoire Functionbeat :", + "home.tutorials.common.functionbeatInstructions.install.windowsTextPre": "Vous utilisez Functionbeat pour la première fois ? Consultez le [guide de démarrage rapide]({functionbeatLink}). 1. Téléchargez le fichier .zip Functionbeat pour Windows via la page [Télécharger]({elasticLink}). 2. Extrayez le contenu du fichier compressé sous {folderPath}. 3. Renommez le répertoire `{directoryName}` en `Functionbeat`. 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell. 5. Depuis l'invite PowerShell, accédez au répertoire Functionbeat :", "home.tutorials.common.functionbeatInstructions.install.windowsTitle": "Télécharger et installer Functionbeat", "home.tutorials.common.functionbeatStatusCheck.buttonLabel": "Vérifier les données", "home.tutorials.common.functionbeatStatusCheck.errorText": "Aucune donnée n'a encore été reçue de Functionbeat.", @@ -4003,16 +4513,16 @@ "home.tutorials.common.heartbeatEnableOnPremInstructions.osxTextPre": "Modifiez le paramètre `heartbeat.monitors` dans le fichier `heartbeat.yml`.", "home.tutorials.common.heartbeatEnableOnPremInstructions.rpmTextPre": "Modifiez le paramètre `heartbeat.monitors` dans le fichier `heartbeat.yml`.", "home.tutorials.common.heartbeatEnableOnPremInstructions.windowsTextPre": "Modifiez le paramètre `heartbeat.monitors` dans le fichier `heartbeat.yml`.", - "home.tutorials.common.heartbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.heartbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.heartbeatInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.heartbeatInstructions.config.debTitle": "Modifier la configuration", - "home.tutorials.common.heartbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.heartbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.heartbeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.heartbeatInstructions.config.osxTitle": "Modifier la configuration", - "home.tutorials.common.heartbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n> **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.heartbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.heartbeatInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.heartbeatInstructions.config.rpmTitle": "Modifier la configuration", - "home.tutorials.common.heartbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.heartbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.heartbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.heartbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.heartbeatInstructions.install.debTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({link}).", @@ -4023,7 +4533,7 @@ "home.tutorials.common.heartbeatInstructions.install.rpmTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({link}).", "home.tutorials.common.heartbeatInstructions.install.rpmTextPre": "Vous utilisez Heartbeat pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", "home.tutorials.common.heartbeatInstructions.install.rpmTitle": "Télécharger et installer Heartbeat", - "home.tutorials.common.heartbeatInstructions.install.windowsTextPre": "Vous utilisez Heartbeat pour la première fois ? Consultez le [guide de démarrage rapide]({heartbeatLink}).\n 1. Téléchargez le fichier .zip Heartbeat pour Windows via la page [Télécharger]({elasticLink}).\n 2. Extrayez le contenu du fichier compressé sous {folderPath}.\n 3. Renommez le répertoire `{directoryName}` en `Heartbeat`.\n 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell.\n 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Heartbeat en tant que service Windows.", + "home.tutorials.common.heartbeatInstructions.install.windowsTextPre": "Vous utilisez Heartbeat pour la première fois ? Consultez le [guide de démarrage rapide]({heartbeatLink}). 1. Téléchargez le fichier .zip Heartbeat pour Windows via la page [Télécharger]({elasticLink}). 2. Extrayez le contenu du fichier compressé sous {folderPath}. 3. Renommez le répertoire `{directoryName}` en `Heartbeat`. 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell. 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Heartbeat en tant que service Windows.", "home.tutorials.common.heartbeatInstructions.install.windowsTitle": "Télécharger et installer Heartbeat", "home.tutorials.common.heartbeatInstructions.start.debTextPre": "La commande `setup` charge le modèle d'indexation Kibana.", "home.tutorials.common.heartbeatInstructions.start.debTitle": "Lancer Heartbeat", @@ -4042,9 +4552,9 @@ "home.tutorials.common.logstashInstructions.install.java.osxTitle": "Télécharger et installer l'environnement d'exécution Java", "home.tutorials.common.logstashInstructions.install.java.windowsTextPre": "Suivez les instructions d'installation [ici]({link}).", "home.tutorials.common.logstashInstructions.install.java.windowsTitle": "Télécharger et installer l'environnement d'exécution Java", - "home.tutorials.common.logstashInstructions.install.logstash.osxTextPre": "Vous utilisez Logstash pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", + "home.tutorials.common.logstashInstructions.install.logstash.osxTextPre": "Vous utilisez Logstash pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", "home.tutorials.common.logstashInstructions.install.logstash.osxTitle": "Télécharger et installer Logstash", - "home.tutorials.common.logstashInstructions.install.logstash.windowsTextPre": "Vous utilisez Logstash pour la première fois ? Consultez le [guide de démarrage rapide]({logstashLink}).\n 1. [Téléchargez]({elasticLink}) le fichier .zip Logstash pour Windows.\n 2. Extrayez le contenu du fichier compressé.", + "home.tutorials.common.logstashInstructions.install.logstash.windowsTextPre": "Vous utilisez Logstash pour la première fois ? Consultez le [guide de démarrage rapide]({logstashLink}). 1. [Téléchargez]({elasticLink}) le fichier .zip Logstash pour Windows. 2. Extrayez le contenu du fichier compressé.", "home.tutorials.common.logstashInstructions.install.logstash.windowsTitle": "Télécharger et installer Logstash", "home.tutorials.common.metricbeat.cloudInstructions.gettingStarted.title": "Commencer", "home.tutorials.common.metricbeat.premCloudInstructions.gettingStarted.title": "Commencer", @@ -4067,16 +4577,16 @@ "home.tutorials.common.metricbeatEnableInstructions.windowsTextPost": "Modifiez les paramètres dans le fichier `modules.d/{moduleName}.yml`.", "home.tutorials.common.metricbeatEnableInstructions.windowsTextPre": "Dans le dossier {path}, exécutez la commande suivante :", "home.tutorials.common.metricbeatEnableInstructions.windowsTitle": "Activer et configurer le module {moduleName}", - "home.tutorials.common.metricbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.metricbeatInstructions.config.debTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.metricbeatInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.metricbeatInstructions.config.debTitle": "Modifier la configuration", - "home.tutorials.common.metricbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.metricbeatInstructions.config.osxTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.metricbeatInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.metricbeatInstructions.config.osxTitle": "Modifier la configuration", - "home.tutorials.common.metricbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.metricbeatInstructions.config.rpmTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.metricbeatInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.metricbeatInstructions.config.rpmTitle": "Modifier la configuration", - "home.tutorials.common.metricbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.metricbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.metricbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.metricbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.metricbeatInstructions.install.debTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({link}).", @@ -4088,7 +4598,7 @@ "home.tutorials.common.metricbeatInstructions.install.rpmTextPre": "Vous utilisez Metricbeat pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", "home.tutorials.common.metricbeatInstructions.install.rpmTitle": "Télécharger et installer Metricbeat", "home.tutorials.common.metricbeatInstructions.install.windowsTextPost": "Modifiez les paramètres sous `output.elasticsearch` dans le fichier {path} afin de pointer vers votre installation Elasticsearch.", - "home.tutorials.common.metricbeatInstructions.install.windowsTextPre": "Vous utilisez Metricbeat pour la première fois ? Consultez le [guide de démarrage rapide]({metricbeatLink}).\n 1. Téléchargez le fichier .zip Metricbeat pour Windows via la page [Télécharger]({elasticLink}).\n 2. Extrayez le contenu du fichier compressé sous {folderPath}.\n 3. Renommez le répertoire `{directoryName}` en `Metricbeat`.\n 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell.\n 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Metricbeat en tant que service Windows.", + "home.tutorials.common.metricbeatInstructions.install.windowsTextPre": "Vous utilisez Metricbeat pour la première fois ? Consultez le [guide de démarrage rapide]({metricbeatLink}). 1. Téléchargez le fichier .zip Metricbeat pour Windows via la page [Télécharger]({elasticLink}). 2. Extrayez le contenu du fichier compressé sous {folderPath}. 3. Renommez le répertoire `{directoryName}` en `Metricbeat`. 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell. 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Metricbeat en tant que service Windows.", "home.tutorials.common.metricbeatInstructions.install.windowsTitle": "Télécharger et installer Metricbeat", "home.tutorials.common.metricbeatInstructions.start.debTextPre": "La commande `setup` charge les tableaux de bord Kibana. Si les tableaux de bord sont déjà configurés, omettez cette commande.", "home.tutorials.common.metricbeatInstructions.start.debTitle": "Lancer Metricbeat", @@ -4103,20 +4613,20 @@ "home.tutorials.common.metricbeatStatusCheck.successText": "Des données ont été reçues de ce module.", "home.tutorials.common.metricbeatStatusCheck.text": "Vérifier que des données sont reçues du module Metricbeat `{moduleName}`", "home.tutorials.common.metricbeatStatusCheck.title": "Statut du module", - "home.tutorials.common.premCloudInstructions.option1.textPre": "Rendez-vous sur [Elastic Cloud]({link}). Enregistrez-vous si vous n'avez pas encore de compte. Un essai gratuit de 14 jours est disponible.\n\nConnectez-vous à la console Elastic Cloud.\n\nPour créer un cluster, dans la console Elastic Cloud :\n 1. Sélectionnez **Créer un déploiement** et spécifiez le **Nom du déploiement**.\n 2. Modifiez les autres options de déploiement selon les besoins (sinon, les valeurs par défaut sont très bien pour commencer).\n 3. Cliquer sur **Créer un déploiement**\n 4. Attendre la fin de la création du déploiement\n 5. Accéder à la nouvelle instance cloud Kibana et suivre les instructions de la page d'accueil de Kibana", + "home.tutorials.common.premCloudInstructions.option1.textPre": "Rendez-vous sur [Elastic Cloud]({link}). Enregistrez-vous si vous n'avez pas encore de compte. Un essai gratuit de 14 jours est disponible. Connectez-vous à la console Elastic Cloud Pour créer un cluster, il suffit de : 1. Sélectionner **Créer un déploiement** et spécifier le **Nom du déploiement** 2. Modifier les autres options de déploiement selon les besoins (sinon, les valeurs par défaut sont très bien pour commencer) 3. Cliquer sur **Créer un déploiement** 4. Attendre la fin de la création du déploiement 5. Accéder à la nouvelle instance cloud Kibana et suivre les instructions de la page d'accueil de Kibana", "home.tutorials.common.premCloudInstructions.option1.title": "Option 1 : essayer dans Elastic Cloud", - "home.tutorials.common.premCloudInstructions.option2.textPre": "Si vous exécutez cette instance Kibana sur une instance Elasticsearch hébergée, passez à la configuration manuelle.\n\nEnregistrez le point de terminaison **Elasticsearch** en tant que {urlTemplate} et le cluster **Mot de passe** en tant que {passwordTemplate} pour les conserver.", + "home.tutorials.common.premCloudInstructions.option2.textPre": "Si vous exécutez cette instance Kibana sur une instance Elasticsearch hébergée, passez à la configuration manuelle. Enregistrez le point de terminaison **Elasticsearch** en tant que {urlTemplate} et le cluster **Mot de passe** en tant que {passwordTemplate} pour les conserver.", "home.tutorials.common.premCloudInstructions.option2.title": "Option 2 : connecter un Kibana local à une instance cloud", "home.tutorials.common.winlogbeat.cloudInstructions.gettingStarted.title": "Premiers pas", "home.tutorials.common.winlogbeat.premCloudInstructions.gettingStarted.title": "Commencer", "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "Commencer", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", "home.tutorials.common.winlogbeatCloudInstructions.config.windowsTitle": "Modifier la configuration", - "home.tutorials.common.winlogbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur `elastic`, {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer le SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte digitale dans {esCertFingerprintTemplate}.\n\n > **_Important :_** n'employez pas l'utilisateur `elastic` intégré pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", + "home.tutorials.common.winlogbeatInstructions.config.windowsTextPostMarkdown": "Où {passwordTemplate} est le mot de passe de l'utilisateur \"elastic\", {esUrlTemplate} est l'URL d'Elasticsearch et {kibanaUrlTemplate} est l'URL de Kibana. Pour [configurer SSL]({configureSslUrl}) avec le certificat par défaut généré par Elasticsearch, ajoutez son empreinte dans {esCertFingerprintTemplate}. > **_Important :_** N'utilisez pas l'utilisateur intégré `elastic` pour sécuriser les clients dans un environnement de production. À la place, configurez des utilisateurs autorisés ou des clés d'API, et n'exposez pas les mots de passe dans les fichiers de configuration. [Learn more]({linkUrl}).", "home.tutorials.common.winlogbeatInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion :", "home.tutorials.common.winlogbeatInstructions.config.windowsTitle": "Modifier la configuration", "home.tutorials.common.winlogbeatInstructions.install.windowsTextPost": "Modifiez les paramètres sous `output.elasticsearch` dans le fichier {path} afin de pointer vers votre installation Elasticsearch.", - "home.tutorials.common.winlogbeatInstructions.install.windowsTextPre": "Vous utilisez Winlogbeat pour la première fois ? Consultez le [guide de démarrage rapide]({winlogbeatLink}).\n 1. Téléchargez le fichier .zip Winlogbeat pour Windows via la page [Télécharger]({elasticLink}).\n 2. Extrayez le contenu du fichier compressé sous {folderPath}.\n 3. Renommez le répertoire `{directoryName}` en `Winlogbeat`.\n 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell.\n 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Winlogbeat en tant que service Windows.", + "home.tutorials.common.winlogbeatInstructions.install.windowsTextPre": "Vous utilisez Winlogbeat pour la première fois ? Consultez le [guide de démarrage rapide]({winlogbeatLink}). 1. Téléchargez le fichier .zip Winlogbeat pour Windows via la page [Télécharger]({elasticLink}). 2. Extrayez le contenu du fichier compressé sous {folderPath}. 3. Renommez le répertoire `{directoryName}` en `Winlogbeat`. 4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell. 5. Dans l'invite PowerShell, exécutez les commandes suivantes afin d'installer Winlogbeat en tant que service Windows.", "home.tutorials.common.winlogbeatInstructions.install.windowsTitle": "Télécharger et installer Winlogbeat", "home.tutorials.common.winlogbeatInstructions.start.windowsTextPre": "La commande `setup` charge les tableaux de bord Kibana. Si les tableaux de bord sont déjà configurés, omettez cette commande.", "home.tutorials.common.winlogbeatInstructions.start.windowsTitle": "Lancer Winlogbeat", @@ -4146,7 +4656,7 @@ "home.tutorials.couchdbMetrics.nameTitle": "Indicateurs CouchDB", "home.tutorials.couchdbMetrics.shortDescription": "Collectez les indicateurs à partir des serveurs CouchDB avec Metricbeat.", "home.tutorials.crowdstrikeLogs.artifacts.dashboards.linkLabel": "Application Security", - "home.tutorials.crowdstrikeLogs.longDescription": "Il s'agit du module Filebeat pour CrowdStrike Falcon utilisant le [connecteur SIEM](https://www.crowdstrike.com/blog/tech-center/integrate-with-your-siem) Falcon. Ce module collecte ces données, les convertit en ECS et les ingère pour les afficher dans le SIEM. Par défaut, le connecteur SIEM Falcon génère les données d'événement de l'API de streaming Falcon au format JSON. [En savoir plus]({learnMoreLink}).", + "home.tutorials.crowdstrikeLogs.longDescription": "Il s'agit du module Filebeat pour CrowdStrike Falcon utilisant le [connecteur SIEM](https://www.crowdstrike.com/blog/tech-center/integrate-with-your-siem) Falcon. Ce module collecte ces données, les convertit en ECS et les ingère pour les afficher dans le SIEM. Par défaut, le connecteur SIEM Falcon génère les données d'événement de l'API de streaming Falcon au format JSON. [En savoir plus]({learnMoreLink}).", "home.tutorials.crowdstrikeLogs.nameTitle": "Logs CrowdStrike", "home.tutorials.crowdstrikeLogs.shortDescription": "Collectez et analysez les logs à partir de CrowdStrike Falcon à l'aide du Falcon SIEM Connector avec Filebeat.", "home.tutorials.cylanceLogs.artifacts.dashboards.linkLabel": "Application Security", @@ -4345,7 +4855,7 @@ "home.tutorials.o365Logs.nameTitle": "Logs Office 365", "home.tutorials.o365Logs.shortDescription": "Collectez et analysez les logs à partir d'Office 365 avec Filebeat.", "home.tutorials.oktaLogs.artifacts.dashboards.linkLabel": "Aperçu d'Okta", - "home.tutorials.oktaLogs.longDescription": "Le module Okta collecte les événements de l'[API Okta](https://developer.okta.com/docs/reference/). Plus précisément, il prend en charge la lecture depuis l'[API de log système Okta](https://developer.okta.com/docs/reference/api/system-log/). [En savoir plus]({learnMoreLink}).", + "home.tutorials.oktaLogs.longDescription": "Le module Okta collecte les événements de l'[API Okta](https://developer.okta.com/docs/reference/). Plus précisément, il prend en charge la lecture depuis l'[API de log système Okta](https://developer.okta.com/docs/reference/api/system-log/). [En savoir plus]({learnMoreLink}).", "home.tutorials.oktaLogs.nameTitle": "Logs Okta", "home.tutorials.oktaLogs.shortDescription": "Collectez et analysez les logs à partir de l'API Okta avec Filebeat.", "home.tutorials.openmetricsMetrics.longDescription": "Le module Metricbeat `openmetrics` récupère des indicateurs depuis un point de terminaison fournissant des indicateurs au format OpenMetrics. [En savoir plus]({learnMoreLink}).", @@ -4356,7 +4866,7 @@ "home.tutorials.oracleMetrics.nameTitle": "Indicateurs Oracle", "home.tutorials.oracleMetrics.shortDescription": "Collectez les indicateurs à partir de serveurs Oracle avec Metricbeat.", "home.tutorials.osqueryLogs.artifacts.dashboards.linkLabel": "Pack de conformité osquery", - "home.tutorials.osqueryLogs.longDescription": "Le module collecte et décode les logs de résultats écrits par [osqueryd](https://osquery.readthedocs.io/en/latest/introduction/using-osqueryd/) au format JSON. Pour configurer `osqueryd`, suivez les instructions d'installation d'osquery pour votre système d'exploitation et configurez le pilote de logging `filesystem` (celui par défaut). Assurez-vous que les horodatages UTC sont activés. [En savoir plus]({learnMoreLink}).", + "home.tutorials.osqueryLogs.longDescription": "Le module collecte et décode les logs de résultats écrits par [osqueryd](https://osquery.readthedocs.io/en/latest/introduction/using-osqueryd/) au format JSON. Pour configurer `osqueryd`, suivez les instructions d'installation d'osquery pour votre système d'exploitation et configurez le pilote de logging `filesystem` (celui par défaut). Assurez-vous que les horodatages UTC sont activés. [En savoir plus]({learnMoreLink}).", "home.tutorials.osqueryLogs.nameTitle": "Logs osquery", "home.tutorials.osqueryLogs.shortDescription": "Collectez et analysez les logs à partir d'Osquery avec Filebeat.", "home.tutorials.panwLogs.artifacts.dashboards.linkLabel": "Flux de réseau PANW", @@ -4420,7 +4930,7 @@ "home.tutorials.statsdMetrics.nameTitle": "Indicateurs statsd", "home.tutorials.statsdMetrics.shortDescription": "Collectez les indicateurs à partir de serveurs Statsd avec Metricbeat.", "home.tutorials.suricataLogs.artifacts.dashboards.linkLabel": "Aperçu des événements Suricata", - "home.tutorials.suricataLogs.longDescription": "Il s'agit d'un module pour le log IDS/IPS/NSM Suricata. Il analyse les logs qui sont au [format JSON Suricata Eve](https://suricata.readthedocs.io/en/latest/output/eve/eve-json-format.html). [En savoir plus]({learnMoreLink}).", + "home.tutorials.suricataLogs.longDescription": "Il s'agit d'un module pour le log IDS/IPS/NSM Suricata. Il analyse les logs qui sont au [format JSON Suricata Eve](https://suricata.readthedocs.io/en/latest/output/eve/eve-json-format.html). [En savoir plus]({learnMoreLink}).", "home.tutorials.suricataLogs.nameTitle": "Logs Suricata", "home.tutorials.suricataLogs.shortDescription": "Collectez et analysez les logs à partir de Suricata IDS/IPS/NSM avec Filebeat.", "home.tutorials.systemLogs.artifacts.dashboards.linkLabel": "Tableau de bord Syslog système", @@ -4443,7 +4953,7 @@ "home.tutorials.traefikMetrics.nameTitle": "Indicateurs Traefik", "home.tutorials.traefikMetrics.shortDescription": "Collectez les indicateurs à partir de Traefik avec Metricbeat.", "home.tutorials.uptimeMonitors.artifacts.dashboards.linkLabel": "Application Uptime", - "home.tutorials.uptimeMonitors.longDescription": "Monitorez la disponibilité des services grâce à une détection active. À partir d'une liste d'URL, Heartbeat pose cette question toute simple : Êtes-vous actif ? [En savoir plus]({learnMoreLink}).", + "home.tutorials.uptimeMonitors.longDescription": "Monitorez la disponibilité des services grâce à une détection active. À partir d'une liste d'URL, Heartbeat pose cette question toute simple : Êtes-vous actif ? [En savoir plus]({learnMoreLink}).", "home.tutorials.uptimeMonitors.nameTitle": "Monitorings Uptime", "home.tutorials.uptimeMonitors.shortDescription": "Surveillez la disponibilité des services avec Heartbeat.", "home.tutorials.uwsgiMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs uWSGI", @@ -4463,7 +4973,7 @@ "home.tutorials.windowsMetrics.nameTitle": "Indicateurs Windows", "home.tutorials.windowsMetrics.shortDescription": "Collectez les indicateurs à partir de Windows avec Metricbeat.", "home.tutorials.zeekLogs.artifacts.dashboards.linkLabel": "Aperçu de Zeek", - "home.tutorials.zeekLogs.longDescription": "Il s'agit d'un module pour Zeek, anciennement appelé Bro. Il analyse les logs qui sont au [format JSON Zeek](https://www.zeek.org/manual/release/logs/index.html). [En savoir plus]({learnMoreLink}).", + "home.tutorials.zeekLogs.longDescription": "Il s'agit d'un module pour Zeek, anciennement appelé Bro. Il analyse les logs qui sont au [format JSON Zeek](https://www.zeek.org/manual/release/logs/index.html). [En savoir plus]({learnMoreLink}).", "home.tutorials.zeekLogs.nameTitle": "Logs Zeek", "home.tutorials.zeekLogs.shortDescription": "Collectez et analysez les logs à partir de la sécurité réseau Zeek avec Filebeat.", "home.tutorials.zookeeperMetrics.artifacts.application.label": "Discover", @@ -4573,6 +5083,9 @@ "indexPatternEditor.rollup.uncaughtError": "Erreur de vue de données de cumul : {error}", "indexPatternEditor.rollupDataView.createIndex.noMatchError": "Erreur de vue de données de cumul : doit correspondre à un index de cumul", "indexPatternEditor.rollupDataView.createIndex.tooManyMatchesError": "Erreur de vue de données de cumul : ne peut correspondre qu’à un index de cumul", + "indexPatternEditor.rollupDataView.deprecationWarning.downsamplingLink": "Sous-échantillonnage", + "indexPatternEditor.rollupDataView.deprecationWarning.textParagraphOne": "Les cumuls sont obsolètes et seront supprimés dans une version ultérieure. {downsamplingLink} peut être utilisé comme solution de rechange.", + "indexPatternEditor.rollupIndexPattern.deprecationWarning.title": "Déclassé dans la version 8.11.0", "indexPatternEditor.saved": "Enregistré", "indexPatternEditor.status.matchAnyLabel.matchAnyDetail": "Votre modèle d'indexation peut correspondre à {sourceCount, plural, one {# source} other {# sources} }.", "indexPatternEditor.status.noSystemIndicesLabel": "Aucun flux de données, index ni alias d'index ne correspond à votre modèle d'indexation.", @@ -4663,7 +5176,7 @@ "indexPatternFieldEditor.editor.form.formatTitle": "Définir le format", "indexPatternFieldEditor.editor.form.nameAriaLabel": "Champ Nom", "indexPatternFieldEditor.editor.form.nameLabel": "Nom", - "indexPatternFieldEditor.editor.form.popularityDescription": "Définissez la popularité pour que le champ apparaisse plus haut ou plus bas dans la liste des champs. Par défaut, Discover classe les champs du plus souvent sélectionné au moins souvent sélectionné.", + "indexPatternFieldEditor.editor.form.popularityDescription": "Définissez la popularité pour que le champ apparaisse plus haut ou plus bas dans la liste des champs. Par défaut, Discover classe les champs du plus souvent sélectionné au moins souvent sélectionné.", "indexPatternFieldEditor.editor.form.popularityLabel": "Popularité", "indexPatternFieldEditor.editor.form.popularityTitle": "Définir la popularité", "indexPatternFieldEditor.editor.form.runtimeType.placeholderLabel": "Sélectionner un type", @@ -5067,7 +5580,7 @@ "inspector.requests.clustersTabLabel": "Clusters et partitions", "inspector.requests.copyToClipboardLabel": "Copier dans le presse-papiers", "inspector.requests.descriptionRowIconAriaLabel": "Description", - "inspector.requests.failedLabel": " (échec)", + "inspector.requests.failedLabel": "(échec)", "inspector.requests.noRequestsLoggedDescription.elementHasNotLoggedAnyRequestsText": "L'élément n'a pas (encore) consigné de requêtes.", "inspector.requests.noRequestsLoggedDescription.whatDoesItUsuallyMeanText": "Cela signifie généralement qu'il n'était pas nécessaire de récupérer des données ou que l'élément n'a pas encore commencé à récupérer des données.", "inspector.requests.noRequestsLoggedTitle": "Aucune requête consignée", @@ -5155,7 +5668,12 @@ "interactiveSetup.verificationCodeForm.submitButton": "{isSubmitting, select, true{Vérification…} other{Vérifier}}", "interactiveSetup.verificationCodeForm.submitErrorTitle": "Vérification du code impossible", "interactiveSetup.verificationCodeForm.title": "Vérification requise", + "kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogram": "Ajouter un histogramme des dates", + "kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogramDetail": "Ajouter un histogramme des dates en utilisant bucket()", + "kbn-esql-validation-autocomplete.esql.autocomplete.allStarConstantDoc": "Tous (*)", "kbn-esql-validation-autocomplete.esql.autocomplete.aPatternString": "Une chaîne modèle", + "kbn-esql-validation-autocomplete.esql.autocomplete.chooseFromTimePicker": "Cliquez pour choisir", + "kbn-esql-validation-autocomplete.esql.autocomplete.chooseFromTimePickerLabel": "Choisissez parmi le sélecteur de période", "kbn-esql-validation-autocomplete.esql.autocomplete.colonDoc": "Deux points (:)", "kbn-esql-validation-autocomplete.esql.autocomplete.commaDoc": "Virgule (,)", "kbn-esql-validation-autocomplete.esql.autocomplete.constantDefinition": "Constant", @@ -5166,11 +5684,15 @@ "kbn-esql-validation-autocomplete.esql.autocomplete.integrationDefinition": "Intégration", "kbn-esql-validation-autocomplete.esql.autocomplete.listDoc": "Liste d'éléments (…)", "kbn-esql-validation-autocomplete.esql.autocomplete.matchingFieldDefinition": "Utiliser pour correspondance avec {matchingField} de la politique", + "kbn-esql-validation-autocomplete.esql.autocomplete.namedParamDefinition": "Paramètre nommé", "kbn-esql-validation-autocomplete.esql.autocomplete.newVarDoc": "Définir une nouvelle variable", "kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabel": "Pas de stratégie disponible", "kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabelsFound": "Cliquez pour créer", "kbn-esql-validation-autocomplete.esql.autocomplete.pipeDoc": "Barre verticale (|)", "kbn-esql-validation-autocomplete.esql.autocomplete.semiColonDoc": "Point-virgule (;)", + "kbn-esql-validation-autocomplete.esql.autocomplete.sourceDefinition": "{type}", + "kbn-esql-validation-autocomplete.esql.autocomplete.timeSystemParamEnd": "L'heure de fin à partir du sélecteur de date", + "kbn-esql-validation-autocomplete.esql.autocomplete.timeSystemParamStart": "L'heure de début à partir du sélecteur de date", "kbn-esql-validation-autocomplete.esql.autocomplete.valueDefinition": "Valeur littérale", "kbn-esql-validation-autocomplete.esql.autocomplete.variableDefinition": "Variable spécifiée par l'utilisateur dans la requête ES|QL", "kbn-esql-validation-autocomplete.esql.definition.addDoc": "Ajouter (+)", @@ -5181,7 +5703,7 @@ "kbn-esql-validation-autocomplete.esql.definition.greaterThanDoc": "Supérieur à", "kbn-esql-validation-autocomplete.esql.definition.greaterThanOrEqualToDoc": "Supérieur ou égal à", "kbn-esql-validation-autocomplete.esql.definition.inDoc": "Teste si la valeur d'une expression est contenue dans une liste d'autres expressions", - "kbn-esql-validation-autocomplete.esql.definition.infoDoc": "Afficher des informations sur le nœud ES actuel", + "kbn-esql-validation-autocomplete.esql.definition.infoDoc": "Spécifier les modificateurs de tri des colonnes", "kbn-esql-validation-autocomplete.esql.definition.isNotNullDoc": "Prédicat pour la comparaison NULL : renvoie \"true\" si la valeur n'est pas NULL", "kbn-esql-validation-autocomplete.esql.definition.isNullDoc": "Prédicat pour la comparaison NULL : renvoie \"true\" si la valeur est NULL", "kbn-esql-validation-autocomplete.esql.definition.lessThanDoc": "Inférieur à", @@ -5198,13 +5720,15 @@ "kbn-esql-validation-autocomplete.esql.definitions.acos": "Renvoie l'arc cosinus de `n` sous forme d'angle, exprimé en radians.", "kbn-esql-validation-autocomplete.esql.definitions.appendSeparatorDoc": "Le ou les caractères qui séparent les champs ajoutés. A pour valeur par défaut une chaîne vide (\"\").", "kbn-esql-validation-autocomplete.esql.definitions.asDoc": "En tant que", - "kbn-esql-validation-autocomplete.esql.definitions.asin": "Renvoie l'arc sinus de l'entrée\nexpression numérique sous forme d'angle, exprimée en radians.", - "kbn-esql-validation-autocomplete.esql.definitions.atan": "Renvoie l'arc tangente de l'entrée\nexpression numérique sous forme d'angle, exprimée en radians.", - "kbn-esql-validation-autocomplete.esql.definitions.atan2": "L'angle entre l'axe positif des x et le rayon allant de\nl'origine au point (x , y) dans le plan cartésien, exprimée en radians.", + "kbn-esql-validation-autocomplete.esql.definitions.asin": "Renvoie l'arc sinus de l'expression numérique sous forme d'angle, exprimé en radians.", + "kbn-esql-validation-autocomplete.esql.definitions.atan": "Renvoie l'arc tangente de l'expression numérique sous forme d'angle, exprimé en radians.", + "kbn-esql-validation-autocomplete.esql.definitions.atan2": "L'angle entre l'axe positif des x et le rayon allant de l'origine au point (x , y) dans le plan cartésien, exprimé en radians.", "kbn-esql-validation-autocomplete.esql.definitions.autoBucketDoc": "Groupement automatique des dates en fonction d'une plage et d'un compartiment cible donnés.", + "kbn-esql-validation-autocomplete.esql.definitions.avg": "La moyenne d'un champ numérique.", "kbn-esql-validation-autocomplete.esql.definitions.byDoc": "Par", "kbn-esql-validation-autocomplete.esql.definitions.case": "Accepte les paires de conditions et de valeurs. La fonction renvoie la valeur correspondant à la première condition évaluée à `true` (vraie). Si le nombre d'arguments est impair, le dernier argument est la valeur par défaut qui est renvoyée si aucune condition ne correspond.", - "kbn-esql-validation-autocomplete.esql.definitions.cbrt": "Renvoie la racine cubique d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\nLa racine cubique de l’infini est nulle.", + "kbn-esql-validation-autocomplete.esql.definitions.categorize": "Catégorise les messages texte.", + "kbn-esql-validation-autocomplete.esql.definitions.cbrt": "Renvoie la racine cubique d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. La racine cubique de l’infini est nulle.", "kbn-esql-validation-autocomplete.esql.definitions.ccqAnyDoc": "L'enrichissement a lieu sur n'importe quel cluster", "kbn-esql-validation-autocomplete.esql.definitions.ccqCoordinatorDoc": "L'enrichissement a lieu sur le cluster de coordination qui reçoit une requête ES|QL", "kbn-esql-validation-autocomplete.esql.definitions.ccqModeDoc": "Mode de requête inter-clusters", @@ -5214,8 +5738,10 @@ "kbn-esql-validation-autocomplete.esql.definitions.coalesce": "Renvoie le premier de ses arguments qui n'est pas nul. Si tous les arguments sont nuls, `null` est renvoyé.", "kbn-esql-validation-autocomplete.esql.definitions.concat": "Concatène deux ou plusieurs chaînes.", "kbn-esql-validation-autocomplete.esql.definitions.cos": "Renvoie le cosinus d'un angle.", - "kbn-esql-validation-autocomplete.esql.definitions.cosh": "Renvoie le cosinus hyperbolique d'un angle.", - "kbn-esql-validation-autocomplete.esql.definitions.date_diff": "Soustrait le `startTimestamp` du `endTimestamp` et renvoie la différence en multiples d'`unité`.\nSi `startTimestamp` est postérieur à `endTimestamp`, des valeurs négatives sont renvoyées.", + "kbn-esql-validation-autocomplete.esql.definitions.cosh": "Renvoie le cosinus hyperbolique d'un nombre.", + "kbn-esql-validation-autocomplete.esql.definitions.count": "Renvoie le nombre total de valeurs en entrée.", + "kbn-esql-validation-autocomplete.esql.definitions.count_distinct": "Renvoie le nombre approximatif de valeurs distinctes.", + "kbn-esql-validation-autocomplete.esql.definitions.date_diff": "Soustrait le `startTimestamp` du `endTimestamp` et renvoie la différence en multiples d'`unité`. Si `startTimestamp` est postérieur à `endTimestamp`, des valeurs négatives sont renvoyées.", "kbn-esql-validation-autocomplete.esql.definitions.date_extract": "Extrait des parties d'une date, telles que l'année, le mois, le jour, l'heure.", "kbn-esql-validation-autocomplete.esql.definitions.date_format": "Renvoie une représentation sous forme de chaîne d'une date dans le format fourni.", "kbn-esql-validation-autocomplete.esql.definitions.date_parse": "Renvoie une date en analysant le deuxième argument selon le format spécifié dans le premier argument.", @@ -5244,34 +5770,44 @@ "kbn-esql-validation-autocomplete.esql.definitions.ends_with": "Renvoie une valeur booléenne qui indique si une chaîne de mots-clés se termine par une autre chaîne.", "kbn-esql-validation-autocomplete.esql.definitions.enrichDoc": "Enrichissez le tableau à l'aide d'un autre tableau. Avant de pouvoir utiliser l'enrichissement, vous devez créer et exécuter une politique d'enrichissement.", "kbn-esql-validation-autocomplete.esql.definitions.evalDoc": "Calcule une expression et place la valeur résultante dans un champ de résultats de recherche.", + "kbn-esql-validation-autocomplete.esql.definitions.exp": "Renvoie la valeur de \"e\" élevée à la puissance d'un nombre donné.", "kbn-esql-validation-autocomplete.esql.definitions.floor": "Arrondir un nombre à l'entier inférieur.", "kbn-esql-validation-autocomplete.esql.definitions.from_base64": "Décodez une chaîne base64.", "kbn-esql-validation-autocomplete.esql.definitions.fromDoc": "Récupère les données à partir d'un ou plusieurs flux de données, index ou alias. Dans une requête ou une sous-requête, vous devez utiliser d'abord la commande from, et cette dernière ne nécessite pas de barre verticale au début. Par exemple, pour récupérer des données d'un index :", - "kbn-esql-validation-autocomplete.esql.definitions.greatest": "Renvoie la valeur maximale de plusieurs colonnes. Similaire à `MV_MAX`\nsauf que ceci est destiné à une exécution sur plusieurs colonnes à la fois.", + "kbn-esql-validation-autocomplete.esql.definitions.greatest": "Renvoie la valeur maximale de plusieurs colonnes. Cette fonction est similaire à `MV_MAX`. Toutefois, elle est destinée à être exécutée sur plusieurs colonnes à la fois.", "kbn-esql-validation-autocomplete.esql.definitions.grokDoc": "Extrait de multiples valeurs de chaîne à partir d'une entrée de chaîne unique, suivant un modèle", + "kbn-esql-validation-autocomplete.esql.definitions.hypot": "Renvoie l'hypoténuse de deux nombres. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. Les hypoténuses des infinis sont nulles.", + "kbn-esql-validation-autocomplete.esql.definitions.inlineStatsDoc": "Calcule un résultat agrégé et fusionne ce résultat dans le flux de données d'entrée. Sans la clause facultative `BY`, cela produira un résultat unique qui sera ajouté à chaque ligne. Avec une clause `BY`, cela produira un résultat par regroupement et fusionnera le résultat dans le flux en fonction des clés de groupe correspondantes.", "kbn-esql-validation-autocomplete.esql.definitions.ip_prefix": "Tronque une adresse IP à une longueur de préfixe donnée.", "kbn-esql-validation-autocomplete.esql.definitions.keepDoc": "Réarrange les champs dans le tableau d'entrée en appliquant les clauses \"KEEP\" dans les champs", "kbn-esql-validation-autocomplete.esql.definitions.least": "Renvoie la valeur minimale de plusieurs colonnes. Cette fonction est similaire à `MV_MIN`. Toutefois, elle est destinée à être exécutée sur plusieurs colonnes à la fois.", "kbn-esql-validation-autocomplete.esql.definitions.left": "Renvoie la sous-chaîne qui extrait la 'longueur' des caractères de la 'chaîne' en partant de la gauche.", "kbn-esql-validation-autocomplete.esql.definitions.length": "Renvoie la longueur des caractères d'une chaîne.", "kbn-esql-validation-autocomplete.esql.definitions.limitDoc": "Renvoie les premiers résultats de recherche, dans l'ordre de recherche, en fonction de la \"limite\" spécifiée.", - "kbn-esql-validation-autocomplete.esql.definitions.locate": "Renvoie un entier qui indique la position d'une sous-chaîne de mots-clés dans une autre chaîne", - "kbn-esql-validation-autocomplete.esql.definitions.log": "Renvoie le logarithme d'une valeur dans une base. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n\nLes journaux de zéros, de nombres négatifs et de base 1 renvoient `null` ainsi qu'un avertissement.", - "kbn-esql-validation-autocomplete.esql.definitions.log10": "Renvoie le logarithme d'une valeur en base 10. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n\nLes logs de 0 et de nombres négatifs renvoient `null` ainsi qu'un avertissement.", + "kbn-esql-validation-autocomplete.esql.definitions.locate": "Renvoie un entier qui indique la position d'une sous-chaîne de mots-clés dans une autre chaîne. Renvoie `0` si la sous-chaîne ne peut pas être trouvée. Notez que les positions des chaînes commencent à partir de `1`.", + "kbn-esql-validation-autocomplete.esql.definitions.log": "Renvoie le logarithme d'une valeur dans une base. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. Les journaux de zéros, de nombres négatifs et de base 1 renvoient `null` ainsi qu'un avertissement.", + "kbn-esql-validation-autocomplete.esql.definitions.log10": "Renvoie le logarithme d'une valeur en base 10. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. Les logs de 0 et de nombres négatifs renvoient `null` ainsi qu'un avertissement.", "kbn-esql-validation-autocomplete.esql.definitions.ltrim": "Retire les espaces au début des chaînes.", + "kbn-esql-validation-autocomplete.esql.definitions.max": "La valeur maximale d'un champ.", + "kbn-esql-validation-autocomplete.esql.definitions.median": "La valeur qui est supérieure à la moitié de toutes les valeurs et inférieure à la moitié de toutes les valeurs, également connue sous le nom de `PERCENTILE` 50.", + "kbn-esql-validation-autocomplete.esql.definitions.median_absolute_deviation": "Renvoie l'écart absolu médian, une mesure de la variabilité. Il s'agit d'un indicateur robuste, ce qui signifie qu'il est utile pour décrire des données qui peuvent présenter des valeurs aberrantes ou ne pas être normalement distribuées. Pour de telles données, il peut être plus descriptif que l'écart-type. Il est calculé comme la médiane de chaque écart de point de données par rapport à la médiane de l'ensemble de l'échantillon. Autrement dit, pour une variable aléatoire `X`, l'écart absolu médian est `median(|median(X) - X|)`.", "kbn-esql-validation-autocomplete.esql.definitions.metadataDoc": "Métadonnées", "kbn-esql-validation-autocomplete.esql.definitions.metricsDoc": "Une commande source spécifique aux indicateurs, utilisez-la pour charger des données à partir des index de TSDB. Similaire à la commande STATS : calcule les statistiques agrégées, telles que la moyenne, le décompte et la somme, sur l'ensemble des résultats de recherche entrants. Si elle est utilisée sans clause BY, une seule ligne est renvoyée, qui est l'agrégation de tout l'ensemble des résultats de recherche entrants. Lorsque vous utilisez une clause BY, une ligne est renvoyée pour chaque valeur distincte dans le champ spécifié dans la clause BY. La commande renvoie uniquement les champs dans l'agrégation, et vous pouvez utiliser un large éventail de fonctions statistiques avec la commande stats. Lorsque vous effectuez plusieurs agrégations, séparez chacune d'entre elle par une virgule.", + "kbn-esql-validation-autocomplete.esql.definitions.min": "La valeur minimale d'un champ.", "kbn-esql-validation-autocomplete.esql.definitions.mv_append": "Concatène les valeurs de deux champs à valeurs multiples.", "kbn-esql-validation-autocomplete.esql.definitions.mv_avg": "Convertit un champ multivalué en un champ à valeur unique comprenant la moyenne de toutes les valeurs.", "kbn-esql-validation-autocomplete.esql.definitions.mv_concat": "Convertit une expression de type chaîne multivalué en une colonne à valeur unique comprenant la concaténation de toutes les valeurs, séparées par un délimiteur.", "kbn-esql-validation-autocomplete.esql.definitions.mv_count": "Convertit une expression multivaluée en une colonne à valeur unique comprenant le total du nombre de valeurs.", "kbn-esql-validation-autocomplete.esql.definitions.mv_dedupe": "Supprime les valeurs en doublon d'un champ multivalué.", - "kbn-esql-validation-autocomplete.esql.definitions.mv_first": "Convertit une expression multivaluée en une colonne à valeur unique comprenant la\npremière valeur. Ceci est particulièrement utile pour lire une fonction qui émet\ndes colonnes multivaluées dans un ordre connu, comme `SPLIT`.\n\nL'ordre dans lequel les champs multivalués sont lus à partir\ndu stockage sous-jacent n'est pas garanti. Il est *souvent* ascendant, mais ne vous y\nfiez pas. Si vous avez besoin de la valeur minimale, utilisez `MV_MIN` au lieu de\n`MV_FIRST`. `MV_MIN` comporte des optimisations pour les valeurs triées, il n'y a donc aucun\navantage en matière de performances pour `MV_FIRST`.", - "kbn-esql-validation-autocomplete.esql.definitions.mv_last": "Convertit une expression multivaluée en une colonne à valeur unique comprenant la dernière\nvaleur. Ceci est particulièrement utile pour lire une fonction qui émet des champs multivalués\ndans un ordre connu, comme `SPLIT`.\n\nL'ordre dans lequel les champs multivalués sont lus à partir\ndu stockage sous-jacent n'est pas garanti. Il est *souvent* ascendant, mais ne vous y\nfiez pas. Si vous avez besoin de la valeur maximale, utilisez `MV_MAX` au lieu de\n`MV_LAST`. `MV_MAX` comporte des optimisations pour les valeurs triées, il n'y a donc aucun\navantage en matière de performances pour `MV_LAST`.", + "kbn-esql-validation-autocomplete.esql.definitions.mv_first": "Convertit une expression multivaluée en une colonne à valeur unique comprenant la première valeur. Ceci est particulièrement utile pour lire une fonction qui émet des colonnes multivaluées dans un ordre connu, comme `SPLIT`.", + "kbn-esql-validation-autocomplete.esql.definitions.mv_last": "Convertit une expression multivaluée en une colonne à valeur unique comprenant la dernière valeur. Ceci est particulièrement utile pour lire une fonction qui émet des colonnes multivaluées dans un ordre connu, comme `SPLIT`.", "kbn-esql-validation-autocomplete.esql.definitions.mv_max": "Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur maximale.", "kbn-esql-validation-autocomplete.esql.definitions.mv_median": "Convertit un champ multivalué en un champ à valeur unique comprenant la valeur médiane.", + "kbn-esql-validation-autocomplete.esql.definitions.mv_median_absolute_deviation": "Convertit un champ multivalué en un champ à valeur unique comprenant l'écart absolu médian. Il est calculé comme la médiane de chaque écart de point de données par rapport à la médiane de l'ensemble de l'échantillon. Autrement dit, pour une variable aléatoire `X`, l'écart absolu médian est `median(|median(X) - X|)`.", "kbn-esql-validation-autocomplete.esql.definitions.mv_min": "Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur minimale.", - "kbn-esql-validation-autocomplete.esql.definitions.mv_slice": "Renvoie un sous-ensemble du champ multivalué en utilisant les valeurs d'index de début et de fin.", + "kbn-esql-validation-autocomplete.esql.definitions.mv_percentile": "Convertit un champ multivalué en un champ à valeur unique comprenant la valeur à laquelle un certain pourcentage des valeurs observées se produit.", + "kbn-esql-validation-autocomplete.esql.definitions.mv_pseries_weighted_sum": "Convertit une expression multivaluée en une colonne à valeur unique en multipliant chaque élément de la liste d'entrée par le terme correspondant dans P-Series et en calculant la somme.", + "kbn-esql-validation-autocomplete.esql.definitions.mv_slice": "Renvoie un sous-ensemble du champ multivalué en utilisant les valeurs d'index de début et de fin. Ceci est particulièrement utile pour lire une fonction qui émet des colonnes multivaluées dans un ordre connu, comme `SPLIT` ou `MV_SORT`.", "kbn-esql-validation-autocomplete.esql.definitions.mv_sort": "Trie une expression multivaluée par ordre lexicographique.", "kbn-esql-validation-autocomplete.esql.definitions.mv_sum": "Convertit un champ multivalué en un champ à valeur unique comprenant la somme de toutes les valeurs.", "kbn-esql-validation-autocomplete.esql.definitions.mv_zip": "Combine les valeurs de deux champs multivalués avec un délimiteur qui les relie.", @@ -5280,53 +5816,63 @@ "kbn-esql-validation-autocomplete.esql.definitions.onDoc": "Activé", "kbn-esql-validation-autocomplete.esql.definitions.pi": "Renvoie Pi, le rapport entre la circonférence et le diamètre d'un cercle.", "kbn-esql-validation-autocomplete.esql.definitions.pow": "Renvoie la valeur d’une `base` élevée à la puissance d’un `exposant`.", + "kbn-esql-validation-autocomplete.esql.definitions.qstr": "Exécute une requête de chaîne de requête. Renvoie true si la chaîne de requête fournie correspond à la ligne.", "kbn-esql-validation-autocomplete.esql.definitions.renameDoc": "Attribue un nouveau nom à une ancienne colonne", "kbn-esql-validation-autocomplete.esql.definitions.repeat": "Renvoie une chaîne construite par la concaténation de la `chaîne` avec elle-même, le `nombre` de fois spécifié.", - "kbn-esql-validation-autocomplete.esql.definitions.replace": "La fonction remplace dans la chaîne `str` toutes les correspondances avec l'expression régulière `regex`\npar la chaîne de remplacement `newStr`.", - "kbn-esql-validation-autocomplete.esql.definitions.right": "Renvoie la sous-chaîne qui extrait la 'longueur' des caractères de 'str' en partant de la droite.", - "kbn-esql-validation-autocomplete.esql.definitions.round": "Arrondit un nombre au nombre spécifié de décimales.\nLa valeur par défaut est 0, qui renvoie l'entier le plus proche. Si le\nnombre de décimales spécifié est négatif, la fonction arrondit au nombre de décimales à gauche\nde la virgule.", + "kbn-esql-validation-autocomplete.esql.definitions.replace": "La fonction remplace dans la chaîne `str` toutes les correspondances avec l'expression régulière `regex` par la chaîne de remplacement `newStr`.", + "kbn-esql-validation-autocomplete.esql.definitions.reverse": "Renvoie une nouvelle chaîne représentant la chaîne d'entrée dans l'ordre inverse.", + "kbn-esql-validation-autocomplete.esql.definitions.right": "Renvoie la sous-chaîne qui extrait la longueur des caractères de `str` en partant de la droite.", + "kbn-esql-validation-autocomplete.esql.definitions.round": "Arrondit un nombre au nombre spécifié de décimales. La valeur par défaut est 0, qui renvoie l'entier le plus proche. Si le nombre de décimales spécifié est négatif, la fonction arrondit au nombre de décimales à gauche.", "kbn-esql-validation-autocomplete.esql.definitions.rowDoc": "Renvoie une ligne contenant une ou plusieurs colonnes avec les valeurs que vous spécifiez. Cette commande peut s'avérer utile pour les tests.", "kbn-esql-validation-autocomplete.esql.definitions.rtrim": "Supprime les espaces à la fin des chaînes.", "kbn-esql-validation-autocomplete.esql.definitions.showDoc": "Renvoie des informations sur le déploiement et ses capacités", - "kbn-esql-validation-autocomplete.esql.definitions.signum": "Renvoie le signe du nombre donné.\nRenvoie `-1` pour les nombres négatifs, `0` pour `0` et `1` pour les nombres positifs.", - "kbn-esql-validation-autocomplete.esql.definitions.sin": "Renvoie la fonction trigonométrique sinusoïdale d'un angle.", - "kbn-esql-validation-autocomplete.esql.definitions.sinh": "Renvoie le sinus hyperbolique d'un angle.", + "kbn-esql-validation-autocomplete.esql.definitions.signum": "Renvoie le signe du nombre donné. Renvoie `-1` pour les nombres négatifs, `0` pour `0` et `1` pour les nombres positifs.", + "kbn-esql-validation-autocomplete.esql.definitions.sin": "Renvoie le sinus d'un angle.", + "kbn-esql-validation-autocomplete.esql.definitions.sinh": "Renvoie le sinus hyperbolique d'un nombre.", "kbn-esql-validation-autocomplete.esql.definitions.sortDoc": "Trie tous les résultats en fonction des champs spécifiés. Par défaut, les valeurs null sont considérées comme supérieures à toutes les autres valeurs. Avec l'ordre de tri croissant, les valeurs null sont classées en dernier. Avec l'ordre de tri décroissant, elles sont classées en premier. Pour modifier cet ordre, utilisez NULLS FIRST ou NULLS LAST", + "kbn-esql-validation-autocomplete.esql.definitions.space": "Renvoie une chaîne composée d'espaces nombre (`number`).", "kbn-esql-validation-autocomplete.esql.definitions.split": "Divise une chaîne de valeur unique en plusieurs chaînes.", - "kbn-esql-validation-autocomplete.esql.definitions.sqrt": "Renvoie la racine carrée d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\nLes racines carrées des nombres négatifs et des infinis sont nulles.", - "kbn-esql-validation-autocomplete.esql.definitions.st_contains": "Renvoie si la première géométrie contient la deuxième géométrie.\nIl s'agit de l'inverse de la fonction `ST_WITHIN`.", - "kbn-esql-validation-autocomplete.esql.definitions.st_disjoint": "Renvoie si les deux géométries ou colonnes géométriques sont disjointes.\nIl s'agit de l'inverse de la fonction `ST_INTERSECTS`.\nEn termes mathématiques : ST_Disjoint(A, B) ⇔ A ⋂ B = ∅", - "kbn-esql-validation-autocomplete.esql.definitions.st_distance": "Calcule la distance entre deux points.\nPour les géométries cartésiennes, c’est la distance pythagoricienne dans les mêmes unités que les coordonnées d'origine.\nPour les géométries géographiques, c’est la distance circulaire le long du grand cercle en mètres.", - "kbn-esql-validation-autocomplete.esql.definitions.st_intersects": "Renvoie `true` (vrai) si deux géométries se croisent.\nElles se croisent si elles ont un point commun, y compris leurs points intérieurs\n(les points situés le long des lignes ou dans des polygones).\nIl s'agit de l'inverse de la fonction `ST_DISJOINT`.\nEn termes mathématiques : ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅", - "kbn-esql-validation-autocomplete.esql.definitions.st_within": "Renvoie si la première géométrie est à l'intérieur de la deuxième géométrie.\nIl s'agit de l'inverse de la fonction `ST_CONTAINS`.", - "kbn-esql-validation-autocomplete.esql.definitions.st_x": "Extrait la coordonnée `x` du point fourni.\nSi les points sont de type `geo_point`, cela revient à extraire la valeur de la `longitude`.", - "kbn-esql-validation-autocomplete.esql.definitions.st_y": "Extrait la coordonnée `y` du point fourni.\nSi les points sont de type `geo_point`, cela revient à extraire la valeur de la `latitude`.", + "kbn-esql-validation-autocomplete.esql.definitions.sqrt": "Renvoie la racine carrée d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. Les racines carrées des nombres négatifs et des infinis sont nulles.", + "kbn-esql-validation-autocomplete.esql.definitions.st_centroid_agg": "Calcule le centroïde spatial sur un champ avec un type de géométrie de point spatial.", + "kbn-esql-validation-autocomplete.esql.definitions.st_contains": "Renvoie si la première géométrie contient la deuxième géométrie. Il s'agit de l'inverse de la fonction `ST_WITHIN`.", + "kbn-esql-validation-autocomplete.esql.definitions.st_disjoint": "Renvoie si les deux géométries ou colonnes géométriques sont disjointes. Il s'agit de l'inverse de la fonction `ST_INTERSECTS`. En termes mathématiques : ST_Disjoint(A, B) ⇔ A ⋂ B = ∅", + "kbn-esql-validation-autocomplete.esql.definitions.st_distance": "Calcule la distance entre deux points. Pour les géométries cartésiennes, c’est la distance pythagoricienne dans les mêmes unités que les coordonnées d'origine. Pour les géométries géographiques, c’est la distance circulaire le long du grand cercle en mètres.", + "kbn-esql-validation-autocomplete.esql.definitions.st_intersects": "Renvoie `true` (vrai) si deux géométries se croisent. Elles se croisent si elles ont un point commun, y compris leurs points intérieurs (points le long de lignes ou à l'intérieur de polygones). Il s'agit de l'inverse de la fonction `ST_DISJOINT`. En termes mathématiques : ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅", + "kbn-esql-validation-autocomplete.esql.definitions.st_within": "Renvoie si la première géométrie est à l'intérieur de la deuxième géométrie. Il s'agit de l'inverse de la fonction `ST_CONTAINS`.", + "kbn-esql-validation-autocomplete.esql.definitions.st_x": "Extrait la coordonnée `x` du point fourni. Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `longitude`.", + "kbn-esql-validation-autocomplete.esql.definitions.st_y": "Extrait la coordonnée `y` du point fourni. Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `latitude`.", "kbn-esql-validation-autocomplete.esql.definitions.starts_with": "Renvoie un booléen qui indique si une chaîne de mot-clés débute par une autre chaîne.", "kbn-esql-validation-autocomplete.esql.definitions.statsDoc": "Calcule les statistiques agrégées, telles que la moyenne, le décompte et la somme, sur l'ensemble des résultats de recherche entrants. Comme pour l'agrégation SQL, si la commande stats est utilisée sans clause BY, une seule ligne est renvoyée, qui est l'agrégation de tout l'ensemble des résultats de recherche entrants. Lorsque vous utilisez une clause BY, une ligne est renvoyée pour chaque valeur distincte dans le champ spécifié dans la clause BY. La commande stats renvoie uniquement les champs dans l'agrégation, et vous pouvez utiliser un large éventail de fonctions statistiques avec la commande stats. Lorsque vous effectuez plusieurs agrégations, séparez chacune d'entre elle par une virgule.", - "kbn-esql-validation-autocomplete.esql.definitions.substring": "Renvoie la sous-chaîne d'une chaîne, délimitée en fonction d'une position de départ et d'une longueur facultative", - "kbn-esql-validation-autocomplete.esql.definitions.tan": "Renvoie la fonction trigonométrique Tangente d'un angle.", - "kbn-esql-validation-autocomplete.esql.definitions.tanh": "Renvoie la fonction hyperbolique Tangente d'un angle.", + "kbn-esql-validation-autocomplete.esql.definitions.substring": "Renvoie la sous-chaîne d'une chaîne, délimitée en fonction d'une position de départ et d'une longueur facultative.", + "kbn-esql-validation-autocomplete.esql.definitions.sum": "La somme d'une expression numérique.", + "kbn-esql-validation-autocomplete.esql.definitions.tan": "Renvoie la tangente d'un angle.", + "kbn-esql-validation-autocomplete.esql.definitions.tanh": "Renvoie la tangente hyperbolique d'un nombre.", "kbn-esql-validation-autocomplete.esql.definitions.tau": "Renvoie le rapport entre la circonférence et le rayon d'un cercle.", "kbn-esql-validation-autocomplete.esql.definitions.to_base64": "Encode une chaîne en chaîne base64.", - "kbn-esql-validation-autocomplete.esql.definitions.to_boolean": "Convertit une valeur d'entrée en une valeur booléenne.\nUne chaîne de valeur *true* sera convertie, sans tenir compte de la casse, en une valeur booléenne *true*.\nPour toute autre valeur, y compris une chaîne vide, la fonction renverra *false*.\nLa valeur numérique *0* sera convertie en *false*, toute autre valeur sera convertie en *true*.", - "kbn-esql-validation-autocomplete.esql.definitions.to_cartesianpoint": "Convertit la valeur d'une entrée en une valeur `cartesian_point`.\nUne chaîne ne sera convertie avec succès que si elle respecte le format WKT Point.", - "kbn-esql-validation-autocomplete.esql.definitions.to_cartesianshape": "Convertit une valeur d'entrée en une valeur `cartesian_shape`.\nUne chaîne ne sera convertie avec succès que si elle respecte le format WKT.", - "kbn-esql-validation-autocomplete.esql.definitions.to_datetime": "Convertit une valeur d'entrée en une valeur de date.\nUne chaîne ne sera convertie efficacement que si elle respecte le format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`.\nPour convertir des dates vers d'autres formats, utilisez `DATE_PARSE`.", + "kbn-esql-validation-autocomplete.esql.definitions.to_boolean": "Convertit une valeur d'entrée en une valeur booléenne. Une chaîne de valeur *true* sera convertie, sans tenir compte de la casse, en une valeur booléenne *true*. Pour toute autre valeur, y compris une chaîne vide, la fonction renverra *false*. La valeur numérique *0* sera convertie en *false*, toute autre valeur sera convertie en *true*.", + "kbn-esql-validation-autocomplete.esql.definitions.to_cartesianpoint": "Convertit la valeur d'une entrée en une valeur `cartesian_point`. Une chaîne ne sera convertie avec succès que si elle respecte le format WKT Point.", + "kbn-esql-validation-autocomplete.esql.definitions.to_cartesianshape": "Convertit une valeur d'entrée en une valeur `cartesian_shape`. Une chaîne ne sera convertie que si elle respecte le format WKT.", + "kbn-esql-validation-autocomplete.esql.definitions.to_date_nanos": "Convertit une entrée en une valeur de date de résolution nanoseconde (ou date_nanos).", + "kbn-esql-validation-autocomplete.esql.definitions.to_dateperiod": "Convertit une valeur d'entrée en une valeur `date_period`.", + "kbn-esql-validation-autocomplete.esql.definitions.to_datetime": "Convertit une valeur d'entrée en une valeur de date. Une chaîne ne sera convertie que si elle respecte le format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`. Pour convertir des dates vers d'autres formats, utilisez `DATE_PARSE`.", "kbn-esql-validation-autocomplete.esql.definitions.to_degrees": "Convertit un nombre en radians en degrés.", - "kbn-esql-validation-autocomplete.esql.definitions.to_double": "Convertit une valeur d'entrée en une valeur double. Si le paramètre d'entrée est de type date,\nsa valeur sera interprétée en millisecondes depuis l'heure Unix,\nconvertie en double. Le booléen *true* sera converti en double *1.0*, et *false* en *0.0*.", - "kbn-esql-validation-autocomplete.esql.definitions.to_geopoint": "Convertit une valeur d'entrée en une valeur `geo_point`.\nUne chaîne ne sera convertie avec succès que si elle respecte le format WKT Point.", - "kbn-esql-validation-autocomplete.esql.definitions.to_geoshape": "Convertit une valeur d'entrée en une valeur `geo_shape`.\nUne chaîne ne sera convertie avec succès que si elle respecte le format WKT.", - "kbn-esql-validation-autocomplete.esql.definitions.to_integer": "Convertit une valeur d'entrée en une valeur entière.\nSi le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes\ndepuis l'heure Unix, convertie en entier.\nLe booléen *true* sera converti en entier *1*, et *false* en *0*.", + "kbn-esql-validation-autocomplete.esql.definitions.to_double": "Convertit une valeur d'entrée en une valeur double. Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en double. Le booléen *true* sera converti en double *1.0*, et *false* en *0.0*.", + "kbn-esql-validation-autocomplete.esql.definitions.to_geopoint": "Convertit une valeur d'entrée en une valeur `geo_point`. Une chaîne ne sera convertie avec succès que si elle respecte le format WKT Point.", + "kbn-esql-validation-autocomplete.esql.definitions.to_geoshape": "Convertit une valeur d'entrée en une valeur `geo_shape`. Une chaîne ne sera convertie avec succès que si elle respecte le format WKT.", + "kbn-esql-validation-autocomplete.esql.definitions.to_integer": "Convertit une valeur d'entrée en une valeur entière. Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en nombre entier. Le booléen *true* sera converti en entier *1*, et *false* en *0*.", "kbn-esql-validation-autocomplete.esql.definitions.to_ip": "Convertit une chaîne d'entrée en valeur IP.", - "kbn-esql-validation-autocomplete.esql.definitions.to_long": "Convertit une valeur d'entrée en une valeur longue. Si le paramètre d'entrée est de type date,\nsa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue.\nLe booléen *true* sera converti en valeur longue *1*, et *false* en *0*.", + "kbn-esql-validation-autocomplete.esql.definitions.to_long": "Convertit une valeur d'entrée en une valeur longue. Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue. Le booléen *true* sera converti en valeur longue *1*, et *false* en *0*.", "kbn-esql-validation-autocomplete.esql.definitions.to_lower": "Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en minuscules.", "kbn-esql-validation-autocomplete.esql.definitions.to_radians": "Convertit un nombre en degrés en radians.", "kbn-esql-validation-autocomplete.esql.definitions.to_string": "Convertit une valeur d'entrée en une chaîne.", - "kbn-esql-validation-autocomplete.esql.definitions.to_unsigned_long": "Convertit une valeur d'entrée en une valeur longue non signée. Si le paramètre d'entrée est de type date,\nsa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue non signée.\nLe booléen *true* sera converti en valeur longue non signée *1*, et *false* en *0*.", + "kbn-esql-validation-autocomplete.esql.definitions.to_timeduration": "Convertit une valeur d'entrée en valeur `time_duration`.", + "kbn-esql-validation-autocomplete.esql.definitions.to_unsigned_long": "Convertit une valeur d'entrée en une valeur longue non signée. Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue non signée. Le booléen *true* sera converti en valeur longue non signée *1*, et *false* en *0*.", "kbn-esql-validation-autocomplete.esql.definitions.to_upper": "Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en majuscules.", "kbn-esql-validation-autocomplete.esql.definitions.to_version": "Convertit une chaîne d'entrée en une valeur de version.", + "kbn-esql-validation-autocomplete.esql.definitions.top": "Collecte les valeurs les plus hautes d'un champ. Inclut les valeurs répétées.", "kbn-esql-validation-autocomplete.esql.definitions.trim": "Supprime les espaces de début et de fin d'une chaîne.", - "kbn-esql-validation-autocomplete.esql.definitions.values": "Renvoie toutes les valeurs d'un groupe dans un tableau.", + "kbn-esql-validation-autocomplete.esql.definitions.values": "Renvoie toutes les valeurs d’un groupe dans un champ multivalué. L'ordre des valeurs renvoyées n'est pas garanti. Si vous avez besoin que les valeurs renvoyées soient dans l'ordre, utilisez `esql-mv_sort`.", + "kbn-esql-validation-autocomplete.esql.definitions.weighted_avg": "La moyenne pondérée d'une expression numérique.", "kbn-esql-validation-autocomplete.esql.definitions.whereDoc": "Utilise \"predicate-expressions\" pour filtrer les résultats de recherche. Une expression predicate, lorsqu'elle est évaluée, renvoie TRUE ou FALSE. La commande where renvoie uniquement les résultats qui donnent la valeur TRUE. Par exemple, pour filtrer les résultats pour une valeur de champ spécifique", "kbn-esql-validation-autocomplete.esql.definitions.withDoc": "Avec", "kbn-esql-validation-autocomplete.esql.divide.warning.divideByZero": "Impossible de diviser par zéro : {left}/{right}", @@ -5343,7 +5889,10 @@ "kbn-esql-validation-autocomplete.esql.validation.metadataBracketsDeprecation": "Les crochets \"[]\" doivent être supprimés de la déclaration FROM METADATA", "kbn-esql-validation-autocomplete.esql.validation.missingFunction": "Fonction inconnue [{name}]", "kbn-esql-validation-autocomplete.esql.validation.noAggFunction": "Au moins une fonction d'agrégation requise dans [{command}], [{expression}] trouvée", + "kbn-esql-validation-autocomplete.esql.validation.noCombinationOfAggAndNonAggValues": "Impossible de combiner les valeurs agrégées et non agrégées dans [{commandName}], [{expression}] trouvée", "kbn-esql-validation-autocomplete.esql.validation.noNestedArgumentSupport": "Les paramètres de la fonction agrégée doivent être un attribut, un littéral ou une fonction non agrégée ; trouvé [{name}] de type [{argType}]", + "kbn-esql-validation-autocomplete.esql.validation.statsNoAggFunction": "Au moins une fonction d'agrégation requise dans [{commandName}], [{expression}] trouvée", + "kbn-esql-validation-autocomplete.esql.validation.statsNoArguments": "[{commandName}] doit contenir au moins une expression d'agrégation ou de regroupement", "kbn-esql-validation-autocomplete.esql.validation.typeOverwrite": "La colonne [{field}] de type {fieldType} a été écrasée par un nouveau type : {newType}", "kbn-esql-validation-autocomplete.esql.validation.unknowAggregateFunction": "Attendait une fonction ou un groupe agrégé mais a obtenu [{value}] de type [{type}]", "kbn-esql-validation-autocomplete.esql.validation.unknownColumn": "Colonne inconnue[{name}]", @@ -5368,6 +5917,18 @@ "kbn-esql-validation-autocomplete.esql.validation.wrongArgumentType": "L'argument de [{name}] doit être [{argType}], valeur [{value}] trouvée de type [{givenType}]", "kbn-esql-validation-autocomplete.esql.validation.wrongDissectOptionArgumentType": "Valeur non valide pour DISSECT append_separator : une chaîne était attendue mais il s'agissait de [{value}]", "kbn-esql-validation-autocomplete.esql.validation.wrongMetadataArgumentType": "Le champ de métadonnées [{value}] n'est pas disponible. Les champs de métadonnées disponibles sont : [{availableFields}]", + "kbn-esql-validation-autocomplete.recommendedQueries.aggregateExample.description": "Agrégation des quantités", + "kbn-esql-validation-autocomplete.recommendedQueries.aggregateExample.label": "Agréger avec STATS", + "kbn-esql-validation-autocomplete.recommendedQueries.caseExample.description": "Conditionnel", + "kbn-esql-validation-autocomplete.recommendedQueries.caseExample.label": "Créer un élément conditionnel avec CASE", + "kbn-esql-validation-autocomplete.recommendedQueries.dateHistogram.description": "Agrégation des totaux au fil du temps", + "kbn-esql-validation-autocomplete.recommendedQueries.dateHistogram.label": "Créer un histogramme de date", + "kbn-esql-validation-autocomplete.recommendedQueries.dateIntervals.description": "Agrégation des totaux au fil du temps", + "kbn-esql-validation-autocomplete.recommendedQueries.dateIntervals.label": "Créez des intervalles de temps de 5 minutes avec EVAL", + "kbn-esql-validation-autocomplete.recommendedQueries.lastHour.description": "Un exemple plus complexe", + "kbn-esql-validation-autocomplete.recommendedQueries.lastHour.label": "Nombre total par rapport au nombre de la dernière heure", + "kbn-esql-validation-autocomplete.recommendedQueries.sortByTime.description": "Trier par heure", + "kbn-esql-validation-autocomplete.recommendedQueries.sortByTime.label": "Trier par heure", "kbnConfig.deprecations.conflictSetting.manualStepOneMessage": "Assurez-vous que \"{fullNewPath}\" contient la valeur correcte dans le fichier de configuration, l'indicateur CLI ou la variable d'environnement (dans Docker uniquement).", "kbnConfig.deprecations.conflictSetting.manualStepTwoMessage": "Supprimez \"{fullOldPath}\" de la configuration.", "kbnConfig.deprecations.conflictSettingMessage": "Le paramètre \"{fullOldPath}\" a été remplacé par \"{fullNewPath}\". Cependant, les deux clés sont présentes. Ignorer \"{fullOldPath}\"", @@ -5378,9 +5939,10 @@ "kbnConfig.deprecations.replacedSettingMessage": "Le paramètre \"{fullOldPath}\" a été remplacé par \"{fullNewPath}\".", "kbnConfig.deprecations.unusedSetting.manualStepOneMessage": "Retirez \"{fullPath}\" dans le fichier de configuration Kibana, l'indicateur CLI ou la variable d'environnement (dans Docker uniquement).", "kbnConfig.deprecations.unusedSettingMessage": "Vous n’avez plus besoin de configurer \"{fullPath}\".", + "kbnGridLayout.row.toggleCollapse": "Basculer vers la réduction", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "L'objet enregistré est manquant.", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "Impossible de restaurer complètement l'URL. Assurez-vous d'utiliser la fonctionnalité de partage.", - "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana n'est pas en mesure de stocker des éléments d'historique dans votre session, car le stockage est arrivé à saturation et il ne semble pas y avoir d'éléments pouvant être supprimés sans risque.\n\nCe problème peut généralement être corrigé en passant à un nouvel onglet, mais il peut être causé par un problème plus important. Si ce message s'affiche régulièrement, veuillez nous en faire part sur {gitHubIssuesUrl}.", + "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana n'est pas en mesure de stocker des éléments d'historique dans votre session, car le stockage est arrivé à saturation et il ne semble pas y avoir d'éléments pouvant être supprimés sans risque. Ce problème peut généralement être corrigé en passant à un nouvel onglet, mais il peut être causé par un problème plus important. Si ce message s'affiche régulièrement, veuillez nous en faire part sur {gitHubIssuesUrl}.", "kibana_utils.stateManagement.url.restoreUrlErrorTitle": "Erreur lors de la restauration de l'état depuis l'URL.", "kibana_utils.stateManagement.url.saveStateInUrlErrorTitle": "Erreur lors de l'enregistrement de l'état dans l'URL.", "kibana-react.dualRangeControl.maxInputAriaLabel": "Maximum de la plage", @@ -5419,263 +5981,315 @@ "kibanaOverview.more.title": "Toujours plus avec Elastic", "kibanaOverview.news.title": "Nouveautés", "languageDocumentation.documentationESQL.abs": "ABS", - "languageDocumentation.documentationESQL.abs.markdown": "\n\n ### ABS\n Renvoie la valeur absolue.\n\n ````\n Numéro ROW = -1.0 \n | EVAL abs_number = ABS(number)\n ````\n ", + "languageDocumentation.documentationESQL.abs.markdown": " ### ABS Renvoie la valeur absolue. ``` ROW number = -1.0 | EVAL abs_number = ABS(number) ```", "languageDocumentation.documentationESQL.acos": "ACOS", - "languageDocumentation.documentationESQL.acos.markdown": "\n\n ### ACOS\n Renvoie l'arc cosinus de `n` sous forme d'angle, exprimé en radians.\n\n ````\n ROW a=.9\n | EVAL acos=ACOS(a)\n ````\n ", + "languageDocumentation.documentationESQL.acos.markdown": " ### ACOS Renvoie l'arc cosinus de `n` sous forme d'angle, exprimé en radians. ``` ROW a=.9 | EVAL acos=ACOS(a) ```", "languageDocumentation.documentationESQL.aggregationFunctions": "Fonctions d'agrégation", "languageDocumentation.documentationESQL.aggregationFunctionsDocumentationESQLDescription": "Ces fonctions peuvent être utilisées avec STATS...BY :", "languageDocumentation.documentationESQL.asin": "ASIN", - "languageDocumentation.documentationESQL.asin.markdown": "\n\n ### ASIN\n Renvoie l'arc sinus de l'entrée\n expression numérique sous forme d'angle, exprimée en radians.\n\n ````\n ROW a=.9\n | EVAL asin=ASIN(a)\n ````\n ", + "languageDocumentation.documentationESQL.asin.markdown": " ### ASIN Renvoie l'arc sinus de l'expression numérique sous forme d'angle, exprimé en radians. ``` ROW a=.9 | EVAL asin=ASIN(a) ```", "languageDocumentation.documentationESQL.atan": "ATAN", - "languageDocumentation.documentationESQL.atan.markdown": "\n\n ### ATAN\n Renvoie l'arc tangente de l'entrée\n expression numérique sous forme d'angle, exprimée en radians.\n\n ````\n ROW a=.12.9\n | EVAL atan=ATAN(a)\n ````\n ", + "languageDocumentation.documentationESQL.atan.markdown": " ### ATAN Renvoie l'arc tangente de l'expression numérique sous forme d'angle, exprimé en radians. ``` ROW a=12.9 | EVAL atan=ATAN(a) ```", "languageDocumentation.documentationESQL.atan2": "ATAN2", - "languageDocumentation.documentationESQL.atan2.markdown": "\n\n ### ATAN2\n L'angle entre l'axe positif des x et le rayon allant de\n l'origine au point (x , y) dans le plan cartésien, exprimée en radians.\n\n ````\n ROW y=12.9, x=.6\n | EVAL atan2=ATAN2(y, x)\n ````\n ", + "languageDocumentation.documentationESQL.atan2.markdown": " ### ATAN2 L'angle entre l'axe positif des x et le rayon allant de l'origine au point (x , y) dans le plan cartésien, exprimé en radians. ``` ROW y=12.9, x=.6 | EVAL atan2=ATAN2(y, x) ```", "languageDocumentation.documentationESQL.autoBucketFunction": "COMPARTIMENT", - "languageDocumentation.documentationESQL.autoBucketFunction.markdown": "### COMPARTIMENT\nCréer des groupes de valeurs, des compartiments (\"buckets\"), à partir d'une entrée d'un numéro ou d'un horodatage. La taille des compartiments peut être fournie directement ou choisie selon une plage de valeurs et de décompte recommandée.\n\n`BUCKET` a deux modes de fonctionnement : \n\n1. Dans lequel la taille du compartiment est calculée selon la recommandation de décompte d'un compartiment (quatre paramètres) et une plage.\n2. Dans lequel la taille du compartiment est fournie directement (deux paramètres).\n\nAvec un nombre cible de compartiments, le début d'une plage et la fin d'une plage, `BUCKET` choisit une taille de compartiment appropriée afin de générer le nombre cible de compartiments ou moins.\n\nPar exemple, demander jusqu'à 20 compartiments pour une année organisera les données en intervalles mensuels :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT hire_date\n````\n\n**REMARQUE** : Le but n'est pas de fournir le nombre précis de compartiments, mais plutôt de sélectionner une plage qui fournit, tout au plus, le nombre cible de compartiments.\n\nVous pouvez combiner `BUCKET` avec une agrégation pour créer un histogramme :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_month = COUNT(*) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT month\n````\n\n**REMARQUE** : `BUCKET` ne crée pas de compartiments qui ne correspondent à aucun document. C'est pourquoi, dans l'exemple précédent, il manque 1985-03-01 ainsi que d'autres dates.\n\nDemander d'autres compartiments peut résulter en une plage réduite. Par exemple, demander jusqu'à 100 compartiments en un an résulte en des compartiments hebdomadaires :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 100, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT week\n````\n\n**REMARQUE** : `AUTO_BUCKET` ne filtre aucune ligne. Il n'utilise que la plage fournie pour choisir une taille de compartiment appropriée. Pour les lignes dont la valeur se situe en dehors de la plage, il renvoie une valeur de compartiment qui correspond à un compartiment situé en dehors de la plage. Associez `BUCKET` à `WHERE` pour filtrer les lignes.\n\nSi la taille de compartiment désirée est connue à l'avance, fournissez-la comme second argument, en ignorant la plage :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 1 week)\n| SORT week\n````\n\n**REMARQUE** : Lorsque vous fournissez la taille du compartiment comme second argument, ce dernier doit être une période temporelle ou une durée.\n\n`BUCKET` peut également être utilisé pour des champs numériques. Par exemple, pour créer un histogramme de salaire :\n\n````\nFROM employees\n| STATS COUNT(*) by bs = BUCKET(salary, 20, 25324, 74999)\n| SORT bs\n````\n\nContrairement à l'exemple précédent qui filtre intentionnellement sur une plage temporelle, vous n'avez pas souvent besoin de filtrer sur une plage numérique. Vous devez trouver les valeurs min et max séparément. ES|QL n'a pas encore de façon aisée d'effectuer cette opération automatiquement.\n\nLa plage peut être ignorée si la taille désirée de compartiment est connue à l'avance. Fournissez-la simplement comme second argument :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.)\n| SORT b\n````\n\n**REMARQUE** : Lorsque vous fournissez la taille du compartiment comme second argument, elle doit être de type à **virgule flottante**.\n\nVoici un exemple sur comment créer des compartiments horaires pour les dernières 24 heures, et calculer le nombre d'événements par heure :\n\n````\nFROM sample_data\n| WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW()\n| STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW())\n````\n\nVoici un exemple permettant de créer des compartiments mensuels pour l'année 1985, et calculer le salaire moyen par mois d'embauche :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT bucket\n````\n\n`BUCKET` peut être utilisé pour les parties de groupage et d'agrégation de la commande `STATS …​ BY ...`, tant que la partie d'agrégation de la fonction est **référencée par un alias défini dans la partie de groupage**, ou que celle-ci est invoquée avec exactement la même expression.\n\nPar exemple :\n\n````\nFROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2\n````\n ", + "languageDocumentation.documentationESQL.autoBucketFunction.markdown": "### BUCKET Crée des groupes de valeurs et des compartiments (\"buckets\"), à partir d'une entrée numérique ou d'un horodatage. La taille des compartiments peut être fournie directement ou choisie selon une plage de valeurs et de décompte recommandée. `BUCKET` a deux modes de fonctionnement : 1. Dans lequel la taille du compartiment est calculée selon la recommandation de décompte d'un compartiment (quatre paramètres) et une plage. 2. Dans lequel la taille du compartiment est fournie directement (deux paramètres). Avec un nombre cible de compartiments, le début d'une plage et la fin d'une plage, `BUCKET` choisit une taille de compartiment appropriée afin de générer le nombre cible de compartiments ou moins. Par exemple, demander jusqu'à 20 compartiments pour une année organisera les données en intervalles mensuels : ``` FROM employees | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\" | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\") | SORT hire_date ``` **REMARQUE** : Le but n'est pas de fournir le nombre précis de compartiments, mais plutôt de sélectionner une plage qui fournit, tout au plus, le nombre cible de compartiments. Vous pouvez combiner `BUCKET` avec une agrégation pour créer un histogramme : ``` FROM employees | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\" | STATS hires_per_month = COUNT(*) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\") | SORT month ``` **REMARQUE** : `BUCKET` ne crée pas de compartiments qui ne correspondent à aucun document. C'est pourquoi, dans l'exemple précédent, il manque 1985-03-01 ainsi que d'autres dates. Demander d'autres compartiments peut résulter en une plage réduite. Par exemple, demander jusqu'à 100 compartiments en un an résulte en des compartiments hebdomadaires : ``` FROM employees | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\" | STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 100, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\") | SORT week ``` **REMARQUE** : `BUCKET` ne filtre aucune ligne. Il n'utilise que la plage fournie pour choisir une taille de compartiment appropriée. Pour les lignes dont la valeur se situe en dehors de la plage, il renvoie une valeur de compartiment qui correspond à un compartiment situé en dehors de la plage. Associez `BUCKET` à `WHERE` pour filtrer les lignes. Si la taille de compartiment désirée est connue à l'avance, fournissez-la comme second argument, en ignorant la plage : ``` FROM employees | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\" | STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 1 week) | SORT week ``` **REMARQUE** : Lorsque vous fournissez la taille du compartiment comme second argument, ce dernier doit être une période temporelle ou une durée. `BUCKET` peut également être utilisé pour des champs numériques. Par exemple, pour créer un histogramme de salaire : ``` FROM employees | STATS COUNT(*) by bs = BUCKET(salary, 20, 25324, 74999) | SORT bs ``` Contrairement à l'exemple précédent qui filtre intentionnellement sur une plage temporelle, vous n'avez pas souvent besoin de filtrer sur une plage numérique. Vous devez trouver les valeurs min et max séparément. ES|QL n'a pas encore de façon aisée d'effectuer cette opération automatiquement. La plage peut être ignorée si la taille désirée de compartiment est connue à l'avance. Fournissez-la simplement comme second argument : ``` FROM employees | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\" | STATS c = COUNT(1) BY b = BUCKET(salary, 5000.) | SORT b ``` **REMARQUE** : Lorsque vous fournissez la taille du compartiment comme second argument, elle doit être de type à **virgule flottante**. Voici un exemple sur comment créer des compartiments horaires pour les dernières 24 heures, et calculer le nombre d'événements par heure : ``` FROM sample_data | WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW() | STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW()) ``` Voici un exemple sur comment créer des compartiments mensuels pour l'année 1985, et calculer le salaire moyen par mois d'embauche : ``` FROM employees | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\" | STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\") | SORT bucket ``` `BUCKET` peut être utilisé à la fois dans la partie agrégation et dans la partie regroupement de la commande `STATS ... BY ...`, tant que la partie d'agrégation de la fonction est **référencée par un alias défini dans la partie de groupage**, ou que celle-ci est invoquée avec exactement la même expression. Par exemple : ``` FROM employees | STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.) | SORT b1, b2 | KEEP s1, b1, s2, b2 ```", + "languageDocumentation.documentationESQL.avg": "AVG", + "languageDocumentation.documentationESQL.avg.markdown": " ### AVG La moyenne d'un champ numérique. ``` FROM employees | STATS AVG(height) ```", "languageDocumentation.documentationESQL.binaryOperators": "Opérateurs binaires", - "languageDocumentation.documentationESQL.binaryOperators.markdown": "### Opérateurs binaires\nLes opérateurs de comparaison binaire suivants sont pris en charge :\n\n* égalité : `==`\n* inégalité : `!=`\n* inférieur à : `<`\n* inférieur ou égal à : `<=`\n* supérieur à : `>`\n* supérieur ou égal à : `>=`\n* ajouter : `+`\n* soustraire : `-`\n* multiplier par : `*`\n* diviser par : `/`\n* module : `%`\n ", + "languageDocumentation.documentationESQL.binaryOperators.markdown": "### Opérateurs binaires Les opérateurs de comparaison binaires suivants sont pris en charge : * égalité : `==` * inégalité : `!=` * inférieur à : `<` * inférieur ou égal à : `<=` * supérieur à : `>` * supérieur ou égal à : `>=` * ajouter : `+` * soustraire : `-` * multiplier : `*` * diviser par : `/` * modulo : `%`", "languageDocumentation.documentationESQL.booleanOperators": "Opérateurs booléens", - "languageDocumentation.documentationESQL.booleanOperators.markdown": "### Opérateurs booléens\nLes opérateurs booléens suivants sont pris en charge :\n\n* `AND`\n* `OR`\n* `NOT`\n ", + "languageDocumentation.documentationESQL.booleanOperators.markdown": "### Opérateurs booléens Les opérateurs booléens suivants sont pris en charge : * `AND` * `OR` * `NOT`", "languageDocumentation.documentationESQL.bucket": "COMPARTIMENT", - "languageDocumentation.documentationESQL.bucket.markdown": "\n\n ### COMPARTIMENT\n Créer des groupes de valeurs, des compartiments (\"buckets\"), à partir d'une entrée d'un numéro ou d'un horodatage.\n La taille des compartiments peut être fournie directement ou choisie selon une plage de valeurs et de décompte recommandée.\n\n ````\n FROM employees\n | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n | SORT hire_date\n ````\n ", + "languageDocumentation.documentationESQL.bucket.markdown": " ### BUCKET Crée des groupes de valeurs et des compartiments (\"buckets\"), à partir d'une entrée numérique ou d'un horodatage. La taille des compartiments peut être fournie directement ou choisie selon une plage de valeurs et de décompte recommandée. ``` FROM employees | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\" | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\") | SORT hire_date ```", "languageDocumentation.documentationESQL.case": "CASE", - "languageDocumentation.documentationESQL.case.markdown": "\n\n ### CAS\n Accepte les paires de conditions et de valeurs. La fonction renvoie la valeur qui\n appartient à la première condition étant évaluée comme `true`.\n\n Si le nombre d'arguments est impair, le dernier argument est la valeur par défaut qui est\n renvoyée si aucune condition ne correspond. Si le nombre d'arguments est pair, et\n qu'aucune condition ne correspond, la fonction renvoie `null`.\n\n ````\n FROM employees\n | EVAL type = CASE(\n languages <= 1, \"monolingual\",\n languages <= 2, \"bilingual\",\n \"polyglot\")\n | KEEP emp_no, languages, type\n ````\n ", + "languageDocumentation.documentationESQL.case.markdown": " ### CASE Accepte les paires de conditions et de valeurs. La fonction renvoie la valeur correspondant à la première condition évaluée à `true` (vraie). Si le nombre d'arguments est impair, le dernier argument est la valeur par défaut qui est renvoyée si aucune condition ne correspond. Si le nombre d'arguments est pair, et qu'aucune condition ne correspond, la fonction renvoie `null`. ``` FROM employees | EVAL type = CASE( languages <= 1, \"monolingual\", languages <= 2, \"bilingual\", \"polyglot\") | KEEP emp_no, languages, type ```", "languageDocumentation.documentationESQL.castOperator": "Cast (::)", - "languageDocumentation.documentationESQL.castOperator.markdown": "### CAST (`::`)\nL'opérateur `::` fournit une syntaxe alternative pratique au type de converstion de fonction `TO_`.\n\nExemple :\n````\nROW ver = CONCAT((\"0\"::INT + 1)::STRING, \".2.3\")::VERSION\n````\n ", + "languageDocumentation.documentationESQL.castOperator.markdown": "### CAST (`::`) L'opérateur `::` fournit une autre syntaxe pratique pour les fonctions de conversion de type `TO_`. Exemple : ``` ROW ver = CONCAT((\"0\"::INT + 1)::STRING, \".2.3\")::VERSION ```", "languageDocumentation.documentationESQL.cbrt": "CBRT", - "languageDocumentation.documentationESQL.cbrt.markdown": "\n\n ### CBRT\n Renvoie la racine cubique d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n La racine cubique de l’infini est nulle.\n\n ````\n ROW d = 1000.0\n | EVAL c = cbrt(d)\n ````\n ", + "languageDocumentation.documentationESQL.cbrt.markdown": " ### CBRT Renvoie la racine cubique d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. La racine cubique de l'infini est nulle. ``` ROW d = 1000.0 | EVAL c = cbrt(d) ```", "languageDocumentation.documentationESQL.ceil": "CEIL", - "languageDocumentation.documentationESQL.ceil.markdown": "\n\n ### CEIL\n Arrondir un nombre à l'entier supérieur.\n\n ```\n ROW a=1.8\n | EVAL a=CEIL(a)\n ```\n Remarque : Il s'agit d'un noop pour `long` (y compris non signé) et `integer`. Pour `double`, la fonction choisit la valeur `double` la plus proche de l'entier, de manière similaire à la méthode Math.ceil.\n ", + "languageDocumentation.documentationESQL.ceil.markdown": " ### CEIL Arrondit un nombre à l'entier supérieur. ``` ROW a=1.8 | EVAL a=CEIL(a) ``` Remarque : Il s'agit d'un noop pour `long` (y compris non signé) et `integer`. Pour `double`, la fonction choisit la valeur `double` la plus proche de l'entier, de manière similaire à la méthode Math.ceil.", "languageDocumentation.documentationESQL.cidr_match": "CIDR_MATCH", - "languageDocumentation.documentationESQL.cidr_match.markdown": "\n\n ### CIDR_MATCH\n Renvoie `true` si l'IP fournie est contenue dans l'un des blocs CIDR fournis.\n\n ````\n FROM hosts \n | WHERE CIDR_MATCH(ip1, \"127.0.0.2/32\", \"127.0.0.3/32\") \n | KEEP card, host, ip0, ip1\n ````\n ", + "languageDocumentation.documentationESQL.cidr_match.markdown": " ### CIDR_MATCH Renvoie `true` si l'IP fournie est contenue dans l'un des blocs CIDR fournis. ``` FROM hosts | WHERE CIDR_MATCH(ip1, \"127.0.0.2/32\", \"127.0.0.3/32\") | KEEP card, host, ip0, ip1 ```", "languageDocumentation.documentationESQL.coalesce": "COALESCE", - "languageDocumentation.documentationESQL.coalesce.markdown": "\n\n ### COALESCE\n Renvoie le premier de ses arguments qui n'est pas nul. Si tous les arguments sont nuls, `null` est renvoyé.\n\n ````\n ROW a=null, b=\"b\"\n | EVAL COALESCE(a, b)\n ````\n ", + "languageDocumentation.documentationESQL.coalesce.markdown": " ### COALESCE Renvoie le premier de ses arguments qui n'est pas nul. Si tous les arguments sont nuls, `null` est renvoyé. ``` ROW a=null, b=\"b\" | EVAL COALESCE(a, b) ```", "languageDocumentation.documentationESQL.commandsDescription": "Une commande source produit un tableau, habituellement avec des données issues d'Elasticsearch. ES|QL est compatible avec les commandes sources suivantes.", "languageDocumentation.documentationESQL.concat": "CONCAT", - "languageDocumentation.documentationESQL.concat.markdown": "\n\n ### CONCAT\n Concatène deux ou plusieurs chaînes.\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fullname = CONCAT(first_name, \" \", last_name)\n ````\n ", + "languageDocumentation.documentationESQL.concat.markdown": " ### CONCAT Concatène deux ou plusieurs chaînes. ``` FROM employees | KEEP first_name, last_name | EVAL fullname = CONCAT(first_name, \" \", last_name) ```", "languageDocumentation.documentationESQL.cos": "COS", - "languageDocumentation.documentationESQL.cos.markdown": "\n\n ### COS\n Renvoie le cosinus d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL cos=COS(a)\n ````\n ", + "languageDocumentation.documentationESQL.cos.markdown": " ### COS Renvoie le cosinus d'un angle. ``` ROW a=1.8 | EVAL cos=COS(a) ```", "languageDocumentation.documentationESQL.cosh": "COSH", - "languageDocumentation.documentationESQL.cosh.markdown": "\n\n ### COSH\n Renvoie le cosinus hyperbolique d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL cosh=COSH(a)\n ```\n ", + "languageDocumentation.documentationESQL.cosh.markdown": " ### COSH Renvoie le cosinus hyperbolique d'un nombre. ``` ROW a=1.8 | EVAL cosh=COSH(a) ```", + "languageDocumentation.documentationESQL.count": "COUNT", + "languageDocumentation.documentationESQL.count_distinct": "COUNT_DISTINCT", + "languageDocumentation.documentationESQL.count_distinct.markdown": " ### COUNT_DISTINCT Renvoie le nombre approximatif de valeurs distinctes. ``` FROM hosts | STATS COUNT_DISTINCT(ip0), COUNT_DISTINCT(ip1) ```", + "languageDocumentation.documentationESQL.count.markdown": " ### COUNT Renvoie le nombre total de valeurs en entrée. ``` FROM employees | STATS COUNT(height) ```", "languageDocumentation.documentationESQL.date_diff": "DATE_DIFF", - "languageDocumentation.documentationESQL.date_diff.markdown": "\n\n ### DATE_DIFF\n Soustrait le `startTimestamp` du `endTimestamp` et renvoie la différence en multiples `d'unité`.\n Si `startTimestamp` est postérieur à `endTimestamp`, des valeurs négatives sont renvoyées.\n\n ````\n ROW date1 = TO_DATETIME(\"2023-12-02T11:00:00.000Z\"), date2 = TO_DATETIME(\"2023-12-02T11:00:00.001Z\")\n | EVAL dd_ms = DATE_DIFF(\"microseconds\", date1, date2)\n ````\n ", + "languageDocumentation.documentationESQL.date_diff.markdown": " ### DATE_DIFF Soustrait le `startTimestamp` du `endTimestamp` et renvoie la différence en multiples d'unité (`unit`). Si `startTimestamp` est postérieur à `endTimestamp`, des valeurs négatives sont renvoyées. ``` ROW date1 = TO_DATETIME(\"2023-12-02T11:00:00.000Z\"), date2 = TO_DATETIME(\"2023-12-02T11:00:00.001Z\") | EVAL dd_ms = DATE_DIFF(\"microseconds\", date1, date2) ```", "languageDocumentation.documentationESQL.date_extract": "DATE_EXTRACT", - "languageDocumentation.documentationESQL.date_extract.markdown": "\n\n ### DATE_EXTRACT\n Extrait des parties d'une date, telles que l'année, le mois, le jour, l'heure.\n\n ````\n ROW date = DATE_PARSE(\"yyyy-MM-dd\", \"2022-05-06\")\n | EVAL year = DATE_EXTRACT(\"year\", date)\n ````\n ", + "languageDocumentation.documentationESQL.date_extract.markdown": " ### DATE_EXTRACT Extrait des parties d'une date, telles que l'année, le mois, le jour, l'heure. ``` ROW date = DATE_PARSE(\"yyyy-MM-dd\", \"2022-05-06\") | EVAL year = DATE_EXTRACT(\"year\", date) ```", "languageDocumentation.documentationESQL.date_format": "DATE_FORMAT", - "languageDocumentation.documentationESQL.date_format.markdown": "\n\n ### DATE_FORMAT\n Renvoie une représentation sous forme de chaîne d'une date dans le format fourni.\n\n ````\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL hired = DATE_FORMAT(\"YYYY-MM-dd\", hire_date)\n ````\n ", + "languageDocumentation.documentationESQL.date_format.markdown": " ### DATE_FORMAT Renvoie une représentation sous forme de chaîne d'une date dans le format fourni. ``` FROM employees | KEEP first_name, last_name, hire_date | EVAL hired = DATE_FORMAT(\"yyyy-MM-dd\", hire_date) ```", "languageDocumentation.documentationESQL.date_parse": "DATE_PARSE", - "languageDocumentation.documentationESQL.date_parse.markdown": "\n\n ### DATE_PARSE\n Renvoie une date en analysant le deuxième argument selon le format spécifié dans le premier argument.\n\n ````\n ROW date_string = \"2022-05-06\"\n | EVAL date = DATE_PARSE(\"yyyy-MM-dd\", date_string)\n ````\n ", + "languageDocumentation.documentationESQL.date_parse.markdown": " ### DATE_PARSE Renvoie une date en analysant le deuxième argument selon le format spécifié dans le premier argument. ``` ROW date_string = \"2022-05-06\" | EVAL date = DATE_PARSE(\"yyyy-MM-dd\", date_string) ```", "languageDocumentation.documentationESQL.date_trunc": "DATE_TRUNC", - "languageDocumentation.documentationESQL.date_trunc.markdown": "\n\n ### DATE_TRUNC\n Arrondit une date à l'intervalle le plus proche.\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL year_hired = DATE_TRUNC(1 year, hire_date)\n ````\n ", + "languageDocumentation.documentationESQL.date_trunc.markdown": " ### DATE_TRUNC Arrondit une date à l'intervalle le plus proche. ``` FROM employees | KEEP first_name, last_name, hire_date | EVAL year_hired = DATE_TRUNC(1 year, hire_date) ```", "languageDocumentation.documentationESQL.dissect": "DISSECT", - "languageDocumentation.documentationESQL.dissect.markdown": "### DISSECT\n`DISSECT` vous permet d'extraire des données structurées d'une chaîne. `DISSECT` compare la chaîne à un modèle basé sur les délimiteurs, et extrait les clés indiquées en tant que colonnes.\n\nPour obtenir la syntaxe des modèles \"dissect\", consultez [la documentation relative au processeur \"dissect\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/dissect-processor.html).\n\n```\nROW a = \"1953-01-23T12:15:00Z - some text - 127.0.0.1\"\n| DISSECT a \"%'{Y}-%{M}-%{D}T%{h}:%{m}:%{s}Z - %{msg} - %{ip}'\"\n```` ", + "languageDocumentation.documentationESQL.dissect.markdown": "### DISSECT `DISSECT` vous permet d'extraire des données structurées d'une chaîne. `DISSECT` compare la chaîne à un modèle basé sur les délimiteurs, et extrait les clés indiquées en tant que colonnes. Pour obtenir la syntaxe des modèles \"dissect\", consultez la [documentation relative au processeur \"dissect\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/dissect-processor.html). ``` ROW a = \"1953-01-23T12:15:00Z - some text - 127.0.0.1\" | DISSECT a \"%'{Y}-%{M}-%{D}T%{h}:%{m}:%{s}Z - %{msg} - %{ip}'\" ```", "languageDocumentation.documentationESQL.drop": "DROP", - "languageDocumentation.documentationESQL.drop.markdown": "### DROP\nAfin de supprimer certaines colonnes d'un tableau, utilisez `DROP` :\n \n```\nFROM employees\n| DROP height\n```\n\nPlutôt que de spécifier chaque colonne par son nom, vous pouvez utiliser des caractères génériques pour supprimer toutes les colonnes dont le nom correspond à un modèle :\n\n```\nFROM employees\n| DROP height*\n````\n ", + "languageDocumentation.documentationESQL.drop.markdown": "### DROP Afin de supprimer certaines colonnes d'un tableau, utilisez `DROP` : ``` FROM employees | DROP height ``` Plutôt que de spécifier chaque colonne par son nom, vous pouvez utiliser des caractères génériques pour supprimer toutes les colonnes dont le nom correspond à un modèle : ``` FROM employees | DROP height* ```", "languageDocumentation.documentationESQL.e": "E", - "languageDocumentation.documentationESQL.e.markdown": "\n\n ### E\n Retourne le nombre d'Euler.\n\n ````\n ROW E()\n ````\n ", + "languageDocumentation.documentationESQL.e.markdown": " ### E Renvoie le nombre d'Euler. ``` ROW E() ```", "languageDocumentation.documentationESQL.ends_with": "ENDS_WITH", - "languageDocumentation.documentationESQL.ends_with.markdown": "\n\n ### ENDS_WITH\n Renvoie une valeur booléenne qui indique si une chaîne de mots-clés se termine par une autre chaîne.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_E = ENDS_WITH(last_name, \"d\")\n ````\n ", + "languageDocumentation.documentationESQL.ends_with.markdown": " ### ENDS_WITH Renvoie une valeur booléenne qui indique si une chaîne de mots-clés se termine par une autre chaîne. ``` FROM employees | KEEP last_name | EVAL ln_E = ENDS_WITH(last_name, \"d\") ```", "languageDocumentation.documentationESQL.enrich": "ENRICH", - "languageDocumentation.documentationESQL.enrich.markdown": "### ENRICH\nVous pouvez utiliser `ENRICH` pour ajouter les données de vos index existants aux enregistrements entrants. Une fonction similaire à l'[enrichissement par ingestion](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html), mais qui fonctionne au moment de la requête.\n\n```\nROW language_code = \"1\"\n| ENRICH languages_policy\n```\n\n`ENRICH` requiert l'exécution d'une [politique d'enrichissement](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-policy). La politique d'enrichissement définit un champ de correspondance (un champ clé) et un ensemble de champs d'enrichissement.\n\n`ENRICH` recherche les enregistrements dans l'[index d'enrichissement](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-index) en se basant sur la valeur du champ de correspondance. La clé de correspondance dans l'ensemble de données d'entrée peut être définie en utilisant `ON `. Si elle n'est pas spécifiée, la correspondance sera effectuée sur un champ portant le même nom que le champ de correspondance défini dans la politique d'enrichissement.\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a\n```\n\nVous pouvez indiquer quels attributs (parmi ceux définis comme champs d'enrichissement dans la politique) doivent être ajoutés au résultat, en utilisant la syntaxe `WITH , ...`.\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH language_name\n```\n\nLes attributs peuvent également être renommés à l'aide de la syntaxe `WITH new_name=`\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH name = language_name\n````\n\nPar défaut (si aucun `WITH` n'est défini), `ENRICH` ajoute au résultat tous les champs d'enrichissement définis dans la politique d'enrichissement.\n\nEn cas de collision de noms, les champs nouvellement créés remplacent les champs existants.\n ", + "languageDocumentation.documentationESQL.enrich.markdown": "### ENRICH Vous pouvez utiliser `ENRICH` pour ajouter les données de vos index existants aux enregistrements entrants. Cette fonction est similaire à [\"ingest enrich\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html), mais agit au moment de la requête. ``` ROW language_code = \"1\" | ENRICH languages_policy ``` `ENRICH` nécessite l'exécution d'une [politique d'enrichissement](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-policy). La politique d'enrichissement définit un champ de correspondance (un champ clé) et un ensemble de champs d'enrichissement. `ENRICH` recherchera les enregistrements dans l'[index d'enrichissement](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-index) en se basant sur la valeur du champ de correspondance. La clé de correspondance dans l'ensemble de données d'entrée peut être définie en utilisant `ON `. Si elle n'est pas spécifiée, la correspondance sera effectuée sur un champ portant le même nom que le champ de correspondance défini dans la politique d'enrichissement. ``` ROW a = \"1\" | ENRICH languages_policy ON a ``` Vous pouvez indiquer quels attributs (parmi ceux définis comme champs d'enrichissement dans la politique) doivent être ajoutés au résultat, en utilisant la syntaxe `WITH , ...`. ``` ROW a = \"1\" | ENRICH languages_policy ON a WITH language_name ``` Les attributs peuvent également être renommés à l'aide de `WITH new_name=` ``` ROW a = \"1\" | ENRICH languages_policy ON a WITH name = language_name ``` Par défaut (si aucun `WITH` n'est défini), `ENRICH` ajoute au résultat tous les champs d'enrichissement définis dans la politique d'enrichissement. En cas de collision de noms, les champs nouvellement créés remplacent les champs existants.", "languageDocumentation.documentationESQL.eval": "EVAL", - "languageDocumentation.documentationESQL.eval.markdown": "### EVAL\n`EVAL` permet d'ajouter des colonnes :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height_feet = height * 3.281, height_cm = height * 100\n````\n\nSi la colonne indiquée existe déjà, la colonne existante sera supprimée et la nouvelle colonne sera ajoutée au tableau :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height = height * 3.281\n````\n\n#### Fonctions\n`EVAL` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez les fonctions.\n ", + "languageDocumentation.documentationESQL.eval.markdown": "### EVAL `EVAL` permet d'ajouter des colonnes : ``` FROM employees | KEEP first_name, last_name, height | EVAL height_feet = height * 3.281, height_cm = height * 100 ``` Si la colonne indiquée existe déjà, la colonne existante sera supprimée et la nouvelle colonne sera ajoutée au tableau : ``` FROM employees | KEEP first_name, last_name, height | EVAL height = height * 3.281 ``` #### Fonctions `EVAL` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez les fonctions.", + "languageDocumentation.documentationESQL.exp": "EXP", + "languageDocumentation.documentationESQL.exp.markdown": " ### EXP Renvoie la valeur de \"e\" élevée à la puissance d'un nombre donné. ``` ROW d = 5.0 | EVAL s = EXP(d) ```", "languageDocumentation.documentationESQL.floor": "FLOOR", - "languageDocumentation.documentationESQL.floor.markdown": "\n\n ### FLOOR\n Arrondir un nombre à l'entier inférieur.\n\n ````\n ROW a=1.8\n | EVAL a=FLOOR(a)\n ````\n Remarque : Il s'agit d'un noop pour `long` (y compris non signé) et `integer`.\n Pour `double`, la fonction choisit la valeur `double` la plus proche de l'entier,\n de manière similaire à Math.floor.\n ", + "languageDocumentation.documentationESQL.floor.markdown": " ### FLOOR Arrondit un nombre à l'entier inférieur. ``` ROW a=1.8 | EVAL a=FLOOR(a) ``` Remarque : Il s'agit d'un noop pour `long` (y compris non signé) et `integer`. Pour `double`, la fonction choisit la valeur `double` la plus proche de l'entier, de manière similaire à la méthode Math.floor.", "languageDocumentation.documentationESQL.from": "FROM", "languageDocumentation.documentationESQL.from_base64": "FROM_BASE64", - "languageDocumentation.documentationESQL.from_base64.markdown": "\n\n ### FROM_BASE64\n Décodez une chaîne base64.\n\n ````\n row a = \"ZWxhc3RpYw==\" \n | eval d = from_base64(a)\n ````\n ", - "languageDocumentation.documentationESQL.from.markdown": "### FROM\nLa commande source `FROM` renvoie un tableau contenant jusqu'à 10 000 documents issus d'un flux de données, d'un index ou d'un alias. Chaque ligne du tableau obtenu correspond à un document. Chaque colonne correspond à un champ et est accessible par le nom de ce champ.\n\n````\nFROM employees\n````\n\nVous pouvez utiliser des [calculs impliquant des dates](https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names) pour désigner les indices, les alias et les flux de données. Cela peut s'avérer utile pour les données temporelles.\n\nUtilisez des listes séparées par des virgules ou des caractères génériques pour rechercher plusieurs flux de données, indices ou alias :\n\n````\nFROM employees-00001,employees-*\n````\n\n#### Métadonnées\n\nES|QL peut accéder aux champs de métadonnées suivants :\n\n* `_index` : l'index auquel appartient le document. Le champ est du type `keyword`.\n* `_id` : l'identifiant du document source. Le champ est du type `keyword`.\n* `_id` : la version du document source. Le champ est du type `long`.\n\nUtilisez la directive `METADATA` pour activer les champs de métadonnées :\n\n````\nFROM index [METADATA _index, _id]\n````\n\nLes champs de métadonnées ne sont disponibles que si la source des données est un index. Par conséquent, `FROM` est la seule commande source qui prend en charge la directive `METADATA`.\n\nUne fois activés, les champs sont disponibles pour les commandes de traitement suivantes, tout comme les autres champs de l'index :\n\n````\nFROM ul_logs, apps [METADATA _index, _version]\n| WHERE id IN (13, 14) AND _version == 1\n| EVAL key = CONCAT(_index, \"_\", TO_STR(id))\n| SORT id, _index\n| KEEP id, _index, _version, key\n````\n\nDe même, comme pour les champs d'index, une fois l'agrégation effectuée, un champ de métadonnées ne sera plus accessible aux commandes suivantes, sauf s'il est utilisé comme champ de regroupement :\n\n````\nFROM employees [METADATA _index, _id]\n| STATS max = MAX(emp_no) BY _index\n````\n ", + "languageDocumentation.documentationESQL.from_base64.markdown": " ### FROM_BASE64 Décode une chaîne base64. ``` row a = \"ZWxhc3RpYw==\" | eval d = from_base64(a) ```", + "languageDocumentation.documentationESQL.from.markdown": "### FROM La commande source `FROM` renvoie un tableau contenant jusqu'à 10 000 documents issus d'un flux de données, d'un index ou d'un alias. Chaque ligne du tableau obtenu correspond à un document. Chaque colonne correspond à un champ et est accessible par le nom de ce champ. ``` FROM employees ``` Vous pouvez utiliser des [calculs impliquant des dates](https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names) pour désigner les indices, les alias et les flux de données. Cela peut s'avérer utile pour les données temporelles. Utilisez des listes séparées par des virgules ou des caractères génériques pour rechercher plusieurs flux de données, indices ou alias : ``` FROM employees-00001,employees-* ``` #### Métadonnées ES|QL peut accéder aux champs de métadonnées suivants : * `_index` : l'index auquel appartient le document. Le champ est du type `keyword`. * `_id` : l'identifiant du document source. Le champ est du type `keyword`. * `_version` : la version du document source. Le champ est du type `long`. Utilisez la directive `METADATA` pour activer les champs de métadonnées : ``` FROM index [METADATA _index, _id] ``` Les champs de métadonnées ne sont disponibles que si la source des données est un index. Par conséquent, `FROM` est la seule commande source qui prend en charge la directive `METADATA`. Une fois activés, les champs sont disponibles pour les commandes de traitement suivantes, tout comme les autres champs de l'index : ``` FROM ul_logs, apps [METADATA _index, _version] | WHERE id IN (13, 14) AND _version == 1 | EVAL key = CONCAT(_index, \"_\", TO_STR(id)) | SORT id, _index | KEEP id, _index, _version, key ``` De même, comme pour les champs d'index, une fois l'agrégation effectuée, un champ de métadonnées ne sera plus accessible aux commandes suivantes, sauf s'il est utilisé comme champ de regroupement : ``` FROM employees [METADATA _index, _id] | STATS max = MAX(emp_no) BY _index ```", "languageDocumentation.documentationESQL.functions": "Fonctions", "languageDocumentation.documentationESQL.functionsDocumentationESQLDescription": "Les fonctions sont compatibles avec \"ROW\" (Ligne), \"EVAL\" (Évaluation) et \"WHERE\" (Où).", "languageDocumentation.documentationESQL.greatest": "GREATEST", - "languageDocumentation.documentationESQL.greatest.markdown": "\n\n ### GREATEST\n Renvoie la valeur maximale de plusieurs colonnes. Similaire à `MV_MAX`\n sauf que ceci est destiné à une exécution sur plusieurs colonnes à la fois.\n\n ````\n ROW a = 10, b = 20\n | EVAL g = GREATEST(a, b)\n ````\n Remarque : Lorsque cette fonction est exécutée sur les champs `keyword` ou `text`, elle renvoie la dernière chaîne dans l'ordre alphabétique. Lorsqu'elle est exécutée sur des colonnes `boolean`, elle renvoie `true` si l'une des valeurs l'est.\n ", + "languageDocumentation.documentationESQL.greatest.markdown": " ### GREATEST Renvoie la valeur maximale de plusieurs colonnes. Cette fonction est similaire à `MV_MAX`. Toutefois, elle est destinée à être exécutée sur plusieurs colonnes à la fois. ``` ROW a = 10, b = 20 | EVAL g = GREATEST(a, b) ``` Remarque : Lorsque cette fonction est exécutée sur les champs `keyword` ou `text`, elle renvoie la dernière chaîne dans l'ordre alphabétique. Lorsqu'elle est exécutée sur des colonnes `boolean`, elle renvoie `true` si l'une des valeurs l'est.", "languageDocumentation.documentationESQL.grok": "GROK", - "languageDocumentation.documentationESQL.grok.markdown": "### GROK\n`GROK` vous permet d'extraire des données structurées d'une chaîne. `GROK` compare la chaîne à des modèles, sur la base d’expressions régulières, et extrait les modèles indiqués en tant que colonnes.\n\nPour obtenir la syntaxe des modèles \"grok\", consultez [la documentation relative au processeur \"grok\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html).\n\n````\nROW a = \"12 15.5 15.6 true\"\n| GROK a \"%'{NUMBER:b:int}' %'{NUMBER:c:float}' %'{NUMBER:d:double}' %'{WORD:e:boolean}'\"\n````\n ", + "languageDocumentation.documentationESQL.grok.markdown": "### GROK `GROK` vous permet d'extraire des données structurées d'une chaîne. `GROK` compare la chaîne à des modèles, sur la base d'expressions régulières, et extrait les modèles indiqués en tant que colonnes. Pour obtenir la syntaxe des modèles \"grok\", consultez la [documentation relative au processeur \"grok\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html). ``` ROW a = \"12 15.5 15.6 true\" | GROK a \"%'{NUMBER:b:int}' %'{NUMBER:c:float}' %'{NUMBER:d:double}' %'{WORD:e:boolean}'\" ```", "languageDocumentation.documentationESQL.groupingFunctions": "Fonctions de groupage", "languageDocumentation.documentationESQL.groupingFunctionsDocumentationESQLDescription": "Ces fonctions de regroupement peuvent être utilisées avec `STATS...BY` :", + "languageDocumentation.documentationESQL.hypot": "HYPOT", + "languageDocumentation.documentationESQL.hypot.markdown": " ### HYPOT Renvoie l'hypoténuse de deux nombres. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. Les hypoténuses des infinis sont nulles. ``` ROW a = 3.0, b = 4.0 | EVAL c = HYPOT(a, b) ```", "languageDocumentation.documentationESQL.inOperator": "IN", - "languageDocumentation.documentationESQL.inOperator.markdown": "### IN\nL'opérateur `IN` permet de tester si un champ ou une expression est égal à un élément d'une liste de littéraux, de champs ou d'expressions :\n\n````\nROW a = 1, b = 4, c = 3\n| WHERE c-a IN (3, b / 2, a)\n````\n ", + "languageDocumentation.documentationESQL.inOperator.markdown": "### IN L'opérateur `IN` permet de tester si un champ ou une expression sont égaux à un élément d'une liste de littéraux, de champs ou d'expressions : ``` ROW a = 1, b = 4, c = 3 | WHERE c-a IN (3, b / 2, a) ```", "languageDocumentation.documentationESQL.ip_prefix": "IP_PREFIX", - "languageDocumentation.documentationESQL.ip_prefix.markdown": "\n\n ### IP_PREFIX\n Tronque une adresse IP à une longueur de préfixe donnée.\n\n ````\n row ip4 = to_ip(\"1.2.3.4\"), ip6 = to_ip(\"fe80::cae2:65ff:fece:feb9\")\n | eval ip4_prefix = ip_prefix(ip4, 24, 0), ip6_prefix = ip_prefix(ip6, 0, 112);\n ````\n ", + "languageDocumentation.documentationESQL.ip_prefix.markdown": " ### IP_PREFIX Tronque une adresse IP à une longueur de préfixe donnée. ``` row ip4 = to_ip(\"1.2.3.4\"), ip6 = to_ip(\"fe80::cae2:65ff:fece:feb9\") | eval ip4_prefix = ip_prefix(ip4, 24, 0), ip6_prefix = ip_prefix(ip6, 0, 112); ```", "languageDocumentation.documentationESQL.keep": "KEEP", - "languageDocumentation.documentationESQL.keep.markdown": "### KEEP\nLa commande `KEEP` permet de définir les colonnes qui seront renvoyées et l'ordre dans lequel elles le seront.\n\nPour limiter les colonnes retournées, utilisez une liste de noms de colonnes séparés par des virgules. Les colonnes sont renvoyées dans l'ordre indiqué :\n \n````\nFROM employees\n| KEEP first_name, last_name, height\n````\n\nPlutôt que de spécifier chaque colonne par son nom, vous pouvez utiliser des caractères génériques pour renvoyer toutes les colonnes dont le nom correspond à un modèle :\n\n````\nFROM employees\n| KEEP h*\n````\n\nLe caractère générique de l'astérisque (\"*\") placé de manière isolée transpose l'ensemble des colonnes qui ne correspondent pas aux autres arguments. La requête suivante renverra en premier lieu toutes les colonnes dont le nom commence par un h, puis toutes les autres colonnes :\n\n````\nFROM employees\n| KEEP h*, *\n````\n ", + "languageDocumentation.documentationESQL.keep.markdown": "### KEEP La commande `KEEP` permet de définir les colonnes qui seront renvoyées et l'ordre dans lequel elles le seront. Pour limiter les colonnes retournées, utilisez une liste de noms de colonnes séparés par des virgules. Les colonnes sont renvoyées dans l'ordre indiqué : ``` FROM employees | KEEP first_name, last_name, height ``` Plutôt que de spécifier chaque colonne par son nom, vous pouvez utiliser des caractères génériques pour renvoyer toutes les colonnes dont le nom correspond à un modèle : ``` FROM employees | KEEP h* ``` Le caractère générique de l'astérisque (`*`) placé de manière isolée transpose l'ensemble des colonnes qui ne correspondent pas aux autres arguments. La requête suivante renverra en premier lieu toutes les colonnes dont le nom commence par un \"h\", puis toutes les autres colonnes : ``` FROM employees | KEEP h*, * ```", "languageDocumentation.documentationESQL.least": "LEAST", - "languageDocumentation.documentationESQL.least.markdown": "\n\n ### LEAST\n Renvoie la valeur minimale de plusieurs colonnes. Cette fonction est similaire à `MV_MIN`. Toutefois, elle est destinée à être exécutée sur plusieurs colonnes à la fois.\n\n ````\n ROW a = 10, b = 20\n | EVAL l = LEAST(a, b)\n ````\n ", + "languageDocumentation.documentationESQL.least.markdown": " ### LEAST Renvoie la valeur minimale de plusieurs colonnes. Cette fonction est similaire à `MV_MIN`. Toutefois, elle est destinée à être exécutée sur plusieurs colonnes à la fois. ``` ROW a = 10, b = 20 | EVAL l = LEAST(a, b) ```", "languageDocumentation.documentationESQL.left": "LEFT", - "languageDocumentation.documentationESQL.left.markdown": "\n\n ### LEFT\n Renvoie la sous-chaîne qui extrait la \"longueur\" des caractères de la \"chaîne\" en partant de la gauche.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL left = LEFT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ````\n ", + "languageDocumentation.documentationESQL.left.markdown": " ### LEFT Renvoie la sous-chaîne qui extrait la \"longueur\" des caractères de la \"chaîne\" en partant de la gauche. ``` FROM employees | KEEP last_name | EVAL left = LEFT(last_name, 3) | SORT last_name ASC | LIMIT 5 ```", "languageDocumentation.documentationESQL.length": "LENGHT", - "languageDocumentation.documentationESQL.length.markdown": "\n\n ### LENGTH\n Renvoie la longueur des caractères d'une chaîne.\n\n ````\n FROM employees\n | KEEP first_name, last_name\n | EVAL fn_length = LENGTH(first_name)\n ````\n ", + "languageDocumentation.documentationESQL.length.markdown": " ### LENGTH Renvoie la longueur des caractères d'une chaîne. ``` FROM employees | KEEP first_name, last_name | EVAL fn_length = LENGTH(first_name) ```", "languageDocumentation.documentationESQL.limit": "LIMIT", - "languageDocumentation.documentationESQL.limit.markdown": "### LIMIT\nLa commande de traitement `LIMIT` permet de restreindre le nombre de lignes :\n \n````\nFROM employees\n| LIMIT 5\n````\n ", + "languageDocumentation.documentationESQL.limit.markdown": "### LIMIT La commande de traitement `LIMIT` permet de restreindre le nombre de lignes : ``` FROM employees | LIMIT 5 ```", "languageDocumentation.documentationESQL.locate": "LOCATE", - "languageDocumentation.documentationESQL.locate.markdown": "\n\n ### LOCATE\n Renvoie un entier qui indique la position d'une sous-chaîne de mots-clés dans une autre chaîne\n\n ````\n row a = \"hello\"\n | eval a_ll = locate(a, \"ll\")\n ````\n ", + "languageDocumentation.documentationESQL.locate.markdown": " ### LOCATE Renvoie un entier qui indique la position d'une sous-chaîne de mots-clés dans une autre chaîne. Renvoie `0` si la sous-chaîne ne peut pas être trouvée. Notez que les positions des chaînes commencent à partir de `1`. ``` row a = \"hello\" | eval a_ll = locate(a, \"ll\") ```", "languageDocumentation.documentationESQL.log": "LOG", - "languageDocumentation.documentationESQL.log.markdown": "\n\n ### LOG\n Renvoie le logarithme d'une valeur dans une base. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n\n Les journaux de zéros, de nombres négatifs et de base 1 renvoient `null` ainsi qu'un avertissement.\n\n ````\n ROW base = 2.0, value = 8.0\n | EVAL s = LOG(base, value)\n ````\n ", + "languageDocumentation.documentationESQL.log.markdown": " ### LOG Renvoie le logarithme d'une valeur dans une base. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. Les logs de zéros, de nombres négatifs et de base 1 renvoient `null` ainsi qu'un avertissement. ``` ROW base = 2.0, value = 8.0 | EVAL s = LOG(base, value) ```", "languageDocumentation.documentationESQL.log10": "LOG10", - "languageDocumentation.documentationESQL.log10.markdown": "\n\n ### LOG10\n Renvoie le logarithme d'une valeur en base 10. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n\n Les logs de 0 et de nombres négatifs renvoient `null` ainsi qu'un avertissement.\n\n ````\n ROW d = 1000.0 \n | EVAL s = LOG10(d)\n ````\n ", + "languageDocumentation.documentationESQL.log10.markdown": " ### LOG10 Renvoie le logarithme d'une valeur en base 10. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. Les logs de 0 et de nombres négatifs renvoient `null` ainsi qu'un avertissement. ``` ROW d = 1000.0 | EVAL s = LOG10(d) ```", "languageDocumentation.documentationESQL.ltrim": "LTRIM", - "languageDocumentation.documentationESQL.ltrim.markdown": "\n\n ### LTRIM\n Retire les espaces au début des chaînes.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = LTRIM(message)\n | EVAL color = LTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ````\n ", - "languageDocumentation.documentationESQL.markdown": "## ES|QL\n\nUne requête ES|QL (langage de requête Elasticsearch) se compose d'une série de commandes, séparées par une barre verticale : `|`. Chaque requête commence par une **commande source**, qui produit un tableau, habituellement avec des données issues d'Elasticsearch. \n\nUne commande source peut être suivie d'une ou plusieurs **commandes de traitement**. Les commandes de traitement peuvent modifier le tableau de sortie de la commande précédente en ajoutant, supprimant ou modifiant les lignes et les colonnes.\n\n````\nsource-command\n| processing-command1\n| processing-command2\n````\n\nLe résultat d'une requête est le tableau produit par la dernière commande de traitement. \n ", + "languageDocumentation.documentationESQL.ltrim.markdown": " ### LTRIM Supprime les espaces au début d'une chaîne. ``` ROW message = \" some text \", color = \" red \" | EVAL message = LTRIM(message) | EVAL color = LTRIM(color) | EVAL message = CONCAT(\"'\", message, \"'\") | EVAL color = CONCAT(\"'\", color, \"'\") ```", + "languageDocumentation.documentationESQL.markdown": "Une requête ES|QL (langage de requête Elasticsearch) se compose d'une série de commandes, séparées par une barre verticale : `|`. Chaque requête commence par une **commande source**, qui produit un tableau, habituellement avec des données issues d'Elasticsearch. Une commande source peut être suivie d'une ou plusieurs **commandes de traitement**. Les commandes de traitement peuvent modifier le tableau de sortie de la commande précédente en ajoutant, supprimant ou modifiant les lignes et les colonnes. ``` source-command | processing-command1 | processing-command2 ``` Le résultat d'une requête est le tableau produit par la dernière commande de traitement.", + "languageDocumentation.documentationESQL.max": "MAX", + "languageDocumentation.documentationESQL.max.markdown": " ### MAX La valeur maximale d'un champ. ``` FROM employees | STATS MAX(languages) ```", + "languageDocumentation.documentationESQL.median": "MEDIAN", + "languageDocumentation.documentationESQL.median_absolute_deviation": "MEDIAN_ABSOLUTE_DEVIATION", + "languageDocumentation.documentationESQL.median_absolute_deviation.markdown": " ### MEDIAN_ABSOLUTE_DEVIATION Renvoie l'écart absolu médian, une mesure de la variabilité. Il s'agit d'un indicateur robuste, ce qui signifie qu'il est utile pour décrire des données qui peuvent présenter des valeurs aberrantes ou ne pas être normalement distribuées. Pour de telles données, il peut être plus descriptif que l'écart-type. Il est calculé comme la médiane de chaque écart de point de données par rapport à la médiane de l'ensemble de l'échantillon. Autrement dit, pour une variable aléatoire `X`, l'écart absolu médian est `median(|median(X) - X|)`. ``` FROM employees | STATS MEDIAN(salary), MEDIAN_ABSOLUTE_DEVIATION(salary) ``` Remarque : Comme `PERCENTILE`, `MEDIAN_ABSOLUTE_DEVIATION` est généralement approximatif.", + "languageDocumentation.documentationESQL.median.markdown": " ### MEDIAN La valeur qui est supérieure à la moitié de toutes les valeurs et inférieure à la moitié de toutes les valeurs, également connue sous le nom de `PERCENTILE` 50 %. ``` FROM employees | STATS MEDIAN(salary), PERCENTILE(salary, 50) ``` Remarque : Comme `PERCENTILE`, `MEDIAN` est généralement approximatif.", + "languageDocumentation.documentationESQL.min": "MIN", + "languageDocumentation.documentationESQL.min.markdown": " ### MIN La valeur minimale d'un champ. ``` FROM employees | STATS MIN(languages) ```", "languageDocumentation.documentationESQL.mv_append": "MV_APPEND", - "languageDocumentation.documentationESQL.mv_append.markdown": "\n\n ### MV_APPEND\n Concatène les valeurs de deux champs à valeurs multiples.\n\n ", + "languageDocumentation.documentationESQL.mv_append.markdown": " ### MV_APPEND Concatène les valeurs de deux champs à valeurs multiples.", "languageDocumentation.documentationESQL.mv_avg": "MV_AVG", - "languageDocumentation.documentationESQL.mv_avg.markdown": "\n\n ### MV_AVG\n Convertit un champ multivalué en un champ à valeur unique comprenant la moyenne de toutes les valeurs.\n\n ````\n ROW a=[3, 5, 1, 6]\n | EVAL avg_a = MV_AVG(a)\n ````\n ", + "languageDocumentation.documentationESQL.mv_avg.markdown": " ### MV_AVG Convertit un champ multivalué en un champ à valeur unique comprenant la moyenne de toutes les valeurs. ``` ROW a=[3, 5, 1, 6] | EVAL avg_a = MV_AVG(a) ```", "languageDocumentation.documentationESQL.mv_concat": "MV_CONCAT", - "languageDocumentation.documentationESQL.mv_concat.markdown": "\n\n ### MV_CONCAT\n Convertit une expression de type chaîne multivalué en une colonne à valeur unique comprenant la concaténation de toutes les valeurs, séparées par un délimiteur.\n\n ````\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL j = MV_CONCAT(a, \", \")\n ````\n ", + "languageDocumentation.documentationESQL.mv_concat.markdown": " ### MV_CONCAT Convertit une expression de type chaîne multivaluée en une colonne à valeur unique comprenant la concaténation de toutes les valeurs, séparées par un délimiteur. ``` ROW a=[\"foo\", \"zoo\", \"bar\"] | EVAL j = MV_CONCAT(a, \", \") ```", "languageDocumentation.documentationESQL.mv_count": "MV_COUNT", - "languageDocumentation.documentationESQL.mv_count.markdown": "\n\n ### MV_COUNT\n Convertit une expression multivaluée en une colonne à valeur unique comprenant le total du nombre de valeurs.\n\n ````\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL count_a = MV_COUNT(a)\n ````\n ", + "languageDocumentation.documentationESQL.mv_count.markdown": " ### MV_COUNT Convertit une expression multivaluée en une colonne à valeur unique comprenant un décompte du nombre de valeurs. ``` ROW a=[\"foo\", \"zoo\", \"bar\"] | EVAL count_a = MV_COUNT(a) ```", "languageDocumentation.documentationESQL.mv_dedupe": "MV_DEDUPE", - "languageDocumentation.documentationESQL.mv_dedupe.markdown": "\n\n ### MV_DEDUPE\n Supprime les valeurs en doublon d'un champ multivalué.\n\n ````\n ROW a=[\"foo\", \"foo\", \"bar\", \"foo\"]\n | EVAL dedupe_a = MV_DEDUPE(a)\n ````\n Remarque : la fonction `MV_DEDUPE` est en mesure de trier les valeurs de la colonne, mais ne le fait pas systématiquement.\n ", + "languageDocumentation.documentationESQL.mv_dedupe.markdown": " ### MV_DEDUPE Supprime les valeurs en doublon d'un champ multivalué. ``` ROW a=[\"foo\", \"foo\", \"bar\", \"foo\"] | EVAL dedupe_a = MV_DEDUPE(a) ``` Remarque : La fonction `MV_DEDUPE` est en mesure de trier les valeurs de la colonne, mais ne le fait pas systématiquement.", "languageDocumentation.documentationESQL.mv_first": "MV_FIRST", - "languageDocumentation.documentationESQL.mv_first.markdown": "\n\n ### MV_FIRST\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la\n première valeur. Ceci est particulièrement utile pour lire une fonction qui émet\n des colonnes multivaluées dans un ordre connu, comme `SPLIT`.\n\n L'ordre dans lequel les champs multivalués sont lus à partir\n du stockage sous-jacent n'est pas garanti. Il est *souvent* ascendant, mais ne vous y\n fiez pas. Si vous avez besoin de la valeur minimale, utilisez `MV_MIN` au lieu de\n `MV_FIRST`. `MV_MIN` comporte des optimisations pour les valeurs triées, il n'y a donc aucun\n avantage en matière de performances pour `MV_FIRST`.\n\n ````\n ROW a=\"foo;bar;baz\"\n | EVAL first_a = MV_FIRST(SPLIT(a, \";\"))\n ````\n ", + "languageDocumentation.documentationESQL.mv_first.markdown": " ### MV_FIRST Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur minimale. Ceci est particulièrement utile pour lire une fonction qui émet des colonnes multivaluées dans un ordre connu, comme `SPLIT`. ``` ROW a=\"foo;bar;baz\" | EVAL first_a = MV_FIRST(SPLIT(a, \";\")) ```", "languageDocumentation.documentationESQL.mv_last": "MV_LAST", - "languageDocumentation.documentationESQL.mv_last.markdown": "\n\n ### MV_LAST\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la dernière\n valeur. Ceci est particulièrement utile pour lire une fonction qui émet des champs multivalués\n dans un ordre connu, comme `SPLIT`.\n\n L'ordre dans lequel les champs multivalués sont lus à partir\n du stockage sous-jacent n'est pas garanti. Il est *souvent* ascendant, mais ne vous y\n fiez pas. Si vous avez besoin de la valeur maximale, utilisez `MV_MAX` au lieu de\n `MV_LAST`. `MV_MAX` comporte des optimisations pour les valeurs triées, il n'y a donc aucun\n avantage en matière de performances pour `MV_LAST`.\n\n ````\n ROW a=\"foo;bar;baz\"\n | EVAL last_a = MV_LAST(SPLIT(a, \";\"))\n ````\n ", + "languageDocumentation.documentationESQL.mv_last.markdown": " ### MV_LAST Convertit une expression multivaluée en une colonne à valeur unique comprenant la dernière valeur. Ceci est particulièrement utile pour lire une fonction qui émet des colonnes multivaluées dans un ordre connu, comme `SPLIT`. ``` ROW a=\"foo;bar;baz\" | EVAL last_a = MV_LAST(SPLIT(a, \";\")) ```", "languageDocumentation.documentationESQL.mv_max": "MV_MAX", - "languageDocumentation.documentationESQL.mv_max.markdown": "\n\n ### MV_MAX\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur maximale.\n\n ````\n ROW a=[3, 5, 1]\n | EVAL max_a = MV_MAX(a)\n ````\n ", + "languageDocumentation.documentationESQL.mv_max.markdown": " ### MV_MAX Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur maximale. ``` ROW a=[3, 5, 1] | EVAL max_a = MV_MAX(a) ```", "languageDocumentation.documentationESQL.mv_median": "MV_MEDIAN", - "languageDocumentation.documentationESQL.mv_median.markdown": "\n\n ### MV_MEDIAN\n Convertit un champ multivalué en un champ à valeur unique comprenant la valeur médiane.\n\n ````\n ROW a=[3, 5, 1]\n | EVAL median_a = MV_MEDIAN(a)\n ````\n ", + "languageDocumentation.documentationESQL.mv_median_absolute_deviation": "MV_MEDIAN_ABSOLUTE_DEVIATION", + "languageDocumentation.documentationESQL.mv_median_absolute_deviation.markdown": " ### MV_MEDIAN_ABSOLUTE_DEVIATION Convertit un champ multivalué en un champ à valeur unique comprenant l'écart absolu médian. Il est calculé comme la médiane de chaque écart de point de données par rapport à la médiane de l'ensemble de l'échantillon. Autrement dit, pour une variable aléatoire `X`, l'écart absolu médian est `median(|median(X) - X|)`. ``` ROW values = [0, 2, 5, 6] | EVAL median_absolute_deviation = MV_MEDIAN_ABSOLUTE_DEVIATION(values), median = MV_MEDIAN(values) ``` Remarque : Si le champ a un nombre pair de valeurs, les médianes seront calculées comme la moyenne des deux valeurs du milieu. Si la valeur n'est pas un nombre à virgule flottante, les moyennes sont arrondies vers 0.", + "languageDocumentation.documentationESQL.mv_median.markdown": " ### MV_MEDIAN Convertit un champ multivalué en un champ à valeur unique comprenant la valeur médiane. ``` ROW a=[3, 5, 1] | EVAL median_a = MV_MEDIAN(a) ```", "languageDocumentation.documentationESQL.mv_min": "MV_MIN", - "languageDocumentation.documentationESQL.mv_min.markdown": "\n\n ### MV_MIN\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur minimale.\n\n ````\n ROW a=[2, 1]\n | EVAL min_a = MV_MIN(a)\n ````\n ", + "languageDocumentation.documentationESQL.mv_min.markdown": " ### MV_MIN Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur minimale. ``` ROW a=[2, 1] | EVAL min_a = MV_MIN(a) ```", + "languageDocumentation.documentationESQL.mv_percentile": "MV_PERCENTILE", + "languageDocumentation.documentationESQL.mv_percentile.markdown": " ### MV_PERCENTILE Convertit un champ multivalué en un champ à valeur unique comprenant la valeur à laquelle un certain pourcentage des valeurs observées se produit. ``` ROW values = [5, 5, 10, 12, 5000] | EVAL p50 = MV_PERCENTILE(values, 50), median = MV_MEDIAN(values) ```", + "languageDocumentation.documentationESQL.mv_pseries_weighted_sum": "MV_PSERIES_WEIGHTED_SUM", + "languageDocumentation.documentationESQL.mv_pseries_weighted_sum.markdown": " ### MV_PSERIES_WEIGHTED_SUM Convertit une expression multivaluée en une colonne à valeur unique en multipliant chaque élément de la liste d'entrée par le terme correspondant dans P-Series et en calculant la somme. ``` ROW a = [70.0, 45.0, 21.0, 21.0, 21.0] | EVAL sum = MV_PSERIES_WEIGHTED_SUM(a, 1.5) | KEEP sum ```", "languageDocumentation.documentationESQL.mv_slice": "MV_SLICE", - "languageDocumentation.documentationESQL.mv_slice.markdown": "\n\n ### MV_SLICE\n Renvoie un sous-ensemble du champ multivalué en utilisant les valeurs d'index de début et de fin.\n\n ````\n row a = [1, 2, 2, 3]\n | eval a1 = mv_slice(a, 1), a2 = mv_slice(a, 2, 3)\n ````\n ", + "languageDocumentation.documentationESQL.mv_slice.markdown": " ### MV_SLICE Renvoie un sous-ensemble du champ multivalué en utilisant les valeurs d'index de début et de fin. Ceci est particulièrement utile pour lire une fonction qui émet des colonnes multivaluées dans un ordre connu, comme `SPLIT` ou `MV_SORT`. ``` row a = [1, 2, 2, 3] | eval a1 = mv_slice(a, 1), a2 = mv_slice(a, 2, 3) ```", "languageDocumentation.documentationESQL.mv_sort": "MV_SORT", - "languageDocumentation.documentationESQL.mv_sort.markdown": "\n\n ### MV_SORT\n Trie une expression multivaluée par ordre lexicographique.\n\n ````\n ROW a = [4, 2, -3, 2]\n | EVAL sa = mv_sort(a), sd = mv_sort(a, \"DESC\")\n ````\n ", + "languageDocumentation.documentationESQL.mv_sort.markdown": " ### MV_SORT Trie un champ multivalué par ordre lexicographique. ``` ROW a = [4, 2, -3, 2] | EVAL sa = mv_sort(a), sd = mv_sort(a, \"DESC\") ```", "languageDocumentation.documentationESQL.mv_sum": "MV_SUM", - "languageDocumentation.documentationESQL.mv_sum.markdown": "\n\n ### MV_SUM\n Convertit un champ multivalué en un champ à valeur unique comprenant la somme de toutes les valeurs.\n\n ````\n ROW a=[3, 5, 6]\n | EVAL sum_a = MV_SUM(a)\n ````\n ", + "languageDocumentation.documentationESQL.mv_sum.markdown": " ### MV_SUM Convertit un champ multivalué en un champ à valeur unique comprenant la somme de toutes les valeurs. ``` ROW a=[3, 5, 6] | EVAL sum_a = MV_SUM(a) ```", "languageDocumentation.documentationESQL.mv_zip": "MV_ZIP", - "languageDocumentation.documentationESQL.mv_zip.markdown": "\n\n ### MV_ZIP\n Combine les valeurs de deux champs multivalués avec un délimiteur qui les relie.\n\n ````\n ROW a = [\"x\", \"y\", \"z\"], b = [\"1\", \"2\"]\n | EVAL c = mv_zip(a, b, \"-\")\n | KEEP a, b, c\n ````\n ", + "languageDocumentation.documentationESQL.mv_zip.markdown": " ### MV_ZIP Combine les valeurs de deux champs multivalués avec un délimiteur qui les relie. ``` ROW a = [\"x\", \"y\", \"z\"], b = [\"1\", \"2\"] | EVAL c = mv_zip(a, b, \"-\") | KEEP a, b, c ```", "languageDocumentation.documentationESQL.mvExpand": "MV_EXPAND", - "languageDocumentation.documentationESQL.mvExpand.markdown": "### MV_EXPAND\nLa commande de traitement `MV_EXPAND` développe les champs multivalués en indiquant une valeur par ligne et en dupliquant les autres champs : \n````\nROW a=[1,2,3], b=\"b\", j=[\"a\",\"b\"]\n| MV_EXPAND a\n````\n ", + "languageDocumentation.documentationESQL.mvExpand.markdown": "### MV_EXPAND La commande de traitement `MV_EXPAND` développe les champs multivalués en indiquant une valeur par ligne et en dupliquant les autres champs : ``` ROW a=[1,2,3], b=\"b\", j=[\"a\",\"b\"] | MV_EXPAND a ```", "languageDocumentation.documentationESQL.now": "NOW", - "languageDocumentation.documentationESQL.now.markdown": "\n\n ### NOW\n Renvoie la date et l'heure actuelles.\n\n ````\n ROW current_date = NOW()\n ````\n ", + "languageDocumentation.documentationESQL.now.markdown": " ### NOW Renvoie la date et l'heure actuelles. ``` ROW current_date = NOW() ```", "languageDocumentation.documentationESQL.operators": "Opérateurs", "languageDocumentation.documentationESQL.operatorsDocumentationESQLDescription": "ES|QL est compatible avec les opérateurs suivants :", + "languageDocumentation.documentationESQL.percentile": "PERCENTILE", + "languageDocumentation.documentationESQL.percentile.markdown": " ### PERCENTILE Renvoie la valeur à laquelle un certain pourcentage des valeurs observées se produit. Par exemple, le 95e percentile est la valeur qui est supérieure à 95 % des valeurs observées et le 50percentile est la médiane (`MEDIAN`). ``` FROM employees | STATS p0 = PERCENTILE(salary, 0) , p50 = PERCENTILE(salary, 50) , p99 = PERCENTILE(salary, 99) ```", "languageDocumentation.documentationESQL.pi": "PI", - "languageDocumentation.documentationESQL.pi.markdown": "\n\n ### PI\n Renvoie Pi, le rapport entre la circonférence et le diamètre d'un cercle.\n\n ````\n ROW PI()\n ````\n ", + "languageDocumentation.documentationESQL.pi.markdown": " ### PI Renvoie Pi, le rapport entre la circonférence et le diamètre d'un cercle. ``` ROW PI() ```", "languageDocumentation.documentationESQL.pow": "POW", - "languageDocumentation.documentationESQL.pow.markdown": "\n\n ### POW\n Renvoie la valeur d’une `base` élevée à la puissance d’un `exposant`.\n\n ````\n ROW base = 2.0, exponent = 2\n | EVAL result = POW(base, exponent)\n ````\n Remarque : Il est toujours possible de dépasser un résultat double ici ; dans ce cas, la valeur `null` sera renvoyée.\n ", + "languageDocumentation.documentationESQL.pow.markdown": " ### POW Renvoie la valeur d'une `base` élevée à la puissance d'un exposant (`exponent`). ``` ROW base = 2.0, exponent = 2 | EVAL result = POW(base, exponent) ``` Remarque : Il est toujours possible de dépasser un résultat double ici ; dans ce cas, la valeur `null` sera renvoyée.", "languageDocumentation.documentationESQL.predicates": "valeurs NULL", - "languageDocumentation.documentationESQL.predicates.markdown": "### Valeurs NULL\nPour une comparaison avec une valeur NULL, utilisez les attributs `IS NULL` et `IS NOT NULL` :\n\n````\nFROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3\n````\n\n````\nFROM employees\n| WHERE is_rehired IS NOT NULL\n| STATS count(emp_no)\n````\n ", + "languageDocumentation.documentationESQL.predicates.markdown": "### Valeurs NULL Pour une comparaison avec une valeur NULL, utilisez les attributs `IS NULL` et `IS NOT NULL` : ``` FROM employees | WHERE birth_date IS NULL | KEEP first_name, last_name | SORT first_name | LIMIT 3 ``` ``` FROM employees | WHERE is_rehired IS NOT NULL | STATS count(emp_no) ```", "languageDocumentation.documentationESQL.processingCommands": "Traitement des commandes", "languageDocumentation.documentationESQL.processingCommandsDescription": "Le traitement des commandes transforme un tableau des entrées par l'ajout, le retrait ou la modification des lignes et des colonnes. ES|QL est compatible avec le traitement des commandes suivant.", "languageDocumentation.documentationESQL.rename": "RENAME", - "languageDocumentation.documentationESQL.rename.markdown": "### RENAME\nUtilisez `RENAME` pour renommer une colonne en utilisant la syntaxe suivante :\n\n````\nRENAME AS \n````\n\nPar exemple :\n\n````\nFROM employees\n| KEEP first_name, last_name, still_hired\n| RENAME still_hired AS employed\n````\n\nSi une colonne portant le nouveau nom existe déjà, elle sera remplacée par la nouvelle colonne.\n\nPlusieurs colonnes peuvent être renommées à l'aide d'une seule commande `RENAME` :\n\n````\nFROM employees\n| KEEP first_name, last_name\n| RENAME first_name AS fn, last_name AS ln\n````\n ", + "languageDocumentation.documentationESQL.rename.markdown": "### RENAME Utilisez `RENAME` pour renommer une colonne en utilisant la syntaxe suivante : ``` RENAME AS ``` For example: ``` FROM employees | KEEP first_name, last_name, still_hired | RENAME still_hired AS employed ``` If a column with the new name already exists, it will be replaced by the new column. Multiple columns can be renamed with a single `RENAME` command: ``` FROM employees | KEEP first_name, last_name | RENAME first_name AS fn, last_name AS ln ```", "languageDocumentation.documentationESQL.repeat": "REPEAT", - "languageDocumentation.documentationESQL.repeat.markdown": "\n\n ### REPEAT\n Renvoie une chaîne construite par la concaténation de la `chaîne` avec elle-même, le `nombre` de fois spécifié.\n\n ````\n ROW a = \"Hello!\"\n | EVAL triple_a = REPEAT(a, 3);\n ````\n ", + "languageDocumentation.documentationESQL.repeat.markdown": " ### REPEAT Renvoie une chaîne construite par la concaténation de la chaîne (`string`) avec elle-même, le nombre (`number`) de fois spécifié. ``` ROW a = \"Hello!\" | EVAL triple_a = REPEAT(a, 3); ```", "languageDocumentation.documentationESQL.replace": "REPLACE", - "languageDocumentation.documentationESQL.replace.markdown": "\n\n ### REPLACE\n La fonction remplace dans la chaîne `str` toutes les correspondances avec l'expression régulière `regex`\n par la chaîne de remplacement `newStr`.\n\n ````\n ROW str = \"Hello World\"\n | EVAL str = REPLACE(str, \"World\", \"Universe\")\n | KEEP str\n ````\n ", + "languageDocumentation.documentationESQL.replace.markdown": " ### REPLACE La fonction remplace dans la chaîne `str` toutes les correspondances avec l'expression régulière `regex` par la chaîne de remplacement `newStr`. ``` ROW str = \"Hello World\" | EVAL str = REPLACE(str, \"World\", \"Universe\") | KEEP str ```", + "languageDocumentation.documentationESQL.reverse": "REVERSE", + "languageDocumentation.documentationESQL.reverse.markdown": " ### REVERSE Renvoie une nouvelle chaîne représentant la chaîne d'entrée dans l'ordre inverse. ``` ROW message = \"Some Text\" | EVAL message_reversed = REVERSE(message); ```", "languageDocumentation.documentationESQL.right": "RIGHT", - "languageDocumentation.documentationESQL.right.markdown": "\n\n ### RIGHT\n Renvoie la sous-chaîne qui extrait la longueur des caractères de `str` en partant de la droite.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL right = RIGHT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ````\n ", + "languageDocumentation.documentationESQL.right.markdown": " ### RIGHT Renvoie la sous-chaîne qui extrait la \"longueur\" des caractères de `str` en partant de la droite. ``` FROM employees | KEEP last_name | EVAL right = RIGHT(last_name, 3) | SORT last_name ASC | LIMIT 5 ```", "languageDocumentation.documentationESQL.round": "ROUND", - "languageDocumentation.documentationESQL.round.markdown": "\n\n ### ROUND\n Arrondit un nombre au nombre spécifié de décimales.\n La valeur par défaut est 0, qui renvoie l'entier le plus proche. Si le\n nombre de décimales spécifié est négatif, la fonction arrondit au nombre de décimales à gauche\n de la virgule.\n\n ````\n FROM employees\n | KEEP first_name, last_name, height\n | EVAL height_ft = ROUND(height * 3.281, 1)\n ````\n ", + "languageDocumentation.documentationESQL.round.markdown": " ### ROUND Arrondit un nombre au nombre spécifié de décimales. La valeur par défaut est 0, qui renvoie l'entier le plus proche. Si le nombre de décimales spécifié est négatif, la fonction arrondit au nombre de décimales à gauche. ``` FROM employees | KEEP first_name, last_name, height | EVAL height_ft = ROUND(height * 3.281, 1) ```", "languageDocumentation.documentationESQL.row": "ROW", - "languageDocumentation.documentationESQL.row.markdown": "### ROW\nLa commande source `ROW` renvoie une ligne contenant une ou plusieurs colonnes avec les valeurs que vous spécifiez. Cette commande peut s'avérer utile pour les tests.\n \n````\nROW a = 1, b = \"two\", c = null\n````\n\nUtilisez des crochets pour créer des colonnes à valeurs multiples :\n\n````\nROW a = [2, 1]\n````\n\nROW permet d'utiliser des fonctions :\n\n````\nROW a = ROUND(1.23, 0)\n````\n ", + "languageDocumentation.documentationESQL.row.markdown": "### ROW La commande source `ROW` renvoie une ligne contenant une ou plusieurs colonnes avec les valeurs que vous spécifiez. Cette commande peut s'avérer utile pour les tests. ``` ROW a = 1, b = \"two\", c = null ``` Utilisez des crochets pour créer des colonnes à valeurs multiples : ``` ROW a = [2, 1] ``` ROW permet d'utiliser des fonctions : ``` ROW a = ROUND(1.23, 0) ```", "languageDocumentation.documentationESQL.rtrim": "RTRIM", - "languageDocumentation.documentationESQL.rtrim.markdown": "\n\n ### RTRIM\n Supprime les espaces à la fin des chaînes.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = RTRIM(message)\n | EVAL color = RTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ````\n ", + "languageDocumentation.documentationESQL.rtrim.markdown": " ### RTRIM Supprime les espaces à la fin d'une chaîne. ``` ROW message = \" some text \", color = \" red \" | EVAL message = RTRIM(message) | EVAL color = RTRIM(color) | EVAL message = CONCAT(\"'\", message, \"'\") | EVAL color = CONCAT(\"'\", color, \"'\") ```", "languageDocumentation.documentationESQL.show": "SHOW", - "languageDocumentation.documentationESQL.show.markdown": "### SHOW\nLa commande source `SHOW ` renvoie des informations sur le déploiement et ses capacités :\n\n* Utilisez `SHOW INFO` pour renvoyer la version du déploiement, la date de compilation et le hachage.\n* Utilisez `SHOW FUNCTIONS` pour renvoyer une liste de toutes les fonctions prises en charge et un résumé de chaque fonction.\n ", + "languageDocumentation.documentationESQL.show.markdown": "### SHOW La commande source `SHOW ` renvoie des informations sur le déploiement et ses capacités : * Utilisez `SHOW INFO` pour renvoyer la version du déploiement, la date de compilation et le hachage. * Utilisez `SHOW FUNCTIONS` pour renvoyer une liste de toutes les fonctions prises en charge et un résumé de chaque fonction.", "languageDocumentation.documentationESQL.signum": "SIGNUM", - "languageDocumentation.documentationESQL.signum.markdown": "\n\n ### SIGNUM\n Renvoie le signe du nombre donné.\n Il renvoie `-1` pour les nombres négatifs, `0` pour `0` et `1` pour les nombres positifs.\n\n ````\n ROW d = 100.0\n | EVAL s = SIGNUM(d)\n ````\n ", + "languageDocumentation.documentationESQL.signum.markdown": " ### SIGNUM Renvoie le signe du nombre donné. Renvoie `-1` pour les nombres négatifs, `0` pour `0` et `1` pour les nombres positifs. ``` ROW d = 100.0 | EVAL s = SIGNUM(d) ```", "languageDocumentation.documentationESQL.sin": "SIN", - "languageDocumentation.documentationESQL.sin.markdown": "\n\n ### SIN\n Renvoie la fonction trigonométrique sinusoïdale d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL sin=SIN(a)\n ````\n ", + "languageDocumentation.documentationESQL.sin.markdown": " ### SIN Renvoie le sinus d'un angle. ``` ROW a=1.8 | EVAL sin=SIN(a) ```", "languageDocumentation.documentationESQL.sinh": "SINH", - "languageDocumentation.documentationESQL.sinh.markdown": "\n\n ### SINH\n Renvoie le sinus hyperbolique d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL sinh=SINH(a)\n ````\n ", + "languageDocumentation.documentationESQL.sinh.markdown": " ### SINH Renvoie le sinus hyperbolique d'un nombre. ``` ROW a=1.8 | EVAL sinh=SINH(a) ```", "languageDocumentation.documentationESQL.sort": "SORT", - "languageDocumentation.documentationESQL.sort.markdown": "### SORT\nUtilisez la commande `SORT` pour trier les lignes sur un ou plusieurs champs :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height\n````\n\nL'ordre de tri par défaut est croissant. Définissez un ordre de tri explicite en utilisant `ASC` ou `DESC` :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC\n````\n\nSi deux lignes disposent de la même clé de tri, l'ordre original sera préservé. Vous pouvez ajouter des expressions de tri pour départager les deux lignes :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC, first_name ASC\n````\n\n#### valeurs `null`\nPar défaut, les valeurs `null` sont considérées comme étant supérieures à toutes les autres valeurs. Selon un ordre de tri croissant, les valeurs `null` sont classées en dernier. Selon un ordre de tri décroissant, les valeurs `null` sont classées en premier. Pour modifier cet ordre, utilisez `NULLS FIRST` ou `NULLS LAST` :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT first_name ASC NULLS FIRST\n````\n ", + "languageDocumentation.documentationESQL.sort.markdown": "### SORT Utilisez la commande `SORT` pour trier les lignes sur un ou plusieurs champs : ``` FROM employees | KEEP first_name, last_name, height | SORT height ``` L'ordre de tri par défaut est croissant. Définissez un ordre de tri explicite en utilisant `ASC` ou `DESC` : ``` FROM employees | KEEP first_name, last_name, height | SORT height DESC ``` Si deux lignes disposent de la même clé de tri, l'ordre original sera préservé. Vous pouvez ajouter des expressions de tri pour départager les deux lignes : ``` FROM employees | KEEP first_name, last_name, height | SORT height DESC, first_name ASC ``` #### Valeurs `null` Par défaut, les valeurs `null` sont considérées comme étant supérieures à toutes les autres valeurs. Selon un ordre de tri croissant, les valeurs `null` sont classées en dernier. Selon un ordre de tri décroissant, les valeurs `null` sont classées en premier. Pour modifier cet ordre, utilisez `NULLS FIRST` ou `NULLS LAST` : ``` FROM employees | KEEP first_name, last_name, height | SORT first_name ASC NULLS FIRST ```", "languageDocumentation.documentationESQL.sourceCommands": "Commandes sources", + "languageDocumentation.documentationESQL.space": "SPACE", + "languageDocumentation.documentationESQL.space.markdown": " ### SPACE Renvoie une chaîne composée d'espaces nombre (`number`). ``` ROW message = CONCAT(\"Hello\", SPACE(1), \"World!\"); ```", "languageDocumentation.documentationESQL.split": "SPLIT", - "languageDocumentation.documentationESQL.split.markdown": "\n\n ### SPLIT\n Divise une chaîne de valeur unique en plusieurs chaînes.\n\n ````\n ROW words=\"foo;bar;baz;qux;quux;corge\"\n | EVAL word = SPLIT(words, \";\")\n ````\n ", + "languageDocumentation.documentationESQL.split.markdown": " ### SPLIT Divise une chaîne de valeur unique en plusieurs chaînes. ``` ROW words=\"foo;bar;baz;qux;quux;corge\" | EVAL word = SPLIT(words, \";\") ```", "languageDocumentation.documentationESQL.sqrt": "SQRT", - "languageDocumentation.documentationESQL.sqrt.markdown": "\n\n ### SQRT\n Renvoie la racine carrée d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n Les racines carrées des nombres négatifs et des infinis sont nulles.\n\n ````\n ROW d = 100.0\n | EVAL s = SQRT(d)\n ````\n ", + "languageDocumentation.documentationESQL.sqrt.markdown": " ### SQRT Renvoie la racine carrée d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. Les racines carrées des nombres négatifs et des infinis sont nulles. ``` ROW d = 100.0 | EVAL s = SQRT(d) ```", + "languageDocumentation.documentationESQL.st_centroid_agg": "ST_CENTROID_AGG", + "languageDocumentation.documentationESQL.st_centroid_agg.markdown": " ### ST_CENTROID_AGG Calcule le centroïde spatial sur un champ avec un type de géométrie de point spatial. ``` FROM airports | STATS centroid=ST_CENTROID_AGG(location) ```", "languageDocumentation.documentationESQL.st_contains": "ST_CONTAINS", - "languageDocumentation.documentationESQL.st_contains.markdown": "\n\n ### ST_CONTAINS\n Renvoie si la première géométrie contient la deuxième géométrie.\n Il s'agit de l'inverse de la fonction `ST_WITHIN`.\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE(\"POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", + "languageDocumentation.documentationESQL.st_contains.markdown": " ### ST_CONTAINS Renvoie si la première géométrie contient la deuxième géométrie. Il s'agit de l'inverse de la fonction `ST_WITHIN`. ``` FROM airport_city_boundaries | WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE(\"POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))\")) | KEEP abbrev, airport, region, city, city_location ```", "languageDocumentation.documentationESQL.st_disjoint": "ST_DISJOINT", - "languageDocumentation.documentationESQL.st_disjoint.markdown": "\n\n ### ST_DISJOINT\n Renvoie si les deux géométries ou colonnes géométriques sont disjointes.\n Il s'agit de l'inverse de la fonction `ST_INTERSECTS`.\n En termes mathématiques : ST_Disjoint(A, B) ⇔ A ⋂ B = ∅\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE(\"POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", + "languageDocumentation.documentationESQL.st_disjoint.markdown": " ### ST_DISJOINT Renvoie si les deux géométries ou colonnes géométriques sont disjointes. Il s'agit de l'inverse de la fonction `ST_INTERSECTS`. En termes mathématiques : ST_Disjoint(A, B) ⇔ A ⋂ B = ∅ ``` FROM airport_city_boundaries | WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE(\"POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))\")) | KEEP abbrev, airport, region, city, city_location ```", "languageDocumentation.documentationESQL.st_distance": "ST_DISTANCE", - "languageDocumentation.documentationESQL.st_distance.markdown": "\n\n ### ST_DISTANCE\n Calcule la distance entre deux points.\n Pour les géométries cartésiennes, c’est la distance pythagoricienne dans les mêmes unités que les coordonnées d'origine.\n Pour les géométries géographiques, c’est la distance circulaire le long du grand cercle en mètres.\n\n ````\n Aéroports FROM\n | WHERE abbrev == \"CPH\"\n | EVAL distance = ST_DISTANCE(location, city_location)\n | KEEP abbrev, name, location, city_location, distance\n ````\n ", + "languageDocumentation.documentationESQL.st_distance.markdown": " ### ST_DISTANCE Calcule la distance entre deux points. Pour les géométries cartésiennes, c’est la distance pythagoricienne dans les mêmes unités que les coordonnées d'origine. Pour les géométries géographiques, c'est la distance circulaire le long du grand cercle en mètres. ``` FROM airports | WHERE abbrev == \"CPH\" | EVAL distance = ST_DISTANCE(location, city_location) | KEEP abbrev, name, location, city_location, distance ```", "languageDocumentation.documentationESQL.st_intersects": "ST_INTERSECTS", - "languageDocumentation.documentationESQL.st_intersects.markdown": "\n\n ### ST_INTERSECTS\n Renvoie `true` (vrai) si deux géométries se croisent.\n Elles se croisent si elles ont un point commun, y compris leurs points intérieurs\n (les points situés le long des lignes ou dans des polygones).\n Il s'agit de l'inverse de la fonction `ST_DISJOINT`.\n En termes mathématiques : ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅\n\n ````\n Aéroports FROM\n | WHERE ST_INTERSECTS(location, TO_GEOSHAPE(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\"))\n ````\n ", + "languageDocumentation.documentationESQL.st_intersects.markdown": " ### ST_INTERSECTS Renvoie `true` (vrai) si deux géométries se croisent. Elles se croisent si elles ont un point commun, y compris leurs points intérieurs (points le long de lignes ou à l'intérieur de polygones). Il s'agit de l'inverse de la fonction `ST_DISJOINT`. En termes mathématiques : ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅ ``` FROM airports | WHERE ST_INTERSECTS(location, TO_GEOSHAPE(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\")) ```", "languageDocumentation.documentationESQL.st_within": "ST_WITHIN", - "languageDocumentation.documentationESQL.st_within.markdown": "\n\n ### ST_WITHIN\n Renvoie si la première géométrie est à l'intérieur de la deuxième géométrie.\n Il s'agit de l'inverse de la fonction `ST_CONTAINS`.\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE(\"POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", + "languageDocumentation.documentationESQL.st_within.markdown": " ### ST_WITHIN Renvoie si la première géométrie est comprise dans la deuxième géométrie. Il s'agit de l'inverse de la fonction `ST_CONTAINS`. ``` FROM airport_city_boundaries | WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE(\"POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))\")) | KEEP abbrev, airport, region, city, city_location ```", "languageDocumentation.documentationESQL.st_x": "ST_X", - "languageDocumentation.documentationESQL.st_x.markdown": "\n\n ### ST_X\n Extrait la coordonnée `x` du point fourni.\n Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `longitude`.\n\n ````\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ````\n ", + "languageDocumentation.documentationESQL.st_x.markdown": " ### ST_X Extrait la coordonnée `x` du point fourni. Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `longitude`. ``` ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\") | EVAL x = ST_X(point), y = ST_Y(point) ```", "languageDocumentation.documentationESQL.st_y": "ST_Y", - "languageDocumentation.documentationESQL.st_y.markdown": "\n\n ### ST_Y\n Extrait la coordonnée `y` du point fourni.\n Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `latitude`.\n\n ````\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ````\n ", + "languageDocumentation.documentationESQL.st_y.markdown": " ### ST_Y Extrait la coordonnée `y` du point fourni. Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `latitude`. ``` ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\") | EVAL x = ST_X(point), y = ST_Y(point) ```", "languageDocumentation.documentationESQL.starts_with": "STARTS_WITH", - "languageDocumentation.documentationESQL.starts_with.markdown": "\n\n ### STARTS_WITH\n Renvoie un booléen qui indique si une chaîne de mot-clés débute par une autre chaîne.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_S = STARTS_WITH(last_name, \"B\")\n ````\n ", + "languageDocumentation.documentationESQL.starts_with.markdown": " ### STARTS_WITH Renvoie une valeur booléenne qui indique si une chaîne de mots-clés débute par une autre chaîne. ``` FROM employees | KEEP last_name | EVAL ln_S = STARTS_WITH(last_name, \"B\") ```", "languageDocumentation.documentationESQL.statsby": "STATS ... BY", - "languageDocumentation.documentationESQL.statsby.markdown": "### STATS ... BY\nUtilisez `STATS ... BY` pour regrouper les lignes en fonction d'une valeur commune et calculer une ou plusieurs valeurs agrégées sur les lignes regroupées.\n\n**Exemples** :\n\n````\nFROM employees\n| STATS count = COUNT(emp_no) BY languages\n| SORT languages\n````\n\nSi `BY` est omis, le tableau de sortie contient exactement une ligne avec les agrégations appliquées sur l'ensemble des données :\n\n````\nFROM employees\n| STATS avg_lang = AVG(languages)\n````\n\nIl est possible de calculer plusieurs valeurs :\n\n````\nFROM employees\n| STATS avg_lang = AVG(languages), max_lang = MAX(languages)\n````\n\nIl est également possible d'effectuer des regroupements en fonction de plusieurs valeurs (uniquement pour les champs longs et les champs de la famille de mots-clés) :\n\n````\nFROM employees\n| EVAL hired = DATE_FORMAT(hire_date, \"YYYY\")\n| STATS avg_salary = AVG(salary) BY hired, languages.long\n| EVAL avg_salary = ROUND(avg_salary)\n| SORT hired, languages.long\n````\n\nConsultez la rubrique **Fonctions d'agrégation** pour obtenir la liste des fonctions pouvant être utilisées avec `STATS ... BY`.\n\nLes fonctions d'agrégation et les expressions de regroupement acceptent toutes deux d'autres fonctions. Ceci est utile pour utiliser `STATS...BY` sur des colonnes à valeur multiple. Par exemple, pour calculer l'évolution moyenne du salaire, vous pouvez utiliser `MV_AVG` pour faire la moyenne des multiples valeurs par employé, et utiliser le résultat avec la fonction `AVG` :\n\n````\nFROM employees\n| STATS avg_salary_change = AVG(MV_AVG(salary_change))\n````\n\nLe regroupement par expression est par exemple le regroupement des employés en fonction de la première lettre de leur nom de famille :\n\n````\nFROM employees\n| STATS my_count = COUNT() BY LEFT(last_name, 1)\n| SORT \"LEFT(last_name, 1)\"\n````\n\nIl n'est pas obligatoire d'indiquer le nom de la colonne de sortie. S'il n'est pas spécifié, le nouveau nom de la colonne est égal à l'expression. La requête suivante renvoie une colonne appelée `AVG(salary)` :\n\n````\nFROM employees\n| STATS AVG(salary)\n````\n\nComme ce nom contient des caractères spéciaux, il doit être placé entre deux caractères (`) lorsqu'il est utilisé dans des commandes suivantes :\n\n````\nFROM employees\n| STATS AVG(salary)\n| EVAL avg_salary_rounded = ROUND(\"AVG(salary)\")\n````\n\n**Remarque** : `STATS` sans aucun groupe est beaucoup plus rapide que l'ajout d'un groupe.\n\n**Remarque** : Le regroupement sur une seule expression est actuellement beaucoup plus optimisé que le regroupement sur plusieurs expressions.\n ", + "languageDocumentation.documentationESQL.statsby.markdown": "### STATS ... BY Utilisez `STATS ... BY` pour regrouper les lignes en fonction d'une valeur commune et calculer une ou plusieurs valeurs agrégées sur les lignes regroupées. **Exemples** : ``` FROM employees | STATS count = COUNT(emp_no) BY languages | SORT languages ``` Si `BY` est omis, le tableau de sortie contient exactement une ligne avec les agrégations appliquées sur l'ensemble des données : ``` FROM employees | STATS avg_lang = AVG(languages) ``` Il est possible de calculer plusieurs valeurs : ``` FROM employees | STATS avg_lang = AVG(languages), max_lang = MAX(languages) ``` Il est également possible d'effectuer des regroupements en fonction de plusieurs valeurs (uniquement pour les champs longs et les champs de la famille de mots-clés) : ``` FROM employees | EVAL hired = DATE_FORMAT(hire_date, \"YYYY\") | STATS avg_salary = AVG(salary) BY hired, languages.long | EVAL avg_salary = ROUND(avg_salary) | SORT hired, languages.long ``` Consultez la rubrique **Fonctions d'agrégation** pour obtenir la liste des fonctions pouvant être utilisées avec `STATS ... BY`. Les fonctions d'agrégation et les expressions de regroupement acceptent toutes deux d'autres fonctions. Ceci est utile pour utiliser `STATS...BY` sur des colonnes à valeur multiple. Par exemple, pour calculer l'évolution moyenne du salaire, vous pouvez utiliser `MV_AVG` pour faire la moyenne des multiples valeurs par employé, et utiliser le résultat avec la fonction `AVG` : ``` FROM employees | STATS avg_salary_change = AVG(MV_AVG(salary_change)) ``` Le regroupement par expression est par exemple le regroupement des employés en fonction de la première lettre de leur nom de famille : ``` FROM employees | STATS my_count = COUNT() BY LEFT(last_name, 1) | SORT `LEFT(last_name, 1)` ``` Il n'est pas obligatoire d'indiquer le nom de la colonne de sortie. S'il n'est pas spécifié, le nouveau nom de la colonne est égal à l'expression. La requête suivante renvoie une colonne appelée `AVG(salary)` : ``` FROM employees | STATS AVG(salary) ``` Comme ce nom contient des caractères spéciaux, il doit être placé entre deux caractères (`) lorsqu'il est utilisé dans les commandes suivantes : ``` FROM employees | STATS AVG(salary) | EVAL avg_salary_rounded = ROUND(`AVG(salary)`) ``` **Remarque** : `STATS` sans aucun groupe est beaucoup plus rapide que l'ajout d'un groupe. **Remarque** : Le regroupement sur une seule expression est actuellement beaucoup plus optimisé que le regroupement sur plusieurs expressions.", "languageDocumentation.documentationESQL.stringOperators": "LIKE et RLIKE", - "languageDocumentation.documentationESQL.stringOperators.markdown": "### LIKE et RLIKE\nPour comparer des chaînes en utilisant des caractères génériques ou des expressions régulières, utilisez `LIKE` ou `RLIKE` :\n\nUtilisez `LIKE` pour faire correspondre des chaînes à l'aide de caractères génériques. Les caractères génériques suivants sont pris en charge :\n\n* `*` correspond à zéro caractère ou plus.\n* `?` correspond à un seul caractère.\n\n````\nFROM employees\n| WHERE first_name LIKE \"?b*\"\n| KEEP first_name, last_name\n````\n\nUtilisez `RLIKE` pour faire correspondre des chaînes à l'aide d'expressions régulières :\n\n````\nFROM employees\n| WHERE first_name RLIKE \".leja.*\"\n| KEEP first_name, last_name\n````\n ", + "languageDocumentation.documentationESQL.stringOperators.markdown": "### LIKE et RLIKE Pour comparer des chaînes en utilisant des caractères génériques ou des expressions régulières, utilisez `LIKE` ou `RLIKE` : Utilisez `LIKE` pour faire correspondre des chaînes à l'aide de caractères génériques. Les caractères génériques suivants sont pris en charge : * `*` correspond à zéro ou plusieurs caractères. * `?` correspond à un seul caractère. ``` FROM employees | WHERE first_name LIKE \"?b*\" | KEEP first_name, last_name ``` Utilisez `RLIKE` pour faire correspondre des chaînes à l'aide d'expressions régulières : ``` FROM employees | WHERE first_name RLIKE \".leja.*\" | KEEP first_name, last_name ```", "languageDocumentation.documentationESQL.substring": "SUBSTRING", - "languageDocumentation.documentationESQL.substring.markdown": "\n\n ### SUBSTRING\n Renvoie la sous-chaîne d'une chaîne, délimitée en fonction d'une position de départ et d'une longueur facultative\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_sub = SUBSTRING(last_name, 1, 3)\n ````\n ", + "languageDocumentation.documentationESQL.substring.markdown": " ### SUBSTRING Renvoie la sous-chaîne d'une chaîne, délimitée en fonction d'une position de départ et d'une longueur facultative. ``` FROM employees | KEEP last_name | EVAL ln_sub = SUBSTRING(last_name, 1, 3) ```", + "languageDocumentation.documentationESQL.sum": "SUM", + "languageDocumentation.documentationESQL.sum.markdown": " ### SUM La somme d'une expression numérique. ``` FROM employees | STATS SUM(languages) ```", "languageDocumentation.documentationESQL.tan": "TAN", - "languageDocumentation.documentationESQL.tan.markdown": "\n\n ### TAN\n Renvoie la fonction trigonométrique Tangente d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL tan=TAN(a)\n ````\n ", + "languageDocumentation.documentationESQL.tan.markdown": " ### TAN Renvoie la tangente d'un angle. ``` ROW a=1.8 | EVAL tan=TAN(a) ```", "languageDocumentation.documentationESQL.tanh": "TANH", - "languageDocumentation.documentationESQL.tanh.markdown": "\n\n ### TANH\n Renvoie la fonction hyperbolique Tangente d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL tanh=TANH(a)\n ````\n ", + "languageDocumentation.documentationESQL.tanh.markdown": " ### TANH Renvoie la tangente hyperbolique d'un nombre. ``` ROW a=1.8 | EVAL tanh=TANH(a) ```", "languageDocumentation.documentationESQL.tau": "TAU", - "languageDocumentation.documentationESQL.tau.markdown": "\n\n ### TAU\n Renvoie le rapport entre la circonférence et le rayon d'un cercle.\n\n ````\n ROW TAU()\n ````\n ", + "languageDocumentation.documentationESQL.tau.markdown": " ### TAU Renvoie le rapport entre la circonférence et le rayon d'un cercle. ``` ROW TAU() ```", "languageDocumentation.documentationESQL.to_base64": "TO_BASE64", - "languageDocumentation.documentationESQL.to_base64.markdown": "\n\n ### TO_BASE64\n Encode une chaîne en chaîne base64.\n\n ````\n row a = \"elastic\" \n | eval e = to_base64(a)\n ````\n ", + "languageDocumentation.documentationESQL.to_base64.markdown": " ### TO_BASE64 Encode une chaîne en chaîne base64. ``` row a = \"elastic\" | eval e = to_base64(a) ```", "languageDocumentation.documentationESQL.to_boolean": "TO_BOOLEAN", - "languageDocumentation.documentationESQL.to_boolean.markdown": "\n\n ### TO_BOOLEAN\n Convertit une valeur d'entrée en une valeur booléenne.\n Une chaîne de valeur *true* sera convertie, sans tenir compte de la casse, en une valeur booléenne *true*.\n Pour toute autre valeur, y compris une chaîne vide, la fonction renverra *false*.\n La valeur numérique *0* sera convertie en *false*, toute autre valeur sera convertie en *true*.\n\n ````\n ROW str = [\"true\", \"TRuE\", \"false\", \"\", \"yes\", \"1\"]\n | EVAL bool = TO_BOOLEAN(str)\n ````\n ", + "languageDocumentation.documentationESQL.to_boolean.markdown": " ### TO_BOOLEAN Convertit une valeur d'entrée en une valeur booléenne. Une chaîne de valeur *true* sera convertie, sans tenir compte de la casse, en une valeur booléenne *true*. Pour toute autre valeur, y compris une chaîne vide, la fonction renverra *false*. La valeur numérique *0* sera convertie en *false*, toute autre valeur sera convertie en *true*. ``` ROW str = [\"true\", \"TRuE\", \"false\", \"\", \"yes\", \"1\"] | EVAL bool = TO_BOOLEAN(str) ```", "languageDocumentation.documentationESQL.to_cartesianpoint": "TO_CARTESIANPOINT", - "languageDocumentation.documentationESQL.to_cartesianpoint.markdown": "\n\n ### TO_CARTESIANPOINT\n Convertit la valeur d'une entrée en une valeur `cartesian_point`.\n Une chaîne ne sera convertie que si elle respecte le format WKT Point.\n\n ````\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POINT(7580.93 2272.77)\"]\n | MV_EXPAND wkt\n | EVAL pt = TO_CARTESIANPOINT(wkt)\n ````\n ", + "languageDocumentation.documentationESQL.to_cartesianpoint.markdown": " ### TO_CARTESIANPOINT Convertit la valeur d'une entrée en une valeur `cartesian_point`. Une chaîne ne sera convertie que si elle respecte le format WKT Point. ``` ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POINT(7580.93 2272.77)\"] | MV_EXPAND wkt | EVAL pt = TO_CARTESIANPOINT(wkt) ```", "languageDocumentation.documentationESQL.to_cartesianshape": "TO_CARTESIANSHAPE", - "languageDocumentation.documentationESQL.to_cartesianshape.markdown": "\n\n ### TO_CARTESIANSHAPE\n Convertit une valeur d'entrée en une valeur `cartesian_shape`.\n Une chaîne ne sera convertie que si elle respecte le format WKT.\n\n ````\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))\"]\n | MV_EXPAND wkt\n | EVAL geom = TO_CARTESIANSHAPE(wkt)\n ````\n ", + "languageDocumentation.documentationESQL.to_cartesianshape.markdown": " ### TO_CARTESIANSHAPE Convertit une valeur d'entrée en une valeur `cartesian_shape`. Une chaîne ne sera convertie que si elle respecte le format WKT. ``` ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))\"] | MV_EXPAND wkt | EVAL geom = TO_CARTESIANSHAPE(wkt) ```", + "languageDocumentation.documentationESQL.to_date_nanos": "TO_DATE_NANOS", + "languageDocumentation.documentationESQL.to_date_nanos.markdown": " ### TO_DATE_NANOS Convertit une entrée en une valeur de date de résolution nanoseconde (ou date_nanos). Remarque : La plage de \"date nanos\" est comprise entre 1970-01-01T00:00:00.000000000Z et 2262-04-11T23:47:16.854775807Z. En outre, les nombres entiers ne peuvent pas être convertis en \"date nanos\", car la plage des nanosecondes en nombres entiers ne couvre qu'environ 2 secondes après l'heure.", + "languageDocumentation.documentationESQL.to_dateperiod": "TO_DATEPERIOD", + "languageDocumentation.documentationESQL.to_dateperiod.markdown": " ### TO_DATEPERIOD Convertit une valeur d'entrée en une valeur `date_period`. ``` row x = \"2024-01-01\"::datetime | eval y = x + \"3 DAYS\"::date_period, z = x - to_dateperiod(\"3 days\"); ```", "languageDocumentation.documentationESQL.to_datetime": "TO_DATETIME", - "languageDocumentation.documentationESQL.to_datetime.markdown": "\n\n ### TO_DATETIME\n Convertit une valeur d'entrée en une valeur de date.\n Une chaîne ne sera convertie que si elle respecte le format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`.\n Pour convertir des dates vers d'autres formats, utilisez `DATE_PARSE`.\n\n ````\n ROW string = [\"1953-09-02T00:00:00.000Z\", \"1964-06-02T00:00:00.000Z\", \"1964-06-02 00:00:00\"]\n | EVAL datetime = TO_DATETIME(string)\n ````\n ", + "languageDocumentation.documentationESQL.to_datetime.markdown": " ### TO_DATETIME Convertit une valeur d'entrée en une valeur de date. Une chaîne ne sera convertie que si elle respecte le format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`. Pour convertir des dates vers d'autres formats, utilisez `DATE_PARSE`. ``` ROW string = [\"1953-09-02T00:00:00.000Z\", \"1964-06-02T00:00:00.000Z\", \"1964-06-02 00:00:00\"] | EVAL datetime = TO_DATETIME(string) ``` Remarque : Notez que lors de la conversion de la résolution en nanosecondes à la résolution en millisecondes avec cette fonction, la date en nanosecondes est tronquée et non arrondie.", "languageDocumentation.documentationESQL.to_degrees": "TO_DEGREES", - "languageDocumentation.documentationESQL.to_degrees.markdown": "\n\n ### TO_DEGREES\n Convertit un nombre en radians en degrés.\n\n ````\n ROW rad = [1.57, 3.14, 4.71]\n | EVAL deg = TO_DEGREES(rad)\n ````\n ", + "languageDocumentation.documentationESQL.to_degrees.markdown": " ### TO_DEGREES Convertit un nombre en radians en degrés. ``` ROW rad = [1.57, 3.14, 4.71] | EVAL deg = TO_DEGREES(rad) ```", "languageDocumentation.documentationESQL.to_double": "TO_DOUBLE", - "languageDocumentation.documentationESQL.to_double.markdown": "\n\n ### TO_DOUBLE\n Convertit une valeur d'entrée en une valeur double. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix,\n convertie en double. Le booléen *true* sera converti en double *1.0*, et *false* en *0.0*.\n\n ````\n ROW str1 = \"5.20128E11\", str2 = \"foo\"\n | EVAL dbl = TO_DOUBLE(\"520128000000\"), dbl1 = TO_DOUBLE(str1), dbl2 = TO_DOUBLE(str2)\n ````\n ", + "languageDocumentation.documentationESQL.to_double.markdown": " ### TO_DOUBLE Convertit une valeur d'entrée en une valeur double. Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en double. Le booléen *true* sera converti en double *1.0*, et *false* en *0.0*. ``` ROW str1 = \"5.20128E11\", str2 = \"foo\" | EVAL dbl = TO_DOUBLE(\"520128000000\"), dbl1 = TO_DOUBLE(str1), dbl2 = TO_DOUBLE(str2) ```", "languageDocumentation.documentationESQL.to_geopoint": "TO_GEOPOINT", - "languageDocumentation.documentationESQL.to_geopoint.markdown": "\n\n ### TO_GEOPOINT\n Convertit une valeur d'entrée en une valeur `geo_point`.\n Une chaîne ne sera convertie que si elle respecte le format WKT Point.\n\n ````\n ROW wkt = \"POINT(42.97109630194 14.7552534413725)\"\n | EVAL pt = TO_GEOPOINT(wkt)\n ````\n ", + "languageDocumentation.documentationESQL.to_geopoint.markdown": " ### TO_GEOPOINT Convertit une valeur d'entrée en une valeur `geo_point`. Une chaîne ne sera convertie que si elle respecte le format WKT Point. ``` ROW wkt = \"POINT(42.97109630194 14.7552534413725)\" | EVAL pt = TO_GEOPOINT(wkt) ```", "languageDocumentation.documentationESQL.to_geoshape": "TO_GEOSHAPE", - "languageDocumentation.documentationESQL.to_geoshape.markdown": "\n\n ### TO_GEOSHAPE\n Convertit une valeur d'entrée en une valeur `geo_shape`.\n Une chaîne ne sera convertie que si elle respecte le format WKT.\n\n ````\n ROW wkt = \"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))\"\n | EVAL geom = TO_GEOSHAPE(wkt)\n ````\n ", + "languageDocumentation.documentationESQL.to_geoshape.markdown": " ### TO_GEOSHAPE Convertit une valeur d'entrée en une valeur `geo_shape`. Une chaîne ne sera convertie que si elle respecte le format WKT. ``` ROW wkt = \"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))\" | EVAL geom = TO_GEOSHAPE(wkt) ```", "languageDocumentation.documentationESQL.to_integer": "TO_INTEGER", - "languageDocumentation.documentationESQL.to_integer.markdown": "\n\n ### TO_INTEGER\n Convertit une valeur d'entrée en une valeur entière.\n Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes\n depuis l'heure Unix, convertie en entier.\n Le booléen *true* sera converti en entier *1*, et *false* en *0*.\n\n ````\n ROW long = [5013792, 2147483647, 501379200000]\n | EVAL int = TO_INTEGER(long)\n ````\n ", + "languageDocumentation.documentationESQL.to_integer.markdown": " ### TO_INTEGER Convertit une valeur d'entrée en une valeur entière. Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en nombre entier. Le booléen *true* sera converti en entier *1*, et *false* en *0*. ``` ROW long = [5013792, 2147483647, 501379200000] | EVAL int = TO_INTEGER(long) ```", "languageDocumentation.documentationESQL.to_ip": "TO_IP", - "languageDocumentation.documentationESQL.to_ip.markdown": "\n\n ### TO_IP\n Convertit une chaîne d'entrée en valeur IP.\n\n ````\n ROW str1 = \"1.1.1.1\", str2 = \"foo\"\n | EVAL ip1 = TO_IP(str1), ip2 = TO_IP(str2)\n | WHERE CIDR_MATCH(ip1, \"1.0.0.0/8\")\n ````\n ", + "languageDocumentation.documentationESQL.to_ip.markdown": " ### TO_IP Convertit une chaîne d'entrée en valeur IP. ``` ROW str1 = \"1.1.1.1\", str2 = \"foo\" | EVAL ip1 = TO_IP(str1), ip2 = TO_IP(str2) | WHERE CIDR_MATCH(ip1, \"1.0.0.0/8\") ```", "languageDocumentation.documentationESQL.to_long": "TO_LONG", - "languageDocumentation.documentationESQL.to_long.markdown": "\n\n ### TO_LONG\n Convertit une valeur d'entrée en une valeur longue. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue.\n Le booléen *true* sera converti en valeur longue *1*, et *false* en *0*.\n\n ````\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_LONG(str1), long2 = TO_LONG(str2), long3 = TO_LONG(str3)\n ````\n ", + "languageDocumentation.documentationESQL.to_long.markdown": " ### TO_LONG Convertit une valeur d'entrée en une valeur longue. Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue. Le booléen *true* sera converti en valeur longue *1*, et *false* en *0*. ``` ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\" | EVAL long1 = TO_LONG(str1), long2 = TO_LONG(str2), long3 = TO_LONG(str3) ```", "languageDocumentation.documentationESQL.to_lower": "TO_LOWER", - "languageDocumentation.documentationESQL.to_lower.markdown": "\n\n ### TO_LOWER\n Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en minuscules.\n\n ````\n ROW message = \"Some Text\"\n | EVAL message_lower = TO_LOWER(message)\n ````\n ", + "languageDocumentation.documentationESQL.to_lower.markdown": " ### TO_LOWER Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en minuscules. ``` ROW message = \"Some Text\" | EVAL message_lower = TO_LOWER(message) ```", "languageDocumentation.documentationESQL.to_radians": "TO_RADIANS", - "languageDocumentation.documentationESQL.to_radians.markdown": "\n\n ### TO_RADIANS\n Convertit un nombre en degrés en radians.\n\n ````\n ROW deg = [90.0, 180.0, 270.0]\n | EVAL rad = TO_RADIANS(deg)\n ````\n ", + "languageDocumentation.documentationESQL.to_radians.markdown": " ### TO_RADIANS Convertit un nombre en degrés en radians. ``` ROW deg = [90.0, 180.0, 270.0] | EVAL rad = TO_RADIANS(deg) ```", "languageDocumentation.documentationESQL.to_string": "TO_STRING", - "languageDocumentation.documentationESQL.to_string.markdown": "\n\n ### TO_STRING\n Convertit une valeur d'entrée en une chaîne.\n\n ````\n ROW a=10\n | EVAL j = TO_STRING(a)\n ````\n ", + "languageDocumentation.documentationESQL.to_string.markdown": " ### TO_STRING Convertit une valeur d'entrée en une chaîne. ``` ROW a=10 | EVAL j = TO_STRING(a) ```", + "languageDocumentation.documentationESQL.to_timeduration": "TO_TIMEDURATION", + "languageDocumentation.documentationESQL.to_timeduration.markdown": " ### TO_TIMEDURATION Convertit une valeur d'entrée en valeur `time_duration`. ``` row x = \"2024-01-01\"::datetime | eval y = x + \"3 hours\"::time_duration, z = x - to_timeduration(\"3 hours\"); ```", "languageDocumentation.documentationESQL.to_unsigned_long": "TO_UNSIGNED_LONG", - "languageDocumentation.documentationESQL.to_unsigned_long.markdown": "\n\n ### TO_UNSIGNED_LONG\n Convertit une valeur d'entrée en une valeur longue non signée. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue non signée.\n Le booléen *true* sera converti en valeur longue non signée *1*, et *false* en *0*.\n\n ````\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_UNSIGNED_LONG(str1), long2 = TO_ULONG(str2), long3 = TO_UL(str3)\n ````\n ", + "languageDocumentation.documentationESQL.to_unsigned_long.markdown": " ### TO_UNSIGNED_LONG Convertit une valeur d'entrée en une valeur longue non signée. Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue non signée. Le booléen *true* sera converti en valeur longue non signée *1*, et *false* en *0*. ``` ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\" | EVAL long1 = TO_UNSIGNED_LONG(str1), long2 = TO_ULONG(str2), long3 = TO_UL(str3) ```", "languageDocumentation.documentationESQL.to_upper": "TO_UPPER", - "languageDocumentation.documentationESQL.to_upper.markdown": "\n\n ### TO_UPPER\n Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en majuscules.\n\n ````\n ROW message = \"Some Text\"\n | EVAL message_upper = TO_UPPER(message)\n ````\n ", + "languageDocumentation.documentationESQL.to_upper.markdown": " ### TO_UPPER Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en majuscules. ``` ROW message = \"Some Text\" | EVAL message_upper = TO_UPPER(message) ```", "languageDocumentation.documentationESQL.to_version": "TO_VERSION", - "languageDocumentation.documentationESQL.to_version.markdown": "\n\n ### TO_VERSION\n Convertit une chaîne d'entrée en une valeur de version.\n\n ````\n ROW v = TO_VERSION(\"1.2.3\")\n ````\n ", + "languageDocumentation.documentationESQL.to_version.markdown": " ### TO_VERSION Convertit une chaîne d'entrée en une valeur de version. ``` ROW v = TO_VERSION(\"1.2.3\") ```", + "languageDocumentation.documentationESQL.top": "TOP", + "languageDocumentation.documentationESQL.top.markdown": " ### TOP Collecte les valeurs les plus élevées d'un champ. Inclut les valeurs répétées. ``` FROM employees | STATS top_salaries = TOP(salary, 3, \"desc\"), top_salary = MAX(salary) ```", "languageDocumentation.documentationESQL.trim": "TRIM", - "languageDocumentation.documentationESQL.trim.markdown": "\n\n ### TRIM\n Supprime les espaces de début et de fin d'une chaîne.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = TRIM(message)\n | EVAL color = TRIM(color)\n ````\n ", + "languageDocumentation.documentationESQL.trim.markdown": " ### TRIM Supprime les espaces de début et de fin d'une chaîne. ``` ROW message = \" some text \", color = \" red \" | EVAL message = TRIM(message) | EVAL color = TRIM(color) ```", + "languageDocumentation.documentationESQL.values": "VALEURS", + "languageDocumentation.documentationESQL.values.markdown": " ### VALUES Renvoie toutes les valeurs d'un groupe dans un champ multivalué. L'ordre des valeurs renvoyées n'est pas garanti. Si vous avez besoin que les valeurs renvoyées soient dans l'ordre, utilisez `esql-mv_sort`. ``` FROM employees | EVAL first_letter = SUBSTRING(first_name, 0, 1) | STATS first_name=MV_SORT(VALUES(first_name)) BY first_letter | SORT first_letter ```", + "languageDocumentation.documentationESQL.weighted_avg": "WEIGHTED_AVG", + "languageDocumentation.documentationESQL.weighted_avg.markdown": " ### WEIGHTED_AVG La moyenne pondérée d'une expression numérique. ``` FROM employees | STATS w_avg = WEIGHTED_AVG(salary, height) by languages | EVAL w_avg = ROUND(w_avg) | KEEP w_avg, languages | SORT languages ```", "languageDocumentation.documentationESQL.where": "WHERE", - "languageDocumentation.documentationESQL.where.markdown": "### WHERE\nUtilisez `WHERE` afin d'obtenir un tableau qui comprend toutes les lignes du tableau d'entrée pour lesquelles la condition fournie est évaluée à `true` :\n \n````\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n````\n\n#### Opérateurs\n\nPour obtenir un aperçu des opérateurs pris en charge, consultez la section **Opérateurs**.\n\n#### Fonctions\n`WHERE` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez la section **Fonctions**.\n ", + "languageDocumentation.documentationESQL.where.markdown": "### WHERE Utilisez `WHERE` afin d'obtenir un tableau qui comprend toutes les lignes du tableau d'entrée pour lesquelles la condition fournie est évaluée à `true` : ``` FROM employees | KEEP first_name, last_name, still_hired | WHERE still_hired == true ``` #### Opérateurs Pour obtenir un aperçu des opérateurs pris en charge, consultez la section **Opérateurs**. #### Fonctions `WHERE` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez la section **Fonctions**.", + "languageDocumentation.documentationFlyoutTitle": "Référence rapide ES|QL", "languageDocumentation.documentationLinkLabel": "Voir toute la documentation", + "languageDocumentation.esqlDocsLabel": "Sélectionnez ou recherchez des thèmes", + "languageDocumentation.esqlDocsLinkLabel": "Voir toute la documentation ES|QL", + "languageDocumentation.esqlSections.initialSectionLabel": "ES|QL", "languageDocumentation.header": "Référence de {language}", + "languageDocumentation.navigationAriaLabel": "Naviguer dans la documentation", + "languageDocumentation.navigationPlaceholder": "Commandes et fonctions", "languageDocumentation.searchPlaceholder": "Recherche", "languageDocumentation.tooltip": "Référence de {lang}", "lensFormulaDocs.avg": "Moyenne", "lensFormulaDocs.boolean": "booléen", "lensFormulaDocs.cardinality": "Décompte unique", - "lensFormulaDocs.cardinality.documentation.markdown": "\nCalcule le nombre de valeurs uniques d'un champ donné. Fonctionne pour les nombres, les chaînes, les dates et les valeurs booléennes.\n\nExemple : calculer le nombre de produits différents : \n`unique_count(product.name)`\n\nExemple : calculer le nombre de produits différents du groupe \"clothes\" : \n`unique_count(product.name, kql='product.group=clothes')`\n ", + "lensFormulaDocs.cardinality.documentation.markdown": "Calcule le nombre de valeurs uniques d'un champ donné. Fonctionne pour les nombres, les chaînes, les dates et les valeurs booléennes. Exemple : Calculer le nombre de produits différents : `unique_count(product.name)` Exemple : Calculer le nombre de produits différents du groupe \"vêtements\" : `unique_count(product.name, kql='product.group=clothes')`", "lensFormulaDocs.cardinality.signature": "champ : chaîne", "lensFormulaDocs.CommonFormulaDocumentation": "Les formules les plus courantes divisent deux valeurs pour produire un pourcentage. Pour obtenir un affichage correct, définissez \"Format de valeur\" sur \"pourcent\".", "lensFormulaDocs.count": "Décompte", - "lensFormulaDocs.count.documentation.markdown": "\nNombre total de documents. Lorsque vous fournissez un champ, le nombre total de valeurs de champ est compté. Lorsque vous utilisez la fonction de décompte pour les champs qui comportent plusieurs valeurs dans un même document, toutes les valeurs sont comptées.\n\n#### Exemples\n\nPour calculer le nombre total de documents, utilisez `count()`.\n\nPour calculer le nombre de produits, utilisez `count(products.id)`.\n\nPour calculer le nombre de documents qui correspondent à un filtre donné, utilisez `count(kql='price > 500')`.\n", + "lensFormulaDocs.count.documentation.markdown": "Nombre total de documents. Lorsque vous fournissez un champ, le nombre total de valeurs de champ est compté. Lorsque vous utilisez la fonction de décompte pour les champs qui comportent plusieurs valeurs dans un même document, toutes les valeurs sont comptées. #### Exemples Pour calculer le nombre total de documents, utilisez `count()`. Pour calculer le nombre de produits, utilisez `count(products.id)`. Pour calculer le nombre de documents qui correspondent à un filtre donné, utilisez `count(kql='price > 500')`.", "lensFormulaDocs.count.signature": "[champ : chaîne]", "lensFormulaDocs.counterRate": "Taux de compteur", - "lensFormulaDocs.counterRate.documentation.markdown": "\nCalcule le taux d'un compteur toujours croissant. Cette fonction renvoie uniquement des résultats utiles inhérents aux champs d'indicateurs de compteur qui contiennent une mesure quelconque à croissance régulière.\nSi la valeur diminue, elle est interprétée comme une mesure de réinitialisation de compteur. Pour obtenir des résultats plus précis, `counter_rate\" doit être calculé d’après la valeur `max` du champ.\n\nCe calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures.\nIl utilise l'intervalle en cours utilisé dans la formule.\n\nExemple : visualiser le taux d'octets reçus au fil du temps par un serveur Memcached : \n`counter_rate(max(memcached.stats.read.bytes))`\n ", + "lensFormulaDocs.counterRate.documentation.markdown": "Calcule le taux d'un compteur toujours croissant. Cette fonction renvoie uniquement des résultats utiles inhérents aux champs d'indicateurs de compteur qui contiennent une mesure quelconque à croissance régulière. Si la valeur diminue, elle est interprétée comme une mesure de réinitialisation de compteur. Pour obtenir des résultats plus précis, `counter_rate\" doit être calculé d’après la valeur `max` du champ. Ce calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures. Il utilise l'intervalle en cours utilisé dans la formule. Exemple : Visualiser le taux d'octets reçus au fil du temps par un serveur Memcached : `counter_rate(max(memcached.stats.read.bytes))`", "lensFormulaDocs.counterRate.signature": "indicateur : nombre", "lensFormulaDocs.cumulative_sum.signature": "indicateur : nombre", "lensFormulaDocs.cumulativeSum": "Somme cumulée", - "lensFormulaDocs.cumulativeSum.documentation.markdown": "\nCalcule la somme cumulée d'un indicateur au fil du temps, en ajoutant toutes les valeurs précédentes d'une série à chaque valeur. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates.\n\nCe calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures.\n\nExemple : visualiser les octets reçus cumulés au fil du temps : \n`cumulative_sum(sum(bytes))`\n", + "lensFormulaDocs.cumulativeSum.documentation.markdown": "Calcule la somme cumulée d'un indicateur au fil du temps, en ajoutant toutes les valeurs précédentes d'une série à chaque valeur. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates. Ce calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures. Exemple : Visualiser les octets reçus cumulés au fil du temps : `cumulative_sum(sum(bytes))`", "lensFormulaDocs.derivative": "Différences", - "lensFormulaDocs.differences.documentation.markdown": "\nCalcule la différence par rapport à la dernière valeur d'un indicateur au fil du temps. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates.\nLes données doivent être séquentielles pour les différences. Si vos données sont vides lorsque vous utilisez des différences, essayez d'augmenter l'intervalle de l'histogramme de dates.\n\nCe calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures.\n\nExemple : visualiser la modification des octets reçus au fil du temps : \n`differences(sum(bytes))`\n", + "lensFormulaDocs.differences.documentation.markdown": "Calcule la différence par rapport à la dernière valeur d'un indicateur au fil du temps. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates. Les données doivent être séquentielles pour les différences. Si vos données sont vides lorsque vous utilisez des différences, essayez d'augmenter l'intervalle de l'histogramme de dates. Ce calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures. Exemple : Visualiser l'évolution du nombre d'octets reçus au fil du temps : `differences(sum(bytes))`", "lensFormulaDocs.differences.signature": "indicateur : nombre", "lensFormulaDocs.documentation.columnCalculationSection": "Calculs de colonnes", "lensFormulaDocs.documentation.columnCalculationSectionDescription": "Ces fonctions sont exécutées pour chaque ligne, mais elles sont fournies avec la colonne entière comme contexte. Elles sont également appelées fonctions de fenêtre.", @@ -5686,92 +6300,92 @@ "lensFormulaDocs.documentation.elasticsearchSection": "Elasticsearch", "lensFormulaDocs.documentation.elasticsearchSectionDescription": "Ces fonctions seront exécutées sur les documents bruts pour chaque ligne du tableau résultant, en agrégeant tous les documents correspondant aux dimensions de répartition en une seule valeur.", "lensFormulaDocs.documentation.filterRatio": "Rapport de filtre", - "lensFormulaDocs.documentation.filterRatioDescription.markdown": "### Rapport de filtre :\n\nUtilisez `kql=''` pour filtrer un ensemble de documents et le comparer à d'autres documents du même regroupement.\nPar exemple, pour consulter l'évolution du taux d'erreur au fil du temps :\n\n````\ncount(kql='response.status_code > 400') / count()\n````\n ", - "lensFormulaDocs.documentation.markdown": "## Fonctionnement\n\nLes formules Lens permettent de réaliser des calculs à l'aide d'une combinaison d'agrégations Elasticsearch et\nde fonctions mathématiques. Trois types principaux de fonctions existent :\n\n* Indicateurs Elasticsearch, comme `sum(bytes)`\n* Fonctions de séries temporelles utilisant les indicateurs Elasticsearch en tant qu'entrée, comme `cumulative_sum()`\n* Fonctions mathématiques, comme `round()`\n\nVoici un exemple de formule qui les utilise tous :\n\n````\nround(100 * moving_average(\naverage(cpu.load.pct),\nwindow=10,\nkql='datacenter.name: east*'\n))\n````\n\nLes fonctions Elasticsearch utilisent un nom de champ, qui peut être entre guillemets. `sum(bytes)` est ainsi identique à\n`sum('bytes')`.\n\nCertaines fonctions utilisent des arguments nommés, comme `moving_average(count(), window=5)`.\n\nLes indicateurs Elasticsearch peuvent être filtrés à l’aide de la syntaxe KQL ou Lucene. Pour ajouter un filtre, utilisez le paramètre\nnommé `kql='field: value'` ou `lucene=''`. Utilisez toujours des guillemets simples pour écrire des requêtes KQL\nou Lucene. Si votre recherche contient un guillemet simple, utilisez une barre oblique inverse pour l’échapper, par exemple : `kql='Women\\'s\".\n\nLes fonctions mathématiques peuvent utiliser des arguments positionnels : par exemple, pow(count(), 3) est identique à count() * count() * count().\n\nUtilisez les opérateurs +, -, / et * pour réaliser des opérations de base.\n", + "lensFormulaDocs.documentation.filterRatioDescription.markdown": "### Rapport de filtre : Utilisez `kql=''` pour filtrer un ensemble de documents et le comparer à d'autres documents du même regroupement. Par exemple, pour consulter l'évolution du taux d'erreur au fil du temps : ``` count(kql='response.status_code > 400') / count() ```", + "lensFormulaDocs.documentation.markdown": "## Fonctionnement Les formules Lens permettent de réaliser des calculs à l'aide d'une combinaison d'agrégations Elasticsearch et de fonctions mathématiques. Il existe trois principaux types de fonctions : * Indicateurs Elasticsearch, comme `sum(bytes)` * Fonctions de séries temporelles utilisant les indicateurs Elasticsearch en tant qu'entrée, comme `cumulative_sum()` * Fonctions mathématiques, comme `round()` Voici un exemple de formule qui les utilise tous : ``` round(100 * moving_average( average(cpu.load.pct), window=10, kql='datacenter.name: east*' )) ``` Les fonctions Elasticsearch utilisent un nom de champ, qui peut être entre guillemets. `sum(bytes)` est ainsi identique à `sum('bytes')`. Certaines fonctions utilisent des arguments nommés, comme `moving_average(count(), window=5)`. Les indicateurs Elasticsearch peuvent être filtrés à l’aide de la syntaxe KQL ou Lucene. Pour ajouter un filtre, utilisez le paramètre `kql='field: value'` ou `lucene=''`. Utilisez toujours des guillemets simples pour écrire des requêtes KQL ou Lucene. Si votre recherche contient un guillemet simple, utilisez une barre oblique inverse pour l'échapper, par exemple : `kql='Women\\'s\". Les fonctions mathématiques peuvent utiliser des arguments positionnels : par exemple, pow(count(), 3) est identique à count() * count() * count(). Utilisez les opérateurs +, -, / et * pour réaliser des opérations de base.", "lensFormulaDocs.documentation.mathSection": "Mathématique", "lensFormulaDocs.documentation.mathSectionDescription": "Ces fonctions seront exécutées pour chaque ligne du tableau résultant en utilisant des valeurs uniques de la même ligne calculées à l'aide d'autres fonctions.", "lensFormulaDocs.documentation.percentOfTotal": "Pourcentage du total", - "lensFormulaDocs.documentation.percentOfTotalDescription.markdown": "### Pourcentage du total\n\nLes formules peuvent calculer `overall_sum` pour tous les regroupements,\nce qui permet de convertir chaque regroupement en un pourcentage du total :\n\n````\nsum(products.base_price) / overall_sum(sum(products.base_price))\n````\n ", + "lensFormulaDocs.documentation.percentOfTotalDescription.markdown": "### Pourcentage du total Les formules peuvent calculer `overall_sum` pour tous les regroupements, ce qui permet de convertir chaque regroupement en un pourcentage du total : ``` sum(products.base_price) / overall_sum(sum(products.base_price)) ```", "lensFormulaDocs.documentation.recentChange": "Modification récente", - "lensFormulaDocs.documentation.recentChangeDescription.markdown": "### Modification récente\n\nUtilisez `reducedTimeRange='30m'` pour ajouter un filtre supplémentaire sur la plage temporelle d'un indicateur aligné avec la fin d'une plage temporelle globale. Vous pouvez l'utiliser pour calculer le degré de modification récente d'une valeur.\n\n````\nmax(system.network.in.bytes, reducedTimeRange=\"30m\")\n- min(system.network.in.bytes, reducedTimeRange=\"30m\")\n````\n ", + "lensFormulaDocs.documentation.recentChangeDescription.markdown": "### Modification récente Utilisez `reducedTimeRange='30m'` pour ajouter un filtre supplémentaire sur la plage temporelle d'un indicateur aligné avec la fin d'une plage temporelle globale. Vous pouvez l'utiliser pour calculer le degré de modification récente d'une valeur. ``` max(system.network.in.bytes, reducedTimeRange=\"30m\") - min(system.network.in.bytes, reducedTimeRange=\"30m\") ```", "lensFormulaDocs.documentation.weekOverWeek": "Semaine après semaine", - "lensFormulaDocs.documentation.weekOverWeekDescription.markdown": "### Semaine après semaine :\n\nUtilisez `shift='1w'` pour obtenir la valeur de chaque regroupement\nde la semaine précédente. Le décalage ne doit pas être utilisé avec la fonction *Valeurs les plus élevées*.\n\n````\npercentile(system.network.in.bytes, percentile=99) /\npercentile(system.network.in.bytes, percentile=99, shift='1w')\n````\n ", + "lensFormulaDocs.documentation.weekOverWeekDescription.markdown": "### Semaine après semaine : Utilisez `shift='1w'` pour obtenir la valeur de chaque regroupement de la semaine précédente. Le décalage ne doit pas être utilisé avec la fonction *Valeurs les plus élevées*. ``` percentile(system.network.in.bytes, percentile=99) / percentile(system.network.in.bytes, percentile=99, shift='1w') ```", "lensFormulaDocs.frequentlyUsedHeading": "Formules courantes", "lensFormulaDocs.interval": "Intervalle de l'histogramme des dates", - "lensFormulaDocs.interval.help": "\nL’intervalle minimum spécifié pour l’histogramme de date, en millisecondes (ms).\n\nExemple : Normalisez l'indicateur de façon dynamique en fonction de la taille d'intervalle du compartiment : \n\"sum(bytes) / interval()\"\n", + "lensFormulaDocs.interval.help": "L’intervalle minimum spécifié pour l’histogramme de date, en millisecondes (ms). Exemple : Normalisez l'indicateur de façon dynamique en fonction de la taille d'intervalle du compartiment : `sum(bytes) / interval()`", "lensFormulaDocs.lastValue": "Dernière valeur", - "lensFormulaDocs.lastValue.documentation.markdown": "\nRenvoie la valeur d'un champ du dernier document, triée par le champ d'heure par défaut de la vue de données.\n\nCette fonction permet de récupérer le dernier état d'une entité.\n\nExemple : obtenir le statut actuel du serveur A : \n`last_value(server.status, kql='server.name=\"A\"')`\n", + "lensFormulaDocs.lastValue.documentation.markdown": "Renvoie la valeur d'un champ du dernier document, triée par le champ d'heure par défaut de la vue de données. Cette fonction permet de récupérer le dernier état d'une entité. Exemple : Obtenir le statut actuel du serveur A : `last_value(server.status, kql='server.name=\"A\"')`", "lensFormulaDocs.lastValue.signature": "champ : chaîne", "lensFormulaDocs.max": "Maximum", "lensFormulaDocs.median": "Médiane", - "lensFormulaDocs.metric.documentation.markdown": "\nRenvoie l'indicateur {metric} d'un champ. Cette fonction fonctionne uniquement pour les champs numériques.\n\nExemple : obtenir l'indicateur {metric} d'un prix : \n`{metric}(price)`\n\nExemple : obtenir l'indicateur {metric} d'un prix pour des commandes du Royaume-Uni : \n`{metric}(price, kql='location:UK')`\n ", + "lensFormulaDocs.metric.documentation.markdown": "Renvoie l'indicateur {metric} d'un champ. Cette fonction fonctionne uniquement pour les champs numériques. Exemple : Obtenir l'indicateur {metric} de prix : `{metric}(price)` Exemple : Obtenir l'indicateur {metric} de prix pour les commandes en provenance du Royaume-Uni : `{metric}(price, kql='location:UK')`", "lensFormulaDocs.metric.signature": "champ : chaîne", "lensFormulaDocs.min": "Minimum", "lensFormulaDocs.moving_average.signature": "indicateur : nombre, [window] : nombre", "lensFormulaDocs.movingAverage": "Moyenne mobile", - "lensFormulaDocs.movingAverage.documentation.markdown": "\nCalcule la moyenne mobile d'un indicateur au fil du temps, en prenant la moyenne des n dernières valeurs pour calculer la valeur actuelle. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates.\nLa valeur de fenêtre par défaut est {defaultValue}.\n\nCe calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures.\n\nPrend un paramètre nommé `window` qui spécifie le nombre de dernières valeurs à inclure dans le calcul de la moyenne de la valeur actuelle.\n\nExemple : lisser une ligne de mesures : \n`moving_average(sum(bytes), window=5)`\n", + "lensFormulaDocs.movingAverage.documentation.markdown": "Calcule la moyenne mobile d'un indicateur au fil du temps, en prenant la moyenne des n dernières valeurs pour calculer la valeur actuelle. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates. La valeur de fenêtre par défaut est {defaultValue}. Ce calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures. Prend un paramètre nommé `window` qui spécifie le nombre de dernières valeurs à inclure dans le calcul de la moyenne de la valeur actuelle. Exemple : Lisser une ligne de mesures : `moving_average(sum(bytes), window=5)`", "lensFormulaDocs.now": "Actuel", - "lensFormulaDocs.now.help": "\nLa durée actuelle passée dans Kibana exprimée en millisecondes (ms).\n\nExemple : Depuis combien de temps (en millisecondes) le serveur est-il en marche depuis son dernier redémarrage ? \n\"now() - last_value(start_time)\"\n", + "lensFormulaDocs.now.help": "La durée actuelle passée dans Kibana exprimée en millisecondes (ms). Exemple : Depuis combien de temps (en millisecondes) le serveur est-il en marche depuis son dernier redémarrage ? `now() - last_value(start_time)`", "lensFormulaDocs.number": "numéro", - "lensFormulaDocs.overall_average.documentation.markdown": "\nCalcule la moyenne d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle.\nD'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes.\n\nSi le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, `overall_average` calcule la moyenne pour toutes les dimensions, quelle que soit la fonction utilisée.\n\nExemple : écart par rapport à la moyenne : \n`sum(bytes) - overall_average(sum(bytes))`\n", - "lensFormulaDocs.overall_max.documentation.markdown": "\nCalcule la valeur maximale d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle.\nD'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes.\n\nSi le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, `overall_max` calcule la valeur maximale pour toutes les dimensions, quelle que soit la fonction utilisée.\n\nExemple : Pourcentage de plage : \n`(sum(bytes) - overall_min(sum(bytes))) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))`\n", + "lensFormulaDocs.overall_average.documentation.markdown": "Calcule la moyenne d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle. D'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes. Si le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, `overall_average` calcule la moyenne pour toutes les dimensions, quelle que soit la fonction utilisée. Exemple : Écart par rapport à la moyenne : `sum(bytes) - overall_average(sum(bytes))`", + "lensFormulaDocs.overall_max.documentation.markdown": "Calcule la valeur maximale d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle. D'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes. Si le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, `overall_max` calcule la valeur maximale pour toutes les dimensions, quelle que soit la fonction utilisée. Exemple : Pourcentage de plage : `(sum(bytes) - overall_min(sum(bytes)) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))`", "lensFormulaDocs.overall_metric": "indicateur : nombre", - "lensFormulaDocs.overall_min.documentation.markdown": "\nCalcule la valeur minimale d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle.\nD'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes.\n\nSi le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, `overall_min` calcule la valeur minimale pour toutes les dimensions, quelle que soit la fonction utilisée.\n\nExemple : Pourcentage de plage : \n`(sum(bytes) - overall_min(sum(bytes)) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))`\n", - "lensFormulaDocs.overall_sum.documentation.markdown": "\nCalcule la somme d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle.\nD'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes.\n\nSi le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, `overall_sum` calcule la somme pour toutes les dimensions, quelle que soit la fonction utilisée.\n\nExemple : Pourcentage total : \n`sum(bytes) / overall_sum(sum(bytes))`\n", + "lensFormulaDocs.overall_min.documentation.markdown": "Calcule la valeur minimale d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle. D'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes. Si le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, `overall_min` calcule la valeur minimale pour toutes les dimensions, quelle que soit la fonction utilisée. Exemple : Pourcentage de plage : `(sum(bytes) - overall_min(sum(bytes)) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))`", + "lensFormulaDocs.overall_sum.documentation.markdown": "Calcule la somme d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle. D'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes. Si le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, `overall_sum` calcule la somme pour toutes les dimensions, quelle que soit la fonction utilisée. Exemple : Pourcentage du total : `sum(bytes) / overall_sum(sum(bytes))`", "lensFormulaDocs.overallAverage": "Moyenne globale", "lensFormulaDocs.overallMax": "Max général", "lensFormulaDocs.overallMin": "Min général", "lensFormulaDocs.overallSum": "Somme générale", "lensFormulaDocs.percentile": "Centile", - "lensFormulaDocs.percentile.documentation.markdown": "\nRenvoie le centile spécifié des valeurs d'un champ. Il s'agit de la valeur de n pour cent des valeurs présentes dans les documents.\n\nExemple : obtenir le nombre d'octets supérieurs à 95 % des valeurs : \n`percentile(bytes, percentile=95)`\n", + "lensFormulaDocs.percentile.documentation.markdown": "Renvoie le centile spécifié des valeurs d'un champ. Il s'agit de la valeur de n pour cent des valeurs présentes dans les documents. Exemple : Obtenir le nombre d'octets supérieurs à 95 % des valeurs : `percentile(bytes, percentile=95)`", "lensFormulaDocs.percentile.signature": "champ : chaîne, [percentile] : nombre", "lensFormulaDocs.percentileRank": "Rang centile", - "lensFormulaDocs.percentileRanks.documentation.markdown": "\nRenvoie le pourcentage de valeurs qui sont en dessous d'une certaine valeur. Par exemple, si une valeur est supérieure à 95 % des valeurs observées, elle est placée au 95e rang centile.\n\nExemple : Obtenir le pourcentage de valeurs qui sont en dessous de 100 : \n`percentile_rank(bytes, value=100)`\n", + "lensFormulaDocs.percentileRanks.documentation.markdown": "Renvoie le pourcentage de valeurs qui sont en dessous d'une certaine valeur. Par exemple, si une valeur est supérieure à 95 % des valeurs observées, elle est placée au 95e rang centile. Exemple : Obtenir le pourcentage de valeurs qui sont en dessous de 100 : `percentile_rank(bytes, value=100)`", "lensFormulaDocs.percentileRanks.signature": "champ : chaîne, [valeur] : nombre", "lensFormulaDocs.standardDeviation": "Écart-type", - "lensFormulaDocs.standardDeviation.documentation.markdown": "\nRenvoie la taille de la variation ou de la dispersion du champ. Cette fonction ne s’applique qu’aux champs numériques.\n\n#### Exemples\n\nPour obtenir l'écart-type d'un prix, utilisez `standard_deviation(price)`.\n\nPour obtenir la variance du prix des commandes passées au Royaume-Uni, utilisez `square(standard_deviation(price, kql='location:UK'))`.\n", + "lensFormulaDocs.standardDeviation.documentation.markdown": "Renvoie la taille de la variation ou de la dispersion du champ. Cette fonction ne s'applique qu'aux champs numériques. #### Exemples Pour obtenir l'écart-type d'un prix, utilisez `standard_deviation(price)`. Pour obtenir la variance du prix des commandes passées au Royaume-Uni, utilisez `square(standard_deviation(price, kql='location:UK'))`.", "lensFormulaDocs.string": "chaîne", "lensFormulaDocs.sum": "Somme", "lensFormulaDocs.time_range": "Plage temporelle", "lensFormulaDocs.time_scale": "indicateur : nombre, unité : s|m|h|d|w|M|y", - "lensFormulaDocs.time_scale.documentation.markdown": "\nCette fonction avancée est utile pour normaliser les comptes et les sommes sur un intervalle de temps spécifique. Elle permet l'intégration avec les indicateurs qui sont stockés déjà normalisés sur un intervalle de temps spécifique.\n\nVous pouvez faire appel à cette fonction uniquement si une fonction d'histogramme des dates est utilisée dans le graphique actuel.\n\nExemple : Un rapport comparant un indicateur déjà normalisé à un autre indicateur devant être normalisé. \n`normalize_by_unit(counter_rate(max(system.diskio.write.bytes)), unit='s') / last_value(apache.status.bytes_per_second)`\n", - "lensFormulaDocs.timeRange.help": "\nL'intervalle de temps spécifié, en millisecondes (ms).\n\nExemple : Quelle est la durée de la plage temporelle actuelle en (ms) ?\n`time_range()`\n\nExemple : Une moyenne statique par minute calculée avec l'intervalle de temps actuel :\n`(sum(bytes) / time_range()) * 1000 * 60`\n", + "lensFormulaDocs.time_scale.documentation.markdown": "Cette fonction avancée est utile pour normaliser les comptes et les sommes sur un intervalle de temps spécifique. Elle permet l'intégration avec les indicateurs qui sont stockés déjà normalisés sur un intervalle de temps spécifique. Vous pouvez faire appel à cette fonction uniquement si une fonction d'histogramme des dates est utilisée dans le graphique actuel. Exemple : Un rapport comparant un indicateur déjà normalisé à un autre indicateur devant être normalisé. `normalize_by_unit(counter_rate(max(system.diskio.write.bytes)), unit='s') / last_value(apache.status.bytes_per_second)`", + "lensFormulaDocs.timeRange.help": "L'intervalle de temps spécifié, en millisecondes (ms). Exemple : Quelle est la durée de la plage temporelle actuelle en (ms) ? `time_range()` Exemple : Une moyenne statique par minute calculée avec l'intervalle de temps actuel : `(sum(bytes) / time_range()) * 1000 * 60`", "lensFormulaDocs.timeScale": "Normaliser par unité", - "lensFormulaDocs.tinymath.absFunction.markdown": "\nCalcule une valeur absolue. Une valeur négative est multipliée par -1, une valeur positive reste identique.\n\nExemple : calculer la distance moyenne par rapport au niveau de la mer `abs(average(altitude))`\n ", - "lensFormulaDocs.tinymath.addFunction.markdown": "\nAjoute jusqu'à deux nombres.\nFonctionne également avec le symbole `+`.\n\nExemple : calculer la somme de deux champs\n\n`sum(price) + sum(tax)`\n\nExemple : compenser le compte par une valeur statique\n\n`add(count(), 5)`\n ", + "lensFormulaDocs.tinymath.absFunction.markdown": "Calcule une valeur absolue. Une valeur négative est multipliée par -1, une valeur positive reste identique. Exemple : calculer la distance moyenne par rapport au niveau de la mer `abs(average(altitude))`", + "lensFormulaDocs.tinymath.addFunction.markdown": "Ajoute jusqu'à deux nombres. Fonctionne également avec le symbole `+`. Exemple : Calculer la somme de deux champs `sum(price) + sum(tax)` Exemple : Compenser le compte par une valeur statique `add(count(), 5)`", "lensFormulaDocs.tinymath.base": "base", - "lensFormulaDocs.tinymath.cbrtFunction.markdown": "\nÉtablit la racine carrée de la valeur.\n\nExemple : calculer la longueur du côté à partir du volume\n`cbrt(last_value(volume))`\n ", - "lensFormulaDocs.tinymath.ceilFunction.markdown": "\nArrondit le plafond de la valeur au chiffre supérieur.\n\nExemple : arrondir le prix au dollar supérieur\n`ceil(sum(price))`\n ", - "lensFormulaDocs.tinymath.clampFunction.markdown": "\nÉtablit une limite minimale et maximale pour la valeur.\n\nExemple : s'assurer de repérer les valeurs aberrantes\n````\nclamp(\n average(bytes),\n percentile(bytes, percentile=5),\n percentile(bytes, percentile=95)\n)\n````\n", + "lensFormulaDocs.tinymath.cbrtFunction.markdown": "Établit la racine carrée de la valeur. Exemple : Calculer la longueur du côté à partir du volume `cbrt(last_value(volume))`", + "lensFormulaDocs.tinymath.ceilFunction.markdown": "Arrondit le plafond de la valeur au chiffre supérieur. Exemple : Arrondir le prix au dollar supérieur `ceil(sum(price))`", + "lensFormulaDocs.tinymath.clampFunction.markdown": "Établit une limite minimale et maximale pour la valeur. Exemple : S'assurer de repérer les valeurs aberrantes ``` clamp( average(bytes), percentile(bytes, percentile=5), percentile(bytes, percentile=95) ) ```", "lensFormulaDocs.tinymath.condition": "condition", - "lensFormulaDocs.tinymath.cubeFunction.markdown": "\nCalcule le cube d'un nombre.\n\nExemple : calculer le volume à partir de la longueur du côté\n`cube(last_value(length))`\n ", + "lensFormulaDocs.tinymath.cubeFunction.markdown": "Calcule le cube d'un nombre. Exemple : Calculer le volume à partir de la longueur du côté `cube(last_value(length))`", "lensFormulaDocs.tinymath.decimals": "décimales", - "lensFormulaDocs.tinymath.defaultFunction.markdown": "\nRenvoie une valeur numérique par défaut lorsque la valeur est nulle.\n\nExemple : Renvoie -1 lorsqu'un champ ne contient aucune donnée.\n`defaults(average(bytes), -1)`\n", + "lensFormulaDocs.tinymath.defaultFunction.markdown": "Renvoie une valeur numérique par défaut lorsque la valeur est nulle. Exemple : Renvoie -1 si un champ n'a pas de données `defaults(average(bytes), -1)`", "lensFormulaDocs.tinymath.defaultValue": "par défaut", - "lensFormulaDocs.tinymath.divideFunction.markdown": "\nDivise le premier nombre par le deuxième.\nFonctionne également avec le symbole `/`.\n\nExemple : calculer la marge bénéficiaire\n`sum(profit) / sum(revenue)`\n\nExemple : `divide(sum(bytes), 2)`\n ", - "lensFormulaDocs.tinymath.eqFunction.markdown": "\nEffectue une comparaison d'égalité entre deux valeurs.\nÀ utiliser en tant que condition pour la fonction de comparaison `ifelse`.\nFonctionne également avec le symbole `==`.\n\nExemple : Renvoie \"true\" si la moyenne d'octets est égale à la quantité de mémoire moyenne.\n`average(bytes) == average(memory)`\n\nExemple : `eq(sum(bytes), 1000000)`\n ", - "lensFormulaDocs.tinymath.expFunction.markdown": "\nÉlève *e* à la puissance n.\n\nExemple : calculer la fonction exponentielle naturelle\n\n`exp(last_value(duration))`\n ", - "lensFormulaDocs.tinymath.fixFunction.markdown": "\nPour les valeurs positives, part du bas. Pour les valeurs négatives, part du haut.\n\nExemple : arrondir à zéro\n`fix(sum(profit))`\n ", - "lensFormulaDocs.tinymath.floorFunction.markdown": "\nArrondit à la valeur entière inférieure la plus proche.\n\nExemple : arrondir un prix au chiffre inférieur\n`floor(sum(price))`\n ", - "lensFormulaDocs.tinymath.gteFunction.markdown": "\nEffectue une comparaison de supériorité entre deux valeurs.\nÀ utiliser en tant que condition pour la fonction de comparaison `ifelse`.\nFonctionne également avec le symbole `>=`.\n\nExemple : Renvoie \"true\" si la moyenne d'octets est supérieure ou égale à la quantité moyenne de mémoire.\n`average(bytes) >= average(memory)`\n\nExemple : `gte(average(bytes), 1000)`\n ", - "lensFormulaDocs.tinymath.gtFunction.markdown": "\nEffectue une comparaison de supériorité entre deux valeurs.\nÀ utiliser en tant que condition pour la fonction de comparaison `ifelse`.\nFonctionne également avec le symbole `>`.\n\nExemple : Renvoie \"true\" si la moyenne d'octets est supérieure à la quantité moyenne de mémoire.\n`average(bytes) > average(memory)`\n\nExemple : `gt(average(bytes), 1000)`\n ", - "lensFormulaDocs.tinymath.ifElseFunction.markdown": "\nRenvoie une valeur selon si l'élément de condition est \"true\" ou \"false\".\n\nExemple : Revenus moyens par client, mais dans certains cas, l'ID du client n'est pas fourni, et le client est alors compté comme client supplémentaire.\n`sum(total)/(unique_count(customer_id) + ifelse( count() > count(kql='customer_id:*'), 1, 0))`\n ", + "lensFormulaDocs.tinymath.divideFunction.markdown": "Divise le premier nombre par le deuxième. Fonctionne également avec le symbole `/`. Exemple : Calculer la marge bénéficiaire `sum(profit) / sum(revenue)` Exemple : `divide(sum(bytes), 2)`", + "lensFormulaDocs.tinymath.eqFunction.markdown": "Effectue une comparaison d'égalité entre deux valeurs. À utiliser en tant que condition pour la fonction de comparaison `ifelse`. Fonctionne également avec le symbole `==`. Exemple : Renvoie \"true\" si la moyenne d'octets est égale à la quantité de mémoire moyenne `average(bytes) == average(memory)` Exemple : `eq(sum(bytes), 1000000)`", + "lensFormulaDocs.tinymath.expFunction.markdown": "Élève *e* à la puissance n. Exemple : Calculer la fonction exponentielle naturelle `exp(last_value(duration))`", + "lensFormulaDocs.tinymath.fixFunction.markdown": "Pour les valeurs positives, part du bas. Pour les valeurs négatives, part du haut. Exemple : Arrondir vers zéro `fix(sum(profit))`", + "lensFormulaDocs.tinymath.floorFunction.markdown": "Arrondit à la valeur entière inférieure la plus proche. Exemple : Arrondir un prix à la baisse `floor(sum(price))`", + "lensFormulaDocs.tinymath.gteFunction.markdown": "Effectue une comparaison de supériorité entre deux valeurs. À utiliser en tant que condition pour la fonction de comparaison `ifelse`. Fonctionne également avec le symbole `>=`. Exemple : Renvoie \"true\" si la moyenne d'octets est supérieure ou égale à la quantité de mémoire moyenne `average(bytes) >= average(memory)` Exemple : `gte(average(bytes), 1000)`", + "lensFormulaDocs.tinymath.gtFunction.markdown": "Effectue une comparaison de supériorité entre deux valeurs. À utiliser en tant que condition pour la fonction de comparaison `ifelse`. Fonctionne également avec le symbole `>`. Exemple : Renvoie \"true\" si la moyenne d'octets est supérieure à la quantité de mémoire moyenne `average(bytes) > average(memory)` Exemple : `gt(average(bytes), 1000)`", + "lensFormulaDocs.tinymath.ifElseFunction.markdown": "Renvoie une valeur selon si l'élément de condition est \"true\" ou \"false\". Exemple : Revenus moyens par client, mais dans certains cas, l'ID du client n'est pas fourni, et le client est alors compté comme client supplémentaire `sum(total)/(unique_count(customer_id) + ifelse( count() > count(kql='customer_id:*'), 1, 0))`", "lensFormulaDocs.tinymath.left": "gauche", - "lensFormulaDocs.tinymath.logFunction.markdown": "\nÉtablit un logarithme avec base optionnelle. La base naturelle *e* est utilisée par défaut.\n\nExemple : calculer le nombre de bits nécessaire au stockage de valeurs\n````\nlog(sum(bytes))\nlog(sum(bytes), 2)\n````\n ", - "lensFormulaDocs.tinymath.lteFunction.markdown": "\nEffectue une comparaison d'infériorité ou de supériorité entre deux valeurs.\nÀ utiliser en tant que condition pour la fonction de comparaison `ifelse`.\nFonctionne également avec le symbole `<=`.\n\nExemple : Renvoie \"true\" si la moyenne d'octets est inférieure ou égale à la quantité moyenne de mémoire.\n`average(bytes) <= average(memory)`\n\nExemple : `lte(average(bytes), 1000)`\n ", - "lensFormulaDocs.tinymath.ltFunction.markdown": "\nEffectue une comparaison d'infériorité entre deux valeurs.\nÀ utiliser en tant que condition pour la fonction de comparaison `ifelse`.\nFonctionne également avec le symbole `<`.\n\nExemple : Renvoie \"true\" si la moyenne d'octets est inférieure à la quantité moyenne de mémoire.\n`average(bytes) <= average(memory)`\n\nExemple : `lt(average(bytes), 1000)`\n ", + "lensFormulaDocs.tinymath.logFunction.markdown": "Établit un logarithme avec base optionnelle. La base naturelle *e* est utilisée par défaut. Exemple : Calculer le nombre de bits nécessaire au stockage de valeurs ``` log(sum(bytes)) log(sum(bytes), 2) ```", + "lensFormulaDocs.tinymath.lteFunction.markdown": "Effectue une comparaison d'infériorité ou de supériorité entre deux valeurs. À utiliser en tant que condition pour la fonction de comparaison `ifelse`. Fonctionne également avec le symbole `<=`. Exemple : Renvoie \"true\" si la moyenne d'octets est inférieure ou égale à la quantité de mémoire moyenne `average(bytes) <= average(memory)` Exemple : `lte(average(bytes), 1000)`", + "lensFormulaDocs.tinymath.ltFunction.markdown": "Effectue une comparaison d'infériorité entre deux valeurs. À utiliser en tant que condition pour la fonction de comparaison `ifelse`. Fonctionne également avec le symbole `<`. Exemple : Renvoie \"true\" si la moyenne d'octets est inférieure à la quantité de mémoire moyenne `average(bytes) <= average(memory)` Exemple : `lt(average(bytes), 1000)`", "lensFormulaDocs.tinymath.max": "max", - "lensFormulaDocs.tinymath.maxFunction.markdown": "\nTrouve la valeur maximale entre deux nombres.\n\nExemple : Trouver le maximum entre deux moyennes de champs\n`pick_max(average(bytes), average(memory))`\n ", + "lensFormulaDocs.tinymath.maxFunction.markdown": "Trouve la valeur maximale entre deux nombres. Exemple : Trouver la valeur maximale entre deux moyennes de champs `pick_max(average(bytes), average(memory))`", "lensFormulaDocs.tinymath.min": "min", - "lensFormulaDocs.tinymath.minFunction.markdown": "\nTrouve la valeur minimale entre deux nombres.\n\nExemple : Trouver le minimum entre deux moyennes de champs\n`pick_min(average(bytes), average(memory))`\n ", - "lensFormulaDocs.tinymath.modFunction.markdown": "\nÉtablit le reste après division de la fonction par un nombre.\n\nExemple : calculer les trois derniers chiffres d'une valeur\n`mod(sum(price), 1000)`\n ", - "lensFormulaDocs.tinymath.multiplyFunction.markdown": "\nMultiplie deux nombres.\nFonctionne également avec le symbole `*`.\n\nExemple : calculer le prix après application du taux d'imposition courant\n`sum(bytes) * last_value(tax_rate)`\n\nExemple : calculer le prix après application du taux d'imposition constant\n`multiply(sum(price), 1.2)`\n ", - "lensFormulaDocs.tinymath.powFunction.markdown": "\nÉlève la valeur à une puissance spécifique. Le deuxième argument est obligatoire.\n\nExemple : calculer le volume en fonction de la longueur du côté\n`pow(last_value(length), 3)`\n ", + "lensFormulaDocs.tinymath.minFunction.markdown": "Trouve la valeur minimale entre deux nombres. Exemple : Trouver la valeur minimale entre deux moyennes de champs `pick_min(average(bytes), average(memory))`", + "lensFormulaDocs.tinymath.modFunction.markdown": "Établit le reste après division de la fonction par un nombre. Exemple : Calculer les trois derniers chiffres d'une valeur `mod(sum(price), 1000)`", + "lensFormulaDocs.tinymath.multiplyFunction.markdown": "Multiplie deux nombres. Fonctionne également avec le symbole `*`. Exemple : Calculer le prix après application du taux d'imposition actuel `sum(bytes) * last_value(tax_rate)` Exemple : Calculer le prix après application du taux d'imposition constant `multiply(sum(price), 1.2)`", + "lensFormulaDocs.tinymath.powFunction.markdown": "Élève la valeur à une puissance spécifique. Le deuxième argument est obligatoire. Exemple : Calculer le volume en fonction de la longueur du côté `pow(last_value(length), 3)`", "lensFormulaDocs.tinymath.right": "droite", - "lensFormulaDocs.tinymath.roundFunction.markdown": "\nArrondit à un nombre donné de décimales, 0 étant la valeur par défaut.\n\nExemples : arrondir au centième\n````\nround(sum(bytes))\nround(sum(bytes), 2)\n````\n ", - "lensFormulaDocs.tinymath.sqrtFunction.markdown": "\nÉtablit la racine carrée d'une valeur positive uniquement.\n\nExemple : calculer la longueur du côté en fonction de la surface\n`sqrt(last_value(area))`\n ", - "lensFormulaDocs.tinymath.squareFunction.markdown": "\nÉlève la valeur à la puissance 2.\n\nExemple : calculer l’aire en fonction de la longueur du côté\n`square(last_value(length))`\n ", - "lensFormulaDocs.tinymath.subtractFunction.markdown": "\nSoustrait le premier nombre du deuxième.\nFonctionne également avec le symbole `-`.\n\nExemple : calculer la plage d'un champ\n`subtract(max(bytes), min(bytes))`\n ", + "lensFormulaDocs.tinymath.roundFunction.markdown": "Arrondit à un nombre donné de décimales, 0 étant la valeur par défaut. Exemple : Arrondir au centième ``` round(sum(bytes)) round(sum(bytes), 2) ```", + "lensFormulaDocs.tinymath.sqrtFunction.markdown": "Établit la racine carrée d'une valeur positive uniquement. Exemple : Calculer la longueur d'un côté en fonction de la surface `sqrt(last_value(area))`", + "lensFormulaDocs.tinymath.squareFunction.markdown": "Élève la valeur à la puissance 2. Exemple : Calculer la surface en fonction de la longueur du côté `square(last_value(length))`", + "lensFormulaDocs.tinymath.subtractFunction.markdown": "Soustrait le premier nombre du deuxième. Fonctionne également avec le symbole `-`. Exemple : Calculer la plage d'un champ `subtract(max(bytes), min(bytes))`", "lensFormulaDocs.tinymath.value": "valeur", "links.contentManagement.saveModalTitle": "Enregistrer le panneau {contentId} dans la bibliothèque", "links.dashboardLink.description": "Accéder au tableau de bord", @@ -5783,14 +6397,16 @@ "links.dashboardLink.editor.loadingDashboardLabel": "Chargement...", "links.dashboardLink.type": "Lien du tableau de bord", "links.description": "Utiliser des liens pour accéder aux tableaux de bord et aux sites web couramment utilisés.", + "links.displayName": "liens", "links.editor.addButtonLabel": "Ajouter un lien", "links.editor.cancelButtonLabel": "Fermer", "links.editor.deleteLinkTitle": "Supprimer le lien {label}", "links.editor.editLinkTitle.hasLabel": "Modifier le lien {label}", "links.editor.horizontalLayout": "Horizontal", - "links.editor.unableToSaveToastTitle": "Erreur lors de l'enregistrement du Panneau de liens", + "links.editor.unableToSaveToastTitle": "Erreur lors de l'enregistrement du panneau de liens", "links.editor.updateButtonLabel": "Mettre à jour le lien", "links.editor.verticalLayout": "Vertical", + "links.embeddable.unsupportedLinkTypeError": "Type de lien non pris en charge", "links.externalLink.description": "Accéder à l'URL", "links.externalLink.displayName": "URL", "links.externalLink.editor.disallowedUrlError": "Cette URL n'est pas autorisée par votre administrateur. Reportez-vous à la configuration \"externalUrl.policy\".", @@ -5831,7 +6447,14 @@ "managedContentBadge.text": "Géré", "management.breadcrumb": "Gestion de la Suite", "management.landing.header": "Bienvenue dans Gestion de la Suite {version}", + "management.landing.solution.header": "Gestion de la Suite {version}", + "management.landing.solution.subhead": "Gérez vos {indicesLink}, {dataViewsLink}, {ingestPipelinesLink}, {usersLink}, et plus encore.", + "management.landing.solution.viewAllPagesButton": "Afficher toutes les pages", "management.landing.subhead": "Gérez vos index, vues de données, objets enregistrés, paramètres Kibana et plus encore.", + "management.landing.subhead.dataViewsLink": "Les vues de données sont introuvables", + "management.landing.subhead.indicesLink": "index système non migrés", + "management.landing.subhead.ingestPipelinesLink": "pipelines d'ingestion", + "management.landing.subhead.usersLink": "utilisateurs", "management.landing.text": "Vous trouverez une liste complète des applications dans le menu de gauche.", "management.landing.withCardNavigation.accessTitle": "Accès", "management.landing.withCardNavigation.alertsTitle": "Alertes et informations exploitables", @@ -5840,6 +6463,7 @@ "management.landing.withCardNavigation.contentTitle": "Contenu", "management.landing.withCardNavigation.dataQualityDescription": "Recherchez et gérez les problèmes de qualité dans vos données de logs.", "management.landing.withCardNavigation.dataTitle": "Données", + "management.landing.withCardNavigation.dataUsageDescription": "Afficher l'utilisation et la conservation des données.", "management.landing.withCardNavigation.dataViewsDescription": "Créez et gérez les données Elasticsearch sélectionnées pour l'exploration.", "management.landing.withCardNavigation.fileManagementDescription": "Accédez à tous les fichiers importés.", "management.landing.withCardNavigation.indexmanagementDescription": "Configurez et assurez la maintenance de vos index Elasticsearch pour le stockage et la récupération des données.", @@ -5855,6 +6479,7 @@ "management.landing.withCardNavigation.rolesDescription": "Créez des rôles uniques pour ce projet et combinez l'ensemble exact de privilèges dont vos utilisateurs ont besoin.", "management.landing.withCardNavigation.rulesDescription": "Définissez à quel moment générer des alertes et des notifications.", "management.landing.withCardNavigation.settingsDescription": "Contrôlez les comportements des projets, tels que l'affichage des dates et le tri par défaut.", + "management.landing.withCardNavigation.spacesDescription": "Organisez vos objets enregistrés en catégories représentatives.", "management.landing.withCardNavigation.tagsDescription": "Organisez, recherchez et filtrez vos objets enregistrés en fonction de critères spécifiques.", "management.landing.withCardNavigation.transformDescription": "Organisez vos données ou copiez les derniers documents dans un index centré sur les entités.", "management.nav.label": "Gestion", @@ -5927,6 +6552,10 @@ "management.settings.spaceCalloutSubtitle": "Les modifications seront uniquement appliquées à l'espace actuel. Ces paramètres sont destinés aux utilisateurs avancés, car des configurations incorrectes peuvent avoir une incidence négative sur des aspects de Kibana.", "management.settings.spaceCalloutTitle": "Les modifications affecteront l'espace actuel.", "management.settings.spaceSettingsTabTitle": "Paramètres de l'espace", + "management.stackManagement.managementDescription": "La console centrale de gestion de la Suite Elastic.", + "management.stackManagement.managementLabel": "Gestion de la Suite", + "management.stackManagement.title": "Gestion de la Suite", + "monaco.esql.hover.acceptableTypes": "Types acceptables", "monaco.esql.hover.policyEnrichedFields": "**Champs**", "monaco.esql.hover.policyIndexes": "**Indexes**", "monaco.esql.hover.policyMatchingField": "**Champ correspondant**", @@ -5934,6 +6563,9 @@ "monaco.painlessLanguage.autocomplete.emitKeywordDescription": "Émettre une valeur sans rien renvoyer", "monaco.painlessLanguage.autocomplete.fieldValueDescription": "Récupérer la valeur du champ \"{fieldName}\"", "monaco.painlessLanguage.autocomplete.paramsKeywordDescription": "Accéder aux variables transmises dans le script", + "navigation.ui_settings.params.defaultRoute.defaultRouteTitle": "Chemin par défaut", + "navigation.uiSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "Doit être une URL relative.", + "navigation.uiSettings.defaultRoute.defaultRouteText": "Ce paramètre spécifie le chemin par défaut lors de l'ouverture de Kibana. Vous pouvez utiliser ce paramètre pour modifier la page de destination à l'ouverture de Kibana. Le chemin doit être une URL relative.", "newsfeed.emptyPrompt.noNewsText": "Si votre instance Kibana n'a pas accès à Internet, demandez à votre administrateur de désactiver cette fonctionnalité. Sinon, nous continuerons d'essayer de récupérer les actualités.", "newsfeed.emptyPrompt.noNewsTitle": "Pas d'actualités ?", "newsfeed.flyoutList.closeButtonLabel": "Fermer", @@ -6285,6 +6917,10 @@ "savedSearch.kibana_context.savedSearchId.help": "Spécifier l'ID de recherche enregistrée à utiliser pour les requêtes et les filtres", "savedSearch.kibana_context.timeRange.help": "Spécifier le filtre de plage temporelle Kibana", "savedSearch.legacyURLConflict.errorMessage": "Cette recherche a la même URL qu'un alias hérité. Désactiver l'alias pour résoudre cette erreur : {json}", + "searchApiKeysComponents.apiKeyForm.createButton": "Créer une clé d'API", + "searchApiKeysComponents.apiKeyForm.noUserPrivileges": "Vous n'avez pas accès à la gestion des clés d'API", + "searchApiKeysComponents.apiKeyForm.showApiKey": "Afficher la clé d'API", + "searchApiKeysComponents.apiKeyForm.title": "Clé d'API", "searchApiPanels.cloudIdDetails.cloudId.description": "Des bibliothèques et des connecteurs clients peuvent utiliser cet identificateur unique propre à Elastic Cloud.", "searchApiPanels.cloudIdDetails.cloudId.title": "Identifiant du cloud", "searchApiPanels.cloudIdDetails.description": "Soyez prêt à ingérer et rechercher vos données en choisissant une option de connexion :", @@ -6307,25 +6943,37 @@ "searchApiPanels.pipeline.overview.pipelineHandling.description": "Gérez les exceptions d'erreur, exécutez un autre pipeline ou redirigez les documents vers un autre index", "searchApiPanels.pipeline.overview.pipelineHandling.title": "Traitement du pipeline", "searchApiPanels.preprocessData.overview.arrayJsonHandling.learnMore": "En savoir plus", + "searchApiPanels.preprocessData.overview.arrayJsonHandling.learnMore.ariaLabel": "En savoir plus sur la gestion des tableaux/JSON", "searchApiPanels.preprocessData.overview.dataEnrichment.description": "Ajouter des informations des sources externes ou appliquer des transformations à vos documents pour une recherche plus contextuelle et pertinente.", "searchApiPanels.preprocessData.overview.dataEnrichment.learnMore": "En savoir plus", + "searchApiPanels.preprocessData.overview.dataEnrichment.learnMore.ariaLabel": "En savoir plus sur l'enrichissement de données", "searchApiPanels.preprocessData.overview.dataEnrichment.title": "Enrichissement des données", "searchApiPanels.preprocessData.overview.dataFiltering.learnMore": "En savoir plus", + "searchApiPanels.preprocessData.overview.dataFiltering.learnMore.ariaLabel": "En savoir plus sur le filtrage des données", "searchApiPanels.preprocessData.overview.dataTransformation.learnMore": "En savoir plus", + "searchApiPanels.preprocessData.overview.dataTransformation.learnMore.ariaLabel": "En savoir plus sur la transformation des données", "searchApiPanels.preprocessData.overview.pipelineHandling.learnMore": "En savoir plus", + "searchApiPanels.preprocessData.overview.pipelineHandling.learnMore.ariaLabel": "En savoir plus sur la gestion des pipelines", + "searchApiPanels.welcomeBanner.codeBox.copyAriaLabel": "Copier l'extrait de code {context}", "searchApiPanels.welcomeBanner.codeBox.copyButtonLabel": "Copier", + "searchApiPanels.welcomeBanner.codeBox.copyLabel": "Copier l'extrait de code", + "searchApiPanels.welcomeBanner.codeBox.selectAriaLabel": "{context} {languageName}", + "searchApiPanels.welcomeBanner.codeBox.selectChangeAriaLabel": "Modifier le langage en {languageName} pour chaque instance de cette page", + "searchApiPanels.welcomeBanner.codeBox.selectLabel": "Sélectionner un langage de programmation pour l'extrait de code {languageName}", "searchApiPanels.welcomeBanner.header.description": "Configurez votre client de langage de programmation, ingérez des données, et vous serez prêt à commencer vos recherches en quelques minutes.", "searchApiPanels.welcomeBanner.header.greeting.customTitle": "👋 Bonjour {name} !", "searchApiPanels.welcomeBanner.header.greeting.defaultTitle": "👋 Bonjour", "searchApiPanels.welcomeBanner.header.title": "Lancez-vous avec Elasticsearch", "searchApiPanels.welcomeBanner.ingestData.alternativeOptions": "Autres options d'ingestion", "searchApiPanels.welcomeBanner.ingestData.alternativeOptions.beatsDescription": "Des agents légers conçus pour le transfert de données pour Elasticsearch. Utilisez Beats pour envoyer des données opérationnelles depuis vos serveurs.", + "searchApiPanels.welcomeBanner.ingestData.alternativeOptions.beatsDocumentation.ariaLabel": "Documentation Beats", "searchApiPanels.welcomeBanner.ingestData.alternativeOptions.beatsDocumentationLabel": "Documentation", "searchApiPanels.welcomeBanner.ingestData.alternativeOptions.beatsTitle": "Beats", "searchApiPanels.welcomeBanner.ingestData.alternativeOptions.logstashDescription": "Pipeline de traitement des données à usage général pour Elasticsearch. Utilisez Logstash pour extraire et transformer les données d'une variétés d'entrées et de sorties.", + "searchApiPanels.welcomeBanner.ingestData.alternativeOptions.logstashDocumentation.ariaLabel": "Documentation Logstash", "searchApiPanels.welcomeBanner.ingestData.alternativeOptions.logstashDocumentationLabel": "Documentation", "searchApiPanels.welcomeBanner.ingestData.alternativeOptions.logstashTitle": "Logstash", - "searchApiPanels.welcomeBanner.ingestData.description": "Ajoutez des données à votre flux de données ou à votre index pour les rendre interrogeables à l'aide de l'API. ", + "searchApiPanels.welcomeBanner.ingestData.description": "Ajoutez des données à votre flux de données ou à votre index pour les rendre interrogeables à l'aide de l'API.", "searchApiPanels.welcomeBanner.ingestData.title": "Ingérer des données", "searchApiPanels.welcomeBanner.ingestPipelinePanel.description": "Vous pouvez utiliser des pipelines d'ingestion pour prétraiter vos données avant leur indexation dans Elasticsearch.", "searchApiPanels.welcomeBanner.ingestPipelinePanel.managedBadge": "Géré", @@ -6338,12 +6986,12 @@ "searchApiPanels.welcomeBanner.installClient.description": "Vous devez d'abord installer le client de langage de programmation de votre choix.", "searchApiPanels.welcomeBanner.installClient.title": "Installer un client", "searchApiPanels.welcomeBanner.panels.learnMore": "En savoir plus", - "searchApiPanels.welcomeBanner.selectClient.apiRequestConsoleDocLink": "Exécuter des requêtes d’API dans la console ", + "searchApiPanels.welcomeBanner.selectClient.apiRequestConsoleDocLink": "Exécuter des requêtes d’API dans la console", "searchApiPanels.welcomeBanner.selectClient.callout.description": "Avec la console, vous pouvez directement commencer à utiliser nos API REST. Aucune installation n’est requise.", "searchApiPanels.welcomeBanner.selectClient.callout.link": "Essayez la console maintenant", "searchApiPanels.welcomeBanner.selectClient.callout.title": "Lancez-vous dans la console", - "searchApiPanels.welcomeBanner.selectClient.description": "Elastic construit et assure la maintenance des clients dans plusieurs langues populaires et notre communauté a contribué à beaucoup d'autres. Sélectionnez votre client de langage favori ou explorez la {console} pour commencer.", - "searchApiPanels.welcomeBanner.selectClient.elasticsearchClientDocLink": "Clients d'Elasticsearch ", + "searchApiPanels.welcomeBanner.selectClient.description": "Elastic construit et assure la maintenance des clients dans plusieurs langues populaires et notre communauté a contribué à beaucoup d'autres. Sélectionnez votre client de langage favori ou découvrez la console pour commencer.", + "searchApiPanels.welcomeBanner.selectClient.elasticsearchClientDocLink": "Clients d'Elasticsearch", "searchApiPanels.welcomeBanner.selectClient.heading": "Choisissez-en un", "searchApiPanels.welcomeBanner.selectClient.title": "Sélectionner votre client", "searchConnectors.config.invalidInteger": "{label} doit être un nombre entier.", @@ -6355,7 +7003,7 @@ "searchConnectors.configurationConnector.config.error.title": "Erreur de connecteur", "searchConnectors.configurationConnector.config.noConfigCallout.description": "Ce connecteur ne possède aucun champ de configuration. Votre connecteur a-t-il pu se connecter avec succès à Elasticsearch et définir sa configuration ?", "searchConnectors.configurationConnector.config.noConfigCallout.title": "Aucun champ de configuration", - "searchConnectors.configurationConnector.config.submitButton.title": "Enregistrer la configuration", + "searchConnectors.configurationConnector.config.submitButton.title": "Sauvegarder et synchroniser", "searchConnectors.connector.documentLevelSecurity.enablePanel.description": "Vous permet de contrôler les documents auxquels peuvent accéder les utilisateurs, selon leurs autorisations. Cela permet de vous assurer que les résultats de recherche ne renvoient que des informations pertinentes et autorisées pour les utilisateurs, selon leurs rôles.", "searchConnectors.connector.documentLevelSecurity.enablePanel.heading": "Sécurité au niveau du document", "searchConnectors.connectors.subscriptionLabel": "Plans d'abonnement", @@ -6507,7 +7155,7 @@ "searchConnectors.nativeConnectors.box.includeInheritedUsersTooltip": "Incluez les groupes et les utilisateurs hérités lors de l'indexation des autorisations. L'activation de ce champ configurable entraînera une dégradation significative des performances.", "searchConnectors.nativeConnectors.box.name": "Box", "searchConnectors.nativeConnectors.box.pathLabel": "Chemin permettant de récupérer les fichiers/dossiers", - "searchConnectors.nativeConnectors.box.pathTooltip": "Le chemin est ignoré lorsque des règles de synchronisation avancées sont appliquées. ", + "searchConnectors.nativeConnectors.box.pathTooltip": "Le chemin est ignoré lorsque des règles de synchronisation avancées sont appliquées.", "searchConnectors.nativeConnectors.box.refreshTokenLabel": "Token d'actualisation", "searchConnectors.nativeConnectors.boxTooltip.name": "Box", "searchConnectors.nativeConnectors.confluence.indexLabelsLabel": "Activer les étiquettes d'indexation", @@ -6709,6 +7357,7 @@ "searchConnectors.nativeConnectors.sharepoint_online.configuration.useDocumentLevelSecurityLabel": "Activer la sécurité au niveau du document", "searchConnectors.nativeConnectors.sharepoint_online.configuration.useDocumentLevelSecurityTooltip": "La sécurité au niveau du document préserve dans Elasticsearch les identités et permissions paramétrées dans Sharepoint Online. Ces métadonnées sont ajoutées à votre document Elasticsearch afin que vous puissiez contrôler l'accès en lecture des utilisateurs et des groupes. La synchronisation de contrôle d'accès garantit que ces métadonnées sont correctement actualisées.", "searchConnectors.nativeConnectors.sharepoint_online.name": "SharePoint en ligne", + "searchConnectors.nativeConnectors.sharepoint_server.configuration.authentication": "Authentification", "searchConnectors.nativeConnectors.sharepoint_server.configuration.fetchUniqueListItemPermissionsLabel": "Récupérer les autorisations d'un élément de liste unique", "searchConnectors.nativeConnectors.sharepoint_server.configuration.fetchUniqueListItemPermissionsTooltip": "Activer cette option pour récupérer les autorisations d'un élément de liste unique. Ce paramètre est susceptible d'augmenter le délai de synchronisation. Si ce paramètre est désactivé, un élément de liste hérite des permissions de son site parent.", "searchConnectors.nativeConnectors.sharepoint_server.configuration.fetchUniqueListPermissionsLabel": "Récupérer les autorisations de liste unique", @@ -6718,6 +7367,8 @@ "searchConnectors.nativeConnectors.sharepoint_server.configuration.site_collections": "Liste de collections de sites SharePoint séparées par des virgules à indexer", "searchConnectors.nativeConnectors.sharepoint_server.configuration.username": "Nom d'utilisateur du serveur SharePoint", "searchConnectors.nativeConnectors.sharepoint_server.name": "Serveur SharePoint", + "searchConnectors.nativeConnectors.sharepoint_server.options.basicLabel": "De base", + "searchConnectors.nativeConnectors.sharepoint_server.options.ntlmLabel": "NTLM", "searchConnectors.nativeConnectors.slack.autoJoinChannels.label": "Rejoindre automatiquement les canaux", "searchConnectors.nativeConnectors.slack.autoJoinChannels.tooltip": "Le bot de l'application Slack pourra seulement lire l'historique des conversations des canaux qu'il a rejoints. L’option par défaut nécessite qu'il soit invité manuellement aux canaux. L'activation de cette option lui permet de s'inviter automatiquement sur tous les canaux publics.", "searchConnectors.nativeConnectors.slack.fetchLastNDays.label": "Nombre de jours d'historique de messages à récupérer", @@ -6870,8 +7521,11 @@ "searchIndexDocuments.result.expandTooltip.showMore": "Afficher {amount} champs en plus", "searchIndexDocuments.result.header.metadata.deleteDocument": "Supprimer le document", "searchIndexDocuments.result.header.metadata.icon.ariaLabel": "Métadonnées pour le document : {id}", + "searchIndexDocuments.result.header.metadata.score": "Score", "searchIndexDocuments.result.header.metadata.title": "Métadonnées du document", "searchIndexDocuments.result.title.id": "ID de document : {id}", + "searchIndexDocuments.result.value.denseVector.copy": "Copier le vecteur", + "searchIndexDocuments.result.value.denseVector.dimLabel": "{value} dimensions", "searchResponseWarnings.badgeButtonLabel": "{warningCount} {warningCount, plural, one {avertissement} other {avertissements}}", "searchResponseWarnings.description.multipleClusters": "Ces clusters ont rencontré des problèmes lors du renvoi des données et les résultats pourraient être incomplets.", "searchResponseWarnings.description.singleCluster": "Ce cluster a rencontré des problèmes lors du renvoi des données et les résultats pourraient être incomplets.", @@ -6879,6 +7533,7 @@ "searchResponseWarnings.title.clustersClause": "Un problème est survenu avec {nonSuccessfulClustersCount} {nonSuccessfulClustersCount, plural, one {cluster} other {clusters}}", "searchResponseWarnings.title.clustersClauseAndRequestsClause": "{clustersClause} pour {requestsCount} requêtes", "searchResponseWarnings.viewDetailsButtonLabel": "Afficher les détails", + "securitySolutionPackages.alertAssignments.upsell": "Passer à {requiredLicense} pour utiliser les affectations d'alertes", "securitySolutionPackages.alertSuppressionRuleDetails.upsell": "La suppression d'alertes est configurée mais elle ne sera pas appliquée en raison d'une licence insuffisante", "securitySolutionPackages.alertSuppressionRuleForm.upsell": "La suppression d'alertes est activée avec la licence {requiredLicense} ou supérieure", "securitySolutionPackages.beta.label": "Bêta", @@ -6890,53 +7545,83 @@ "securitySolutionPackages.dataTable.eventsTab.unit": "{totalCount, plural, =1 {alerte} other {alertes}}", "securitySolutionPackages.dataTable.loadingEventsDataLabel": "Chargement des événements", "securitySolutionPackages.dataTable.unit": "{totalCount, plural, =1 {alerte} other {alertes}}", + "securitySolutionPackages.ecsDataQualityDashboard.actions.askAssistant": "Demander à l'assistant", "securitySolutionPackages.ecsDataQualityDashboard.addToCaseSuccessToast": "Résultats de qualité des données ajoutés au cas", "securitySolutionPackages.ecsDataQualityDashboard.addToNewCaseButton": "Ajouter au nouveau cas", + "securitySolutionPackages.ecsDataQualityDashboard.all": "Tous", + "securitySolutionPackages.ecsDataQualityDashboard.allFields": "Tous les champs", "securitySolutionPackages.ecsDataQualityDashboard.allTab.allFieldsTableTitle": "Tous les champs - {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.cancelButton": "Annuler", + "securitySolutionPackages.ecsDataQualityDashboard.changeYourSearchCriteriaOrRun": "Modifiez vos critères de recherche ou lancez une nouvelle vérification", "securitySolutionPackages.ecsDataQualityDashboard.checkAllButton": "Tout vérifier", "securitySolutionPackages.ecsDataQualityDashboard.checkAllErrorCheckingIndexMessage": "Une erreur s'est produite lors de la vérification de l'index {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.checkingLabel": "Vérification de {index}", + "securitySolutionPackages.ecsDataQualityDashboard.checkNow": "Vérifier maintenant", + "securitySolutionPackages.ecsDataQualityDashboard.close": "Fermer", "securitySolutionPackages.ecsDataQualityDashboard.coldDescription": "L'index n'est plus mis à jour et il est interrogé peu fréquemment. Les informations doivent toujours être interrogeables, mais il est acceptable que ces requêtes soient plus lentes.", "securitySolutionPackages.ecsDataQualityDashboard.coldPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {est} other {sont}} \"cold\". Les index \"cold\" ne sont plus mis à jour et ne sont pas interrogés fréquemment. Les informations doivent toujours être interrogeables, mais il est acceptable que ces requêtes soient plus lentes.", "securitySolutionPackages.ecsDataQualityDashboard.compareFieldsTable.searchFieldsPlaceholder": "Rechercher dans les champs", "securitySolutionPackages.ecsDataQualityDashboard.copyToClipboardButton": "Copier dans le presse-papiers", "securitySolutionPackages.ecsDataQualityDashboard.createADataQualityCaseForIndexHeaderText": "Créer un cas de qualité des données pour l'index {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.createADataQualityCaseHeaderText": "Créer un cas de qualité des données", + "securitySolutionPackages.ecsDataQualityDashboard.customFields": "Champs personnalisés", "securitySolutionPackages.ecsDataQualityDashboard.customTab.customFieldsTableTitle": "Champs personnalisés - {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.customTab.ecsComplaintFieldsTableTitle": "Champs de plainte ECS - {indexName}", + "securitySolutionPackages.ecsDataQualityDashboard.dataQuality": "Qualité des données", + "securitySolutionPackages.ecsDataQualityDashboard.dataQualityDashboardConversationId": "Tableau de bord de Qualité des données", "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPill": "Qualité des données ({indexName})", "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPillTooltip": "Ajoutez ce rapport de Qualité des données comme contexte", "securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPrompt": "Expliquez les résultats ci-dessus et donnez des options pour résoudre les incompatibilités.", "securitySolutionPackages.ecsDataQualityDashboard.defaultPanelTitle": "Vérifier les mappings d'index", + "securitySolutionPackages.ecsDataQualityDashboard.detectionEngineRulesWontWorkMessage": "❌ Les règles de moteur de détection référençant ces champs ne leur correspondront peut-être pas correctement", + "securitySolutionPackages.ecsDataQualityDashboard.docs": "Documents", + "securitySolutionPackages.ecsDataQualityDashboard.documentValuesActual": "Valeurs du document (réelles)", + "securitySolutionPackages.ecsDataQualityDashboard.ecsCompliantFields": "Champs conformes à ECS", + "securitySolutionPackages.ecsDataQualityDashboard.ecsDescription": "Description ECS", + "securitySolutionPackages.ecsDataQualityDashboard.ecsMappingType": "Type de mapping ECS", + "securitySolutionPackages.ecsDataQualityDashboard.ecsMappingTypeExpected": "Type de mapping ECS (attendu)", + "securitySolutionPackages.ecsDataQualityDashboard.ecsValues": "Valeurs ECS", + "securitySolutionPackages.ecsDataQualityDashboard.ecsValuesExpected": "Valeurs ECS (attendues)", "securitySolutionPackages.ecsDataQualityDashboard.ecsVersionStat": "Version ECS", + "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorGenericCheckTitle": "Une erreur s'est produite durant la vérification", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsBody": "Un problème est survenu lors du chargement des mappings : {error}", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMappingsTitle": "Impossible de charger les mappings d'index", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMetadataTitle": "Les index correspondant au modèle {pattern} ne seront pas vérifiés", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingUnallowedValuesBody": "Un problème est survenu lors du chargement des valeurs non autorisées : {error}", "securitySolutionPackages.ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingUnallowedValuesTitle": "Impossible de charger les valeurs non autorisées", + "securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.checkingIndexPrompt": "Vérification de l'index", "securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.loadingEcsMetadataPrompt": "Chargement des métadonnées ECS", "securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.loadingMappingsPrompt": "Chargement des mappings", "securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.loadingStatsPrompt": "Chargement des statistiques", "securitySolutionPackages.ecsDataQualityDashboard.emptyLoadingPrompt.loadingUnallowedValuesPrompt": "Chargement des valeurs non autorisées", + "securitySolutionPackages.ecsDataQualityDashboard.errorLoadingHistoricalResults": "Impossible de charger l'historique", "securitySolutionPackages.ecsDataQualityDashboard.errorLoadingIlmExplainLabel": "Erreur lors du chargement d'ILM Explain : {details}", "securitySolutionPackages.ecsDataQualityDashboard.errorLoadingMappingsLabel": "Erreur lors du chargement des mappings pour {patternOrIndexName} : {details}", "securitySolutionPackages.ecsDataQualityDashboard.errorLoadingStatsLabel": "Erreur lors du chargement des statistiques : {details}", "securitySolutionPackages.ecsDataQualityDashboard.errorLoadingUnallowedValuesLabel": "Erreur lors du chargement des valeurs non autorisées pour l'index {indexName} : {details}", + "securitySolutionPackages.ecsDataQualityDashboard.errors.error": "Erreur", "securitySolutionPackages.ecsDataQualityDashboard.errors.errorMayOccurLabel": "Des erreurs peuvent survenir lorsque le modèle ou les métadonnées de l'index sont temporairement indisponibles, ou si vous ne disposez pas des privilèges requis pour l'accès", + "securitySolutionPackages.ecsDataQualityDashboard.errors.errors": "Erreurs", + "securitySolutionPackages.ecsDataQualityDashboard.errors.errorsCalloutSummary": "La qualité des données n'a pas été vérifiée pour certains index", "securitySolutionPackages.ecsDataQualityDashboard.errors.manage": "gérer", "securitySolutionPackages.ecsDataQualityDashboard.errors.monitor": "moniteur", "securitySolutionPackages.ecsDataQualityDashboard.errors.or": "ou", + "securitySolutionPackages.ecsDataQualityDashboard.errors.pattern": "Modèle", "securitySolutionPackages.ecsDataQualityDashboard.errors.read": "lire", "securitySolutionPackages.ecsDataQualityDashboard.errors.theFollowingPrivilegesLabel": "Les privilèges suivants sont requis pour vérifier un index :", "securitySolutionPackages.ecsDataQualityDashboard.errors.viewIndexMetadata": "view_index_metadata", "securitySolutionPackages.ecsDataQualityDashboard.errorsPopover.viewErrorsButton": "Afficher les erreurs", + "securitySolutionPackages.ecsDataQualityDashboard.fail": "Échec", + "securitySolutionPackages.ecsDataQualityDashboard.failedTooltip": "Échoué", + "securitySolutionPackages.ecsDataQualityDashboard.field": "Champ", "securitySolutionPackages.ecsDataQualityDashboard.fieldsLabel": "Champs", + "securitySolutionPackages.ecsDataQualityDashboard.filterResultsByOutcome": "Filtrer les résultats par issue", "securitySolutionPackages.ecsDataQualityDashboard.frozenDescription": "L'index n'est plus mis à jour et il est rarement interrogé. Les informations doivent toujours être interrogeables, mais il est acceptable que ces requêtes soient extrêmement lentes.", "securitySolutionPackages.ecsDataQualityDashboard.frozenPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {est} other {sont}} \"frozen\". Les index gelés ne sont plus mis à jour et sont rarement interrogés. Les informations doivent toujours être interrogeables, mais il est acceptable que ces requêtes soient extrêmement lentes.", "securitySolutionPackages.ecsDataQualityDashboard.getResultErrorTitle": "Erreur lors de la lecture des résultats d'examen qualité des données sauvegardées", "securitySolutionPackages.ecsDataQualityDashboard.hotDescription": "L'index est mis à jour et interrogé de façon active", "securitySolutionPackages.ecsDataQualityDashboard.hotPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {est} other {sont}} \"hot\". Les index \"hot\" sont mis à jour et interrogés de façon active.", + "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseCapitalized": "Phase ILM", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseCold": "froid", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseFrozen": "frozen", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseHot": "hot", @@ -6948,7 +7633,20 @@ "securitySolutionPackages.ecsDataQualityDashboard.ilmPhasesEmptyPromptTitle": "Sélectionner une ou plusieurs phases ILM", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseUnmanaged": "non géré", "securitySolutionPackages.ecsDataQualityDashboard.ilmPhaseWarm": "warm", + "securitySolutionPackages.ecsDataQualityDashboard.incompatibleCallout": "Les champs sont incompatibles avec ECS lorsque les mappings d'index, ou les valeurs des champs de l'index, ne sont pas conformes à la version {version} d'Elastic Common Schema (ECS).", + "securitySolutionPackages.ecsDataQualityDashboard.incompatibleCalloutTitle": "{fieldCount} {fieldCount, plural, =1 {Champ incompatible} other {Champs incompatibles}}", + "securitySolutionPackages.ecsDataQualityDashboard.incompatibleEmptyContent": "Tous les mappings de champs et toutes les valeurs de documents de cet index sont conformes à Elastic Common Schema (ECS).", + "securitySolutionPackages.ecsDataQualityDashboard.incompatibleEmptyTitle": "Toutes les valeurs et tous les mappings de champs sont conformes à ECS", + "securitySolutionPackages.ecsDataQualityDashboard.incompatibleFieldMappings": "Mappings de champ incompatibles – {indexName}", + "securitySolutionPackages.ecsDataQualityDashboard.incompatibleFields": "Champs incompatibles", + "securitySolutionPackages.ecsDataQualityDashboard.incompatibleFieldsWithCount": "{count, plural, one {Champ incompatible} other {Champs incompatibles}}", + "securitySolutionPackages.ecsDataQualityDashboard.incompatibleFieldValues": "Valeurs de champ incompatibles – {indexName}", + "securitySolutionPackages.ecsDataQualityDashboard.index": "Index", + "securitySolutionPackages.ecsDataQualityDashboard.indexCheckFlyout.historyTab": "Historique", + "securitySolutionPackages.ecsDataQualityDashboard.indexCheckFlyout.latestCheckTab": "Dernière vérification", "securitySolutionPackages.ecsDataQualityDashboard.indexLifecycleManagementPhasesTooltip": "La qualité des données sera vérifiée pour les index comprenant ces phases de gestion du cycle de vie des index (ILM, Index Lifecycle Management)", + "securitySolutionPackages.ecsDataQualityDashboard.indexMappingType": "Type de mapping d'index", + "securitySolutionPackages.ecsDataQualityDashboard.indexMappingTypeActual": "Type de mapping d'index (réel)", "securitySolutionPackages.ecsDataQualityDashboard.indexNameLabel": "Nom de l'index", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.addToNewCaseButton": "Ajouter au nouveau cas", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.allCallout": "Tous les mappings relatifs aux champs de cet index, y compris ceux qui sont conformes à la version {version} d'Elastic Common Schema (ECS) et ceux qui ne le sont pas", @@ -6984,47 +7682,81 @@ "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownDescription": "L'index `{indexName}` a des [mappings]({mappingUrl}) ou des valeurs de champ différentes de l'[Elastic Common Schema]({ecsReferenceUrl}) (ECS), [définitions]({ecsFieldReferenceUrl}).de version `{version}`.", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.summaryMarkdownTitle": "Qualité des données", "securitySolutionPackages.ecsDataQualityDashboard.indexProperties.unknownCategoryLabel": "Inconnu", - "securitySolutionPackages.ecsDataQualityDashboard.indexSizeTooltip": "La taille de l'index principal (n'inclut pas de répliques)", + "securitySolutionPackages.ecsDataQualityDashboard.indexSizeTooltip": "Taille de l'index (sans les répliques)", + "securitySolutionPackages.ecsDataQualityDashboard.indices": "Index", + "securitySolutionPackages.ecsDataQualityDashboard.indicesChecked": "Index vérifiés", + "securitySolutionPackages.ecsDataQualityDashboard.introducingDataQualityHistory": "Présentation de l'historique de la qualité des données", "securitySolutionPackages.ecsDataQualityDashboard.lastCheckedLabel": "Dernière vérification", + "securitySolutionPackages.ecsDataQualityDashboard.loadingHistoricalResults": "Chargement des résultats antérieurs", + "securitySolutionPackages.ecsDataQualityDashboard.mappingThatConflictWithEcsMessage": "❌ Les mappings ou valeurs de champs qui ne sont pas conformes à ECS ne sont pas pris en charge", + "securitySolutionPackages.ecsDataQualityDashboard.noResultsMatchYourSearchCriteria": "Aucun résultat ne correspond à vos critères de recherche.", + "securitySolutionPackages.ecsDataQualityDashboard.notIncludedInHistoricalResults": "Non inclus dans les résultats antérieurs. Pour afficher les données complètes sur les champs de la même famille, exécutez une nouvelle vérification.", + "securitySolutionPackages.ecsDataQualityDashboard.pagesMayNotDisplayEventsMessage": "❌ Les pages peuvent ne pas afficher certains événements ou champs en raison de mappings ou valeurs de champs inattendus", + "securitySolutionPackages.ecsDataQualityDashboard.pass": "Réussite", + "securitySolutionPackages.ecsDataQualityDashboard.passedTooltip": "Approuvé", "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.allPassedTooltip": "Tous les index correspondant à ce modèle ont réussi les vérifications de qualité des données", + "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someFailedTooltip": "Au moins un index correspondant à ce modèle a échoué à un contrôle de qualité des données", + "securitySolutionPackages.ecsDataQualityDashboard.patternLabel.someUncheckedTooltip": "Au moins un index correspondant à ce modèle n'a pas été vérifié pour la qualité des données", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.docsLabel": "Documents", "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.indicesLabel": "Index", - "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.patternOrIndexTooltip": "Modèle, ou index spécifique", + "securitySolutionPackages.ecsDataQualityDashboard.patternSummary.patternOrIndexTooltip": "Nom d'index ou modèle", "securitySolutionPackages.ecsDataQualityDashboard.postResultErrorTitle": "Erreur lors de l'écriture des résultats d'examen qualité des données sauvegardées", "securitySolutionPackages.ecsDataQualityDashboard.remoteClustersCallout.title": "Les clusters distants ne seront pas vérifiés", "securitySolutionPackages.ecsDataQualityDashboard.remoteClustersCallout.toCheckIndicesOnRemoteClustersLabel": "Pour vérifier les index sur des clusters distants prenant en charge la recherche dans différents clusters, connectez-vous à l'instance Kibana du cluster distant", + "securitySolutionPackages.ecsDataQualityDashboard.result": "Résultat", + "securitySolutionPackages.ecsDataQualityDashboard.sameFamily": "Même famille", "securitySolutionPackages.ecsDataQualityDashboard.sameFamilyBadgeLabel": "même famille", "securitySolutionPackages.ecsDataQualityDashboard.sameFamilyTab.sameFamilyFieldMappingsTableTitle": "Mêmes familles de mappings de champ – {indexName}", "securitySolutionPackages.ecsDataQualityDashboard.securitySolutionPackages.ecsDataQualityDashboardSubtitle": "Vérifiez la compatibilité des mappings et des valeurs d'index avec", "securitySolutionPackages.ecsDataQualityDashboard.selectAnIndexPrompt": "Sélectionner un index pour le comparer à la version ECS", "securitySolutionPackages.ecsDataQualityDashboard.selectOneOrMorPhasesPlaceholder": "Sélectionner une ou plusieurs phases ILM", + "securitySolutionPackages.ecsDataQualityDashboard.size": "Taille", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.checkedLabel": "vérifié", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.docsLabel": "Documents", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleFieldsLabel": "Champs incompatibles", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.incompatibleIndexToolTip": "Mappings et valeurs incompatibles avec ECS", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesCheckedLabel": "Index vérifiés", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.indicesLabel": "Index", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sameFamilyLabel": "Même famille", "securitySolutionPackages.ecsDataQualityDashboard.statLabels.sizeLabel": "Taille", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsToolTip": "Nombre total de documents, dans tous les index", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatibleToolTip": "Nombre total de champs incompatibles avec ECS, dans tous les index qui ont été vérifiés", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesToolTip": "Nombre total de tous les index", - "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizeToolTip": "La taille totale de tous les index principaux (n'inclut pas de répliques)", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCheckedIndicesPatternToolTip": "Nombre total d'index vérifiés correspondant à ce modèle d'index", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalCheckedIndicesToolTip": "Nombre total d'index vérifiés", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsPatternToolTip": "Nombre total de documents dans les index correspondant à ce modèle d'indexation", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalDocsToolTip": "Nombre total de documents dans l'ensemble des index", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatiblePatternToolTip": "Nombre total de champs vérifiés incompatibles avec ECS dans les index correspondant à ce modèle d'indexation", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIncompatibleToolTip": "Nombre total de champs vérifiés incompatibles avec ECS", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesPatternToolTip": "Nombre total d'index correspondant à ce modèle d'indexation", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalIndicesToolTip": "Nombre total d'index", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizePatternToolTip": "Taille totale des indices (hors répliques) correspondant à ce modèle d'indexation", + "securitySolutionPackages.ecsDataQualityDashboard.statLabels.totalSizeToolTip": "Taille totale des indices (hors répliques)", "securitySolutionPackages.ecsDataQualityDashboard.storage.docs.unit": "{totalCount, plural, =1 {Document} other {Documents}}", "securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataLabel": "Aucune donnée à afficher", "securitySolutionPackages.ecsDataQualityDashboard.storageTreemap.noDataReasonLabel": "Le champ {stackByField1} n'était présent dans aucun groupe", + "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.actionsColumn": "Actions", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.collapseLabel": "Réduire", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.expandRowsColumn": "Développer les lignes", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexesNameLabel": "Nom de l'index", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.indexToolTip": "Cet index correspond au nom d'index ou de modèle : {pattern}", "securitySolutionPackages.ecsDataQualityDashboard.summaryTable.lastCheckColumn": "Dernière vérification", + "securitySolutionPackages.ecsDataQualityDashboard.thisIndexHasNotBeenCheckedTooltip": "Cet index n'a pas été vérifié", "securitySolutionPackages.ecsDataQualityDashboard.timestampDescriptionLabel": "Date/heure d'origine de l'événement. Il s'agit des date et heure extraites de l'événement, représentant généralement le moment auquel l'événement a été généré par la source. Si la source de l'événement ne comporte pas d'horodatage original, cette valeur est habituellement remplie la première fois que l'événement a été reçu par le pipeline. Champs requis pour tous les événements.", "securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedErrorsToastTitle": "Erreurs copiées dans le presse-papiers", "securitySolutionPackages.ecsDataQualityDashboard.toasts.copiedResultsToastTitle": "Résultats copiés dans le presse-papiers", + "securitySolutionPackages.ecsDataQualityDashboard.toggleHistoricalResultCheckedAt": "Bascule de résultat historique vérifié à {checkedAt}", + "securitySolutionPackages.ecsDataQualityDashboard.totalChecks": "{formattedCount} {count, plural, one {vérification} other {vérifications}}", + "securitySolutionPackages.ecsDataQualityDashboard.tryIt": "Essayer", "securitySolutionPackages.ecsDataQualityDashboard.unmanagedDescription": "L'index n'est pas géré par la Gestion du cycle de vie des index (ILM)", "securitySolutionPackages.ecsDataQualityDashboard.unmanagedPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {n'est pas géré} other {ne sont pas gérés}} par la gestion du cycle de vie des index (ILM)", + "securitySolutionPackages.ecsDataQualityDashboard.viewHistory": "Afficher l'historique", + "securitySolutionPackages.ecsDataQualityDashboard.viewPastResults": "Voir les résultats antérieurs", "securitySolutionPackages.ecsDataQualityDashboard.warmDescription": "L'index n'est plus mis à jour mais il est toujours interrogé", "securitySolutionPackages.ecsDataQualityDashboard.warmPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {est} other {sont}} \"warm\". Les index \"warm\" ne sont plus mis à jour, mais ils sont toujours interrogés.", "securitySolutionPackages.entityAnalytics.navigation": "Analyse des entités", "securitySolutionPackages.entityAnalytics.pageDesc": "Détecter les menaces des utilisateurs et des hôtes de votre réseau avec l'Analyse des entités", "securitySolutionPackages.entityAnalytics.paywall.upgradeButton": "Passer à {requiredLicenseOrProduct}", + "securitySolutionPackages.features.featureRegistry.assistant.manageGlobalKnowledgeBaseSubFeatureDescription": "Apportez des modifications à n'importe quelle entrée de la base de connaissances personnalisée au niveau de l'espace (global). Cela permettra également aux utilisateurs de modifier les entrées globales créées par d'autres utilisateurs.", + "securitySolutionPackages.features.featureRegistry.assistant.manageGlobalKnowledgeBaseSubFeatureDetails": "Autoriser les modifications des entrées globales", + "securitySolutionPackages.features.featureRegistry.assistant.manageGlobalKnowledgeBaseSubFeatureName": "Base de connaissances", "securitySolutionPackages.features.featureRegistry.assistant.updateAnonymizationSubFeatureDetails": "Autoriser les modifications", "securitySolutionPackages.features.featureRegistry.assistant.updateAnonymizationSubFeatureName": "Sélection et Anonymisation de champ", "securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails": "Modifier les paramètres du cas", @@ -7032,8 +7764,10 @@ "securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails": "Supprimer les cas et les commentaires", "securitySolutionPackages.features.featureRegistry.deleteSubFeatureName": "Supprimer", "securitySolutionPackages.features.featureRegistry.linkSecuritySolutionAssistantTitle": "Assistant d’intelligence artificielle d’Elastic", + "securitySolutionPackages.features.featureRegistry.linkSecuritySolutionAttackDiscoveryTitle": "Attack discovery", "securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitle": "Cas", "securitySolutionPackages.features.featureRegistry.linkSecuritySolutionTitle": "Sécurité", + "securitySolutionPackages.features.featureRegistry.securityGroupDescription": "Chaque privilège de sous-fonctionnalité de ce groupe doit être attribué individuellement. L'affectation globale n'est prise en charge que lorsque votre offre tarifaire n'autorise pas les privilèges de fonctionnalités individuelles.", "securitySolutionPackages.features.featureRegistry.subFeatures.assistant.description": "Modifiez les champs par défaut autorisés à être utilisés par l'assistant IA et Attack discovery. Anonymisez n'importe quel contenu pour les champs sélectionnés.", "securitySolutionPackages.features.featureRegistry.subFeatures.blockList": "Liste noire", "securitySolutionPackages.features.featureRegistry.subFeatures.blockList.description": "Étendez la protection d'Elastic Defend contre les processus malveillants et protégez-vous des applications potentiellement nuisibles.", @@ -7079,6 +7813,10 @@ "securitySolutionPackages.navigation.landingLinks": "Vues de sécurité", "securitySolutionPackages.sideNav.betaBadge.label": "Bêta", "securitySolutionPackages.sideNav.togglePanel": "Activer/Désactiver le panneau de navigation", + "securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.betaBadge": "Version d'évaluation technique", + "securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.betaTooltip": "Cette fonctionnalité est en version d’évaluation technique, elle est susceptible d’être modifiée. Veuillez utiliser Attack Discovery avec prudence dans les environnements de production.", + "securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.pageTitle": "Attack discovery", + "securitySolutionPackages.upselling.sections.attackDiscovery.findPotentialAttacksWithAiTitle": "Trouvez les attaques potentielles grâce à l'IA", "share.advancedSettings.csv.quoteValuesText": "Les valeurs doivent-elles être mises entre guillemets dans les exportations CSV ?", "share.advancedSettings.csv.quoteValuesTitle": "Mettre les valeurs CSV entre guillemets", "share.advancedSettings.csv.separatorText": "Séparer les valeurs exportées avec cette chaîne", @@ -7094,8 +7832,6 @@ "share.dashboard.link.description": "Partagez un lien direct avec cette recherche.", "share.embed.dashboard.helpText": "Intégrez ce tableau de bord dans une autre page web. Sélectionnez les éléments à inclure dans la vue intégrable.", "share.embed.helpText": "Intégrez ce {objectType} dans une autre page web.", - "share.export.generateButtonLabel": "Exporter un fichier", - "share.export.helpText": "Sélectionnez le type de fichier que vous souhaitez exporter pour cette visualisation.", "share.fileType": "Type de fichier", "share.link.copied": "Texte copié", "share.link.copyEmbedCodeButton": "Copier le code intégré", @@ -7106,7 +7842,7 @@ "share.modalContent.copyUrlButtonLabel": "Copier l'URL Post", "share.postURLWatcherMessage": "Copiez cette URL POST pour appeler la génération depuis l'extérieur de Kibana ou à partir de Watcher.", "share.postURLWatcherMessage.unsavedChanges": "L'URL peut changer si vous mettez Kibana à niveau.", - "share.screenCapturePanelContent.optimizeForPrintingHelpText": "Utilise plusieurs pages, affichant au maximum 2 visualisations par page ", + "share.screenCapturePanelContent.optimizeForPrintingHelpText": "Utilise plusieurs pages, affichant au maximum 2 visualisations par page", "share.screenCapturePanelContent.optimizeForPrintingLabel": "Pour l'impression", "share.urlPanel.canNotShareAsSavedObjectHelpText": "Pour le partager comme objet enregistré, enregistrez le {objectType}.", "share.urlPanel.copyIframeCodeButtonLabel": "Copier le code iFrame", @@ -7139,6 +7875,8 @@ "sharedUXPackages.card.noData.noPermission.title": "Contactez votre administrateur", "sharedUXPackages.card.noData.title": "Ajouter Elastic Agent", "sharedUXPackages.chrome.sideNavigation.betaBadge.label": "Bêta", + "sharedUXPackages.chrome.sideNavigation.feedbackCallout.btn": "Faites-nous en part", + "sharedUXPackages.chrome.sideNavigation.feedbackCallout.title": "Comment fonctionne la navigation de votre côté ? Il manque quelque chose ?", "sharedUXPackages.chrome.sideNavigation.recentlyAccessed.title": "Récent", "sharedUXPackages.chrome.sideNavigation.togglePanel": "Afficher/Masquer le panneau de navigation \"{title}\"", "sharedUXPackages.codeEditor.ariaLabel": "Éditeur de code", @@ -7206,11 +7944,17 @@ "sharedUXPackages.noDataPage.introNoDocLink": "Ajoutez vos données pour commencer.", "sharedUXPackages.noDataPage.welcomeTitle": "Bienvenue dans Elastic {solution}.", "sharedUXPackages.noDataViewsPrompt.addDataViewText": "Créer une vue de données", + "sharedUXPackages.noDataViewsPrompt.addDataViewTextNoPrivilege": "Créer une vue de données", + "sharedUXPackages.noDataViewsPrompt.addDataViewTooltipNoPrivilege": "Demandez à votre administrateur les autorisations nécessaires pour créer une vue de données.", + "sharedUXPackages.noDataViewsPrompt.createDataView": "Créer une vue de données", "sharedUXPackages.noDataViewsPrompt.dataViewExplanation": "Les vues de données identifient les données Elasticsearch que vous souhaitez explorer. Vous pouvez faire pointer des vues de données vers un ou plusieurs flux de données, index et alias d'index, tels que vos données de log d'hier, ou vers tous les index contenant vos données de log.", + "sharedUXPackages.noDataViewsPrompt.esqlExplanation": "ES|QL est un langage de requête canalisé de nouvelle génération et un moteur de calcul développé par Elastic pour filtrer, transformer et analyser les données. ES|QL vous aide à rationaliser vos workflows afin d'assurer un traitement des données rapide et efficace.", + "sharedUXPackages.noDataViewsPrompt.esqlPanel.title": "Interrogez vos données avec ES|QL", "sharedUXPackages.noDataViewsPrompt.learnMore": "Envie d'en savoir plus ?", "sharedUXPackages.noDataViewsPrompt.noPermission.dataViewExplanation": "Les vues de données identifient les données Elasticsearch que vous souhaitez explorer. Pour créer des vues de données, demandez les autorisations requises à votre administrateur.", "sharedUXPackages.noDataViewsPrompt.readDocumentation": "Lisez les documents", - "sharedUXPackages.noDataViewsPrompt.youHaveData": "Vous avez des données dans Elasticsearch.", + "sharedUXPackages.noDataViewsPrompt.tryEsqlText": "Essayer ES|QL", + "sharedUXPackages.noDataViewsPrompt.youHaveData": "Comment souhaitez-vous explorer vos données Elasticsearch ?", "sharedUXPackages.prompt.errors.notFound.body": "Désolé, la page que vous recherchez est introuvable. Elle a peut-être été retirée ou renommée, ou peut-être qu'elle n'a jamais existé.", "sharedUXPackages.prompt.errors.notFound.goBacklabel": "Retour", "sharedUXPackages.prompt.errors.notFound.title": "Page introuvable", @@ -7218,6 +7962,7 @@ "sharedUXPackages.solutionNav.menuText": "menu", "sharedUXPackages.solutionNav.mobileTitleText": "{solutionName} {menuText}", "sharedUXPackages.solutionNav.openLabel": "Ouvrir la navigation latérale", + "sse.internalError": "Une erreur interne s'est produite", "telemetry.callout.appliesSettingTitle": "Les modifications apportées à ce paramètre s'appliquent dans {allOfKibanaText} et sont enregistrées automatiquement.", "telemetry.callout.appliesSettingTitle.allOfKibanaText": "tout Kibana", "telemetry.callout.clusterStatisticsDescription": "Voici un exemple des statistiques de cluster de base que nous collecterons. Cela comprend le nombre d'index, de partitions et de nœuds. Cela comprend également des statistiques d'utilisation de niveau élevé, comme l'état d'activation du monitoring.", @@ -7295,12 +8040,12 @@ "timelion.help.functions.fitHelpText": "Remplit les valeurs nulles à l'aide d'une fonction fit définie.", "timelion.help.functions.hide.args.hideHelpText": "Masquer ou afficher les séries", "timelion.help.functions.hideHelpText": "Masquer les séries par défaut", - "timelion.help.functions.holt.args.alphaHelpText": "\n Pondération de lissage de 0 à 1.\n Augmentez l’alpha pour que la nouvelle série suive de plus près l'originale.\n Diminuez-le pour rendre la série plus lisse.", - "timelion.help.functions.holt.args.betaHelpText": "\n Pondération de tendance de 0 à 1.\n Augmentez le bêta pour que les lignes montantes/descendantes continuent à monter/descendre plus longtemps.\n Diminuez-le pour que la fonction apprenne plus rapidement la nouvelle tendance.", - "timelion.help.functions.holt.args.gammaHelpText": "\n Pondération saisonnière de 0 à 1. Vos données ressemblent-elles à une vague ?\n Augmentez cette valeur pour donner plus d'importance aux saisons récentes et ainsi modifier plus rapidement la forme de la vague.\n Diminuez-la pour réduire l'importance des nouvelles saisons et ainsi rendre l'historique plus important.\n ", - "timelion.help.functions.holt.args.sampleHelpText": "\n Le nombre de saisons à échantillonner avant de commencer à \"prédire\" dans une série saisonnière.\n (Utile uniquement avec gamma, par défaut : all)", + "timelion.help.functions.holt.args.alphaHelpText": "Pondération de lissage de 0 à 1. Augmentez l’alpha pour que la nouvelle série suive de plus près l'originale. Diminuez-le pour rendre la série plus lisse.", + "timelion.help.functions.holt.args.betaHelpText": "Pondération de tendance de 0 à 1. Augmentez le bêta pour que les lignes montantes/descendantes continuent à monter/descendre plus longtemps. Diminuez-le pour que la fonction apprenne plus rapidement la nouvelle tendance.", + "timelion.help.functions.holt.args.gammaHelpText": "Pondération saisonnière de 0 à 1. Vos données ressemblent-elles à une vague ? Augmentez cette valeur pour donner plus d'importance aux saisons récentes et ainsi modifier plus rapidement la forme de la vague. Diminuez-la pour réduire l'importance des nouvelles saisons et ainsi rendre l'historique plus important.", + "timelion.help.functions.holt.args.sampleHelpText": "Le nombre de saisons à échantillonner avant de commencer à \"prédire\" dans une série saisonnière. (Utile uniquement avec gamma, par défaut : all)", "timelion.help.functions.holt.args.seasonHelpText": "La longueur de la saison, par ex. 1w, si votre modèle se répète chaque semaine. (Utile uniquement avec gamma)", - "timelion.help.functions.holtHelpText": "\n Échantillonner le début d'une série et l'utiliser pour prévoir ce qui devrait se produire\n via plusieurs paramètres facultatifs. En règle générale, cela ne prédit pas\n l'avenir, mais ce qui devrait se produire maintenant en fonction des données passées,\n ce qui peut être utile pour la détection des anomalies. Notez que les valeurs null seront remplacées par des valeurs prévues.", + "timelion.help.functions.holtHelpText": "Échantillonner le début d'une série et l'utiliser pour prévoir ce qui devrait se produire via plusieurs paramètres facultatifs. En règle générale, cela ne prédit pas l'avenir, mais ce qui devrait se produire maintenant en fonction des données passées, ce qui peut être utile pour la détection des anomalies. Notez que les valeurs null seront remplacées par des valeurs prévues.", "timelion.help.functions.label.args.labelHelpText": "Valeur de légende pour les séries. Vous pouvez utiliser $1, $2, etc. dans la chaîne pour correspondre aux groupes de captures d'expressions régulières.", "timelion.help.functions.label.args.regexHelpText": "Une expression régulière compatible avec les groupes de captures", "timelion.help.functions.labelHelpText": "Modifiez l'étiquette des séries. Utiliser %s pour référencer l'étiquette existante", @@ -7367,10 +8112,10 @@ "timelion.help.functions.trim.args.startHelpText": "Compartiments à retirer du début de la série. Par défaut : 1", "timelion.help.functions.trimHelpText": "Définir N compartiments au début ou à la fin de la série sur null pour ajuster le \"problème de compartiment partiel\"", "timelion.help.functions.worldbank.args.codeHelpText": "Chemin de l'API Worldbank (Banque mondiale). Il s'agit généralement de tout ce qui suit le domaine, avant la chaîne de requête. Par exemple : {apiPathExample}.", - "timelion.help.functions.worldbankHelpText": "\n [expérimental]\n Extrayez des données de {worldbankUrl} à l'aide du chemin d’accès aux séries.\n La Banque mondiale fournit surtout des données annuelles et n'a souvent aucune donnée pour l'année en cours.\n Essayez {offsetQuery} si vous n’obtenez pas de données pour les plages temporelles récentes.", + "timelion.help.functions.worldbankHelpText": "[expérimental] Extrayez des données de {worldbankUrl} à l'aide du chemin d’accès aux séries. La Banque mondiale fournit surtout des données annuelles et n'a souvent aucune donnée pour l'année en cours. Essayez {offsetQuery} si vous n’obtenez pas de données pour les plages temporelles récentes.", "timelion.help.functions.worldbankIndicators.args.countryHelpText": "Identifiant de pays de la Banque mondiale. Généralement le code à 2 caractères du pays.", "timelion.help.functions.worldbankIndicators.args.indicatorHelpText": "Le code d'indicateur à utiliser. Vous devrez le rechercher sur {worldbankUrl}. Souvent très complexe. Par exemple, {indicatorExample} correspond à la population.", - "timelion.help.functions.worldbankIndicatorsHelpText": "\n [expérimental]\n Extrayez des données de {worldbankUrl} à l'aide du nom et de l'indicateur du pays. La Banque mondiale fournit\n surtout des données annuelles et n'a souvent aucune donnée pour l'année en cours. Essayez {offsetQuery} si vous n’obtenez pas de données pour\n les plages temporelles récentes.", + "timelion.help.functions.worldbankIndicatorsHelpText": "[expérimental] Extrayez des données de {worldbankUrl} à l'aide du nom et de l'indicateur du pays. La Banque mondiale fournit surtout des données annuelles et n'a souvent aucune donnée pour l'année en cours. Essayez {offsetQuery} si vous n’obtenez pas de données pour les plages temporelles récentes.", "timelion.help.functions.yaxis.args.colorHelpText": "Couleur de l'étiquette de l'axe", "timelion.help.functions.yaxis.args.labelHelpText": "Étiquette de l'axe", "timelion.help.functions.yaxis.args.maxHelpText": "Valeur max.", @@ -7425,6 +8170,9 @@ "timelion.vis.invalidIntervalErrorMessage": "Format d'intervalle non valide.", "timelion.vis.selectIntervalHelpText": "Choisissez une option ou créez une valeur personnalisée. Exemples : 30s, 20m, 24h, 2d, 1w, 1M", "timelion.vis.selectIntervalPlaceholder": "Choisir un intervalle", + "tryInConsole.button.text": "Exécuter dans la console", + "tryInConsole.embeddedConsoleButton.ariaLabel": "Exécuter dans la console - s'ouvre dans la console intégrée", + "tryInConsole.inNewTab.button.ariaLabel": "Exécuter dans la console - s'ouvre dans un nouvel onglet", "uiActions.actionPanel.more": "Plus", "uiActions.actionPanel.title": "Options", "uiActions.errors.incompatibleAction": "Action non compatible", @@ -7503,10 +8251,11 @@ "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "Aide pour la syntaxe", "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "Variables de filtre", "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "Aide", - "uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "Format non valide.", + "unifiedDataTable.additionalActionsColumnHeader": "Colonne d'actions supplémentaires", "unifiedDataTable.advancedDiffModesTooltip": "Les modes avancés offrent des capacités de diffraction améliorées, mais ils fonctionnent sur des documents bruts et ne prennent donc pas en charge le formatage des champs.", "unifiedDataTable.clearSelection": "Effacer la sélection", - "unifiedDataTable.compareSelectedRowsButtonLabel": "Comparer", + "unifiedDataTable.compareSelectedRowsButtonDisabledTooltip": "La comparaison est limitée à {limit} lignes", + "unifiedDataTable.compareSelectedRowsButtonLabel": "Comparer les éléments sélectionnés", "unifiedDataTable.comparingDocuments": "Comparaison de {documentCount} documents", "unifiedDataTable.comparingResults": "Comparaison de {documentCount} résultats", "unifiedDataTable.comparisonColumnPinnedTooltip": "Document épinglé : {documentId}", @@ -7519,10 +8268,15 @@ "unifiedDataTable.controlColumnHeader": "Colonne de commande", "unifiedDataTable.copyColumnNameToClipboard.toastTitle": "Copié dans le presse-papiers", "unifiedDataTable.copyColumnValuesToClipboard.toastTitle": "Valeurs de la colonne \"{column}\" copiées dans le presse-papiers", + "unifiedDataTable.copyDocsToClipboardJSON": "Copier des documents au format JSON", "unifiedDataTable.copyEscapedValueWithFormulasToClipboardWarningText": "Les valeurs peuvent contenir des formules avec échappement.", "unifiedDataTable.copyFailedErrorText": "Impossible de copier dans le presse-papiers avec ce navigateur", - "unifiedDataTable.copyResultsToClipboardJSON": "Copier les résultats dans le presse-papiers (JSON)", + "unifiedDataTable.copyResultsToClipboardJSON": "Copier les résultats au format JSON", + "unifiedDataTable.copyRowsAsJsonToClipboard.toastTitle": "Copié dans le presse-papiers", + "unifiedDataTable.copyRowsAsTextToClipboard.toastTitle": "Copié dans le presse-papiers", + "unifiedDataTable.copySelectionToClipboard": "Copier la sélection en tant que texte", "unifiedDataTable.copyValueToClipboard.toastTitle": "Copié dans le presse-papiers", + "unifiedDataTable.deselectAllRowsOnPageColumnHeader": "Désélectionner toutes les lignes visibles", "unifiedDataTable.diffModeChars": "Par caractère", "unifiedDataTable.diffModeFullValue": "Valeur totale", "unifiedDataTable.diffModeLines": "Par ligne", @@ -7533,17 +8287,20 @@ "unifiedDataTable.enableShowDiff": "Vous devez activer l'option Afficher les différences", "unifiedDataTable.exitDocumentComparison": "Quitter le mode comparaison", "unifiedDataTable.fieldColumnTitle": "Champ", + "unifiedDataTable.grid.additionalRowActions": "Actions supplémentaires", "unifiedDataTable.grid.closePopover": "Fermer la fenêtre contextuelle", "unifiedDataTable.grid.copyCellValueButton": "Copier la valeur", "unifiedDataTable.grid.copyClipboardButtonTitle": "Copier la valeur de {column}", "unifiedDataTable.grid.copyColumnNameToClipBoardButton": "Copier le nom", "unifiedDataTable.grid.copyColumnValuesToClipBoardButton": "Copier la colonne", - "unifiedDataTable.grid.documentHeader": "Document", + "unifiedDataTable.grid.documentHeader": "Résumé", "unifiedDataTable.grid.editFieldButton": "Modifier le champ de la vue de données", + "unifiedDataTable.grid.esqlMultivalueFilteringDisabled": "Le filtrage multivalué n'est pas pris en charge dans ES|QL", "unifiedDataTable.grid.filterFor": "Filtrer sur", "unifiedDataTable.grid.filterForAria": "Filtrer sur cette {value}", "unifiedDataTable.grid.filterOut": "Exclure", "unifiedDataTable.grid.filterOutAria": "Exclure cette {value}", + "unifiedDataTable.grid.resetColumnWidthButton": "Réinitialiser la largeur", "unifiedDataTable.grid.selectDoc": "Sélectionner le document \"{rowNumber}\"", "unifiedDataTable.grid.viewDoc": "Afficher/Masquer les détails de la boîte de dialogue", "unifiedDataTable.gridSampleSize.fetchMoreLinkDisabledTooltip": "Pour charger plus, l'intervalle d'actualisation doit d'abord être désactivé", @@ -7565,6 +8322,8 @@ "unifiedDataTable.sampleSizeSettings.sampleSizeLabel": "Taille de l'échantillon", "unifiedDataTable.searchGenerationWithDescription": "Tableau généré par la recherche {searchTitle}", "unifiedDataTable.searchGenerationWithDescriptionGrid": "Tableau généré par la recherche {searchTitle} ({searchDescription})", + "unifiedDataTable.selectAllDocs": "Tout sélectionner ({rowsCount})", + "unifiedDataTable.selectAllRowsOnPageColumnHeader": "Sélectionner toutes les lignes visibles", "unifiedDataTable.selectColumnHeader": "Sélectionner la colonne", "unifiedDataTable.selectedResultsButtonLabel": "Sélectionné", "unifiedDataTable.selectedRowsButtonLabel": "Sélectionné", @@ -7581,9 +8340,15 @@ "unifiedDataTable.showSelectedResultsOnly": "Afficher uniquement les résultats sélectionnés", "unifiedDataTable.tableHeader.timeFieldIconTooltip": "Ce champ représente l'heure à laquelle les événements se sont produits.", "unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel": "{timeFieldName} : ce champ représente l'heure à laquelle les événements se sont produits.", + "unifiedDocViewer.docView.logsOverview.accordion.qualityIssues.table.datasetQualityLinkTitle": "Détails de l’ensemble de données", + "unifiedDocViewer.docView.logsOverview.accordion.qualityIssues.table.field": "Problème", + "unifiedDocViewer.docView.logsOverview.accordion.qualityIssues.table.textIgnored": "champ ignoré", + "unifiedDocViewer.docView.logsOverview.accordion.qualityIssues.table.values": "Valeurs", "unifiedDocViewer.docView.logsOverview.accordion.title.cloud": "Cloud", "unifiedDocViewer.docView.logsOverview.accordion.title.other": "Autre", + "unifiedDocViewer.docView.logsOverview.accordion.title.qualityIssues": "Problèmes de qualité", "unifiedDocViewer.docView.logsOverview.accordion.title.serviceInfra": "Service et Infrastructure", + "unifiedDocViewer.docView.logsOverview.accordion.title.techPreview": "PRÉVERSION TECHNIQUE", "unifiedDocViewer.docView.logsOverview.label.cloudAvailabilityZone": "Zone de disponibilité du cloud", "unifiedDocViewer.docView.logsOverview.label.cloudInstanceId": "ID d'instance du cloud", "unifiedDocViewer.docView.logsOverview.label.cloudProjectId": "ID de projet du cloud", @@ -7606,8 +8371,9 @@ "unifiedDocViewer.docView.table.ignored.singleAboveTooltip": "La valeur dans ce champ est trop longue et ne peut pas être recherchée ni filtrée.", "unifiedDocViewer.docView.table.ignored.singleMalformedTooltip": "La valeur dans ce champ est mal formée et ne peut pas être recherchée ni filtrée.", "unifiedDocViewer.docView.table.ignored.singleUnknownTooltip": "La valeur dans ce champ a été ignorée par Elasticsearch et ne peut pas être recherchée ni filtrée.", - "unifiedDocViewer.docView.table.searchPlaceHolder": "Rechercher les noms de champs", + "unifiedDocViewer.docView.table.searchPlaceHolder": "Rechercher des noms ou valeurs de champs", "unifiedDocViewer.docViews.json.jsonTitle": "JSON", + "unifiedDocViewer.docViews.table.esqlMultivalueFilteringDisabled": "Le filtrage multivalué n'est pas pris en charge dans ES|QL", "unifiedDocViewer.docViews.table.filterForFieldPresentButtonAriaLabel": "Filtrer sur le champ", "unifiedDocViewer.docViews.table.filterForFieldPresentButtonTooltip": "Filtrer sur le champ", "unifiedDocViewer.docViews.table.filterForValueButtonAriaLabel": "Filtrer sur la valeur", @@ -7628,6 +8394,8 @@ "unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "Les champs non indexés ou les valeurs ignorées ne peuvent pas être recherchés", "unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedWarningMessage": "Il est impossible d’effectuer une recherche sur des champs non indexés", "unifiedDocViewer.docViews.table.unpinFieldLabel": "Désépingler le champ", + "unifiedDocViewer.docViews.table.viewLessButton": "Afficher moins", + "unifiedDocViewer.docViews.table.viewMoreButton": "Voir plus", "unifiedDocViewer.fieldActions.copyToClipboard": "Copier dans le presse-papiers", "unifiedDocViewer.fieldActions.filterForFieldPresent": "Filtrer sur le champ", "unifiedDocViewer.fieldActions.filterForValue": "Filtrer sur la valeur", @@ -7639,15 +8407,19 @@ "unifiedDocViewer.fieldChooser.discoverField.name": "Champ", "unifiedDocViewer.fieldChooser.discoverField.value": "Valeur", "unifiedDocViewer.fieldsTable.ariaLabel": "Valeurs des champs", + "unifiedDocViewer.fieldsTable.pinControlColumnHeader": "Épingler la colonne Champ", "unifiedDocViewer.flyout.closeButtonLabel": "Fermer", "unifiedDocViewer.flyout.documentNavigation": "Navigation dans le document", "unifiedDocViewer.flyout.docViewerDetailHeading": "Document", "unifiedDocViewer.flyout.docViewerEsqlDetailHeading": "Résultat", + "unifiedDocViewer.flyout.screenReaderDescription": "Vous êtes dans une boîte de dialogue non modale. Pour fermer la boîte de dialogue, appuyez sur Échap.", "unifiedDocViewer.flyout.toastColumnAdded": "La colonne \"{columnName}\" a été ajoutée", "unifiedDocViewer.flyout.toastColumnRemoved": "La colonne \"{columnName}\" a été supprimée", + "unifiedDocViewer.hideNullValues.switchLabel": "Masquer les champs nuls", "unifiedDocViewer.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", "unifiedDocViewer.json.copyToClipboardLabel": "Copier dans le presse-papiers", "unifiedDocViewer.loadingJSON": "Chargement de JSON", + "unifiedDocViewer.showOnlySelectedFields.switchLabel": "Éléments sélectionnés uniquement", "unifiedDocViewer.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.", "unifiedDocViewer.sourceViewer.errorMessageTitle": "Une erreur s'est produite.", "unifiedDocViewer.sourceViewer.refresh": "Actualiser", @@ -7721,7 +8493,7 @@ "unifiedFieldList.useGroupedFields.emptyFieldsLabel": "Champs vides", "unifiedFieldList.useGroupedFields.emptyFieldsLabelHelp": "Champs ne possédant aucune des valeurs spécifiées dans vos filtres.", "unifiedFieldList.useGroupedFields.metaFieldsLabel": "Champs méta", - "unifiedFieldList.useGroupedFields.noAvailableDataLabel": "Aucun champ disponible ne contient de données.", + "unifiedFieldList.useGroupedFields.noAvailableDataLabel": "Aucun champ disponible contenant des données.", "unifiedFieldList.useGroupedFields.noEmptyDataLabel": "Aucun champ vide.", "unifiedFieldList.useGroupedFields.noMetaDataLabel": "Aucun champ méta.", "unifiedFieldList.useGroupedFields.popularFieldsLabel": "Champs populaires", @@ -7782,7 +8554,7 @@ "unifiedSearch.filter.filterBar.invalidDateFormatProvidedErrorMessage": "Format de date non valide fourni", "unifiedSearch.filter.filterBar.labelWarningInfo": "Le champ {fieldName} n'existe pas dans la vue en cours.", "unifiedSearch.filter.filterBar.labelWarningText": "Avertissement", - "unifiedSearch.filter.filterBar.negatedFilterPrefix": "NON ", + "unifiedSearch.filter.filterBar.negatedFilterPrefix": "NON", "unifiedSearch.filter.filterBar.pinFilterButtonLabel": "Épingler dans toutes les applications", "unifiedSearch.filter.filterBar.pinnedFilterPrefix": "Épinglé", "unifiedSearch.filter.filterBar.preview": "Aperçu {icon}", @@ -7875,9 +8647,15 @@ "unifiedSearch.optionsList.popover.sortOrder.desc": "Décroissant", "unifiedSearch.query.queryBar.clearInputLabel": "Effacer l'entrée", "unifiedSearch.query.queryBar.comboboxAriaLabel": "Rechercher et filtrer la page {pageType}", + "unifiedSearch.query.queryBar.esqlMenu.documentation": "Documentation", + "unifiedSearch.query.queryBar.esqlMenu.exampleQueries": "Requêtes recommandées", + "unifiedSearch.query.queryBar.esqlMenu.feedback": "Soumettre un commentaire", + "unifiedSearch.query.queryBar.esqlMenu.label": "Aide sur ES|QL", + "unifiedSearch.query.queryBar.esqlMenu.quickReference": "Référence rapide", + "unifiedSearch.query.queryBar.esqlMenu.switcherLabelTitle": "Vue de données", "unifiedSearch.query.queryBar.indexPattern.addFieldButton": "Ajouter un champ à cette vue de données", "unifiedSearch.query.queryBar.indexPattern.addNewDataView": "Créer une vue de données", - "unifiedSearch.query.queryBar.indexPattern.createForMatchingIndices": "Explorer {indicesLength, plural,\n one {# index correspondant}\n other {# index correspondants}}", + "unifiedSearch.query.queryBar.indexPattern.createForMatchingIndices": "Explorer {indicesLength, plural, one {# index correspondant} other {# index correspondants}}", "unifiedSearch.query.queryBar.indexPattern.dataViewsLabel": "Vues de données", "unifiedSearch.query.queryBar.indexPattern.findDataView": "Rechercher une vue de données", "unifiedSearch.query.queryBar.indexPattern.findFilterSet": "Trouver une requête", @@ -8202,6 +8980,8 @@ "visTypeMarkdown.function.help": "Visualisation Markdown", "visTypeMarkdown.function.markdown.help": "Markdown à rendre", "visTypeMarkdown.function.openLinksInNewTab.help": "Ouvre les liens dans un nouvel onglet", + "visTypeMarkdown.markdownDescription": "Ajoutez du texte et des images à votre tableau de bord.", + "visTypeMarkdown.markdownTitleInWizard": "Texte", "visTypeMarkdown.params.fontSizeLabel": "Taille de police de base en points", "visTypeMarkdown.params.helpLinkLabel": "Aide", "visTypeMarkdown.params.openLinksLabel": "Ouvrir les liens dans un nouvel onglet", @@ -8263,7 +9043,7 @@ "visTypeTable.function.args.splitColumnHelpText": "Diviser par la configuration des dimensions de colonne", "visTypeTable.function.args.splitRowHelpText": "Diviser par la configuration des dimensions de ligne", "visTypeTable.function.args.titleHelpText": "Titre de la visualisation. Le titre est utilisé comme nom de fichier par défaut pour l'exportation CSV.", - "visTypeTable.function.args.totalFuncHelpText": "Spécifie la fonction de calcul du nombre total de lignes. Les options possibles sont : ", + "visTypeTable.function.args.totalFuncHelpText": "Spécifie la fonction de calcul du nombre total de lignes. Les options possibles sont :", "visTypeTable.function.dimension.metrics": "Indicateurs", "visTypeTable.function.dimension.splitColumn": "Diviser par colonne", "visTypeTable.function.dimension.splitRow": "Diviser par ligne", @@ -8530,6 +9310,7 @@ "visTypeTimeseries.indexPatternSelect.switchModePopover.title": "Mode de vue de données", "visTypeTimeseries.indexPatternSelect.switchModePopover.useKibanaIndices": "Utiliser des vues de données Kibana", "visTypeTimeseries.indexPatternSelect.updateIndex": "Mettre à jour la visualisation avec la vue de données saisie", + "visTypeTimeseries.kbnVisTypes.metricsDescription": "Réalisez des analyses avancées de vos données temporelles.", "visTypeTimeseries.kbnVisTypes.metricsTitle": "TSVB", "visTypeTimeseries.lastValueModeIndicator.lastBucketDate": "Compartiment : {lastBucketDate}", "visTypeTimeseries.lastValueModeIndicator.lastValue": "Dernière valeur", @@ -8921,7 +9702,7 @@ "visTypeVega.vegaParser.dataExceedsSomeParamsUseTimesLimitErrorMessage": "Les données ne doivent pas avoir plus d'un paramètre {urlParam}, {valuesParam} et {sourceParam}", "visTypeVega.vegaParser.hostConfigIsDeprecatedWarningMessage": "{deprecatedConfigName} a été déclassé. Utilisez {newConfigName} à la place.", "visTypeVega.vegaParser.hostConfigValueTypeErrorMessage": "S'il est présent, le paramètre {configName} doit être un objet", - "visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage": "Vos spécifications requièrent un champ {schemaParam} avec une URL valide pour\nVega (voir {vegaSchemaUrl}) ou\nVega-Lite (voir {vegaLiteSchemaUrl}).\nL'URL est uniquement un identificateur. Kibana et votre navigateur n'accéderont jamais à cette URL.", + "visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage": "Vos spécifications requièrent un champ {schemaParam} avec une URL valide pour Vega (voir {vegaSchemaUrl}) ou Vega-Lite (voir {vegaLiteSchemaUrl}). L'URL est uniquement un identificateur. Kibana et votre navigateur n'accéderont jamais à cette URL.", "visTypeVega.vegaParser.invalidVegaSpecErrorMessage": "Spécification Vega non valide", "visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage": "S'il est présent, le paramètre {configName} doit être un objet", "visTypeVega.vegaParser.maxBoundsValueTypeWarningMessage": "{maxBoundsConfigName} doit être un tableau avec quatre nombres", @@ -9086,7 +9867,7 @@ "visualizations.confirmModal.saveDuplicateConfirmationMessage": "L'enregistrement de \"{name}\" crée un doublon de titre. Voulez-vous tout de même enregistrer ?", "visualizations.confirmModal.saveDuplicateConfirmationTitle": "Cette visualisation existe déjà", "visualizations.confirmModal.title": "Modifications non enregistrées", - "visualizations.controls.notificationMessage": "Les contrôles d'entrée sont déclassés et seront supprimés dans une prochaine version. Utilisez les nouveaux contrôles pour filtrer les données de votre tableau de bord et interagir avec elles. ", + "visualizations.controls.notificationMessage": "Les contrôles d'entrée sont déclassés et seront supprimés dans une prochaine version. Utilisez les nouveaux contrôles pour filtrer les données de votre tableau de bord et interagir avec elles.", "visualizations.createVisualization.failedToLoadErrorMessage": "Impossible de charger la visualisation", "visualizations.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "Vous devez fournir un indexPattern ou un savedSearchId", "visualizations.createVisualization.noVisTypeErrorMessage": "Vous devez fournir un type de visualisation valide", @@ -9095,6 +9876,7 @@ "visualizations.editor.createBreadcrumb": "Créer", "visualizations.editor.defaultEditBreadcrumbText": "Modifier la visualisation", "visualizations.editVisualization.readOnlyErrorMessage": "Les visualisations {visTypeTitle} sont en lecture seule et ne peuvent pas être ouvertes dans l'éditeur", + "visualizations.embeddable.errorTitle": "Impossible de charger la visualisation", "visualizations.embeddable.inspectorTitle": "Inspecteur", "visualizations.embeddable.legacyURLConflict.errorMessage": "Cette visualisation a la même URL qu'un alias hérité. Désactiver l'alias pour résoudre cette erreur : {json}", "visualizations.embeddable.placeholderTitle": "Titre de l'espace réservé", @@ -9142,6 +9924,8 @@ "visualizations.newChart.libraryMode.new": "nouveau", "visualizations.newChart.libraryMode.old": "âge", "visualizations.newGaugeChart.notificationMessage": "La nouvelle bibliothèque de graphiques de jauge ne prend pas encore en charge l'agrégation de graphiques fractionnés. {conditionalMessage}", + "visualizations.newVisWizard.aggBasedGroupDescription": "Utilisez notre bibliothèque Visualize classique pour créer des graphiques basés sur des agrégations.", + "visualizations.newVisWizard.aggBasedGroupTitle": "Basé sur une agrégation", "visualizations.newVisWizard.chooseSourceTitle": "Choisir une source", "visualizations.newVisWizard.filterVisTypeAriaLabel": "Filtrer un type de visualisation", "visualizations.newVisWizard.goBackLink": "Sélectionner une visualisation différente", @@ -9152,6 +9936,7 @@ "visualizations.newVisWizard.searchSelection.notFoundLabel": "Aucun recherche enregistrée ni aucun index correspondants n'ont été trouvés.", "visualizations.newVisWizard.searchSelection.savedObjectType.dataView": "Vue de données", "visualizations.newVisWizard.searchSelection.savedObjectType.search": "Recherche enregistrée", + "visualizations.newVisWizard.title": "Nouvelle visualisation", "visualizations.noDataView.label": "vue de données", "visualizations.noMatchRoute.bannerText": "L'application Visualize ne reconnaît pas cet itinéraire : {route}.", "visualizations.noMatchRoute.bannerTitleText": "Page introuvable", @@ -9199,6 +9984,7 @@ "visualizations.visualizeListingDashboardAppName": "Application Tableau de bord", "visualizations.visualizeListingDeleteErrorTitle": "Erreur lors de la suppression de la visualisation", "visualizations.visualizeListingDeleteErrorTitle.duplicateWarning": "L'enregistrement de \"{value}\" crée un doublon de titre.", + "visualizations.visualizeSavedObjectName": "Visualisation", "visualizationUiComponents.colorPicker.seriesColor.label": "Couleur de la série", "visualizationUiComponents.colorPicker.tooltip.auto": "Lens choisit automatiquement des couleurs à votre place sauf si vous spécifiez une couleur personnalisée.", "visualizationUiComponents.colorPicker.tooltip.custom": "Effacez la couleur personnalisée pour revenir au mode \"Auto\".", @@ -9257,8 +10043,9 @@ "xpack.actions.availableConnectorFeatures.compatibility.alertingRules": "Règles d'alerting", "xpack.actions.availableConnectorFeatures.compatibility.cases": "Cas", "xpack.actions.availableConnectorFeatures.compatibility.generativeAIForObservability": "IA générative pour l'observabilité", - "xpack.actions.availableConnectorFeatures.compatibility.generativeAIForSearchPlayground": "L'IA générative pour Search Playground", + "xpack.actions.availableConnectorFeatures.compatibility.generativeAIForSearchPlayground": "IA générative pour Search", "xpack.actions.availableConnectorFeatures.compatibility.generativeAIForSecurity": "IA générative pour la sécurité", + "xpack.actions.availableConnectorFeatures.compatibility.securitySolution": "Solution de sécurité", "xpack.actions.availableConnectorFeatures.securitySolution": "Solution de sécurité", "xpack.actions.availableConnectorFeatures.uptime": "Uptime", "xpack.actions.builtin.cases.jiraTitle": "Jira", @@ -9278,9 +10065,10 @@ "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "Les actions sont indisponibles - les informations de licence ne sont pas disponibles actuellement.", "xpack.actions.subActionsFramework.urlValidationError": "Erreur lors de la validation de l'URL : {message}", "xpack.actions.urlAllowedHostsConfigurationError": "Le {field} cible \"{value}\" n'est pas ajouté à la configuration Kibana xpack.actions.allowedHosts", + "xpack.aiAssistant.aiAssistantLabel": "Assistant d'intelligence artificielle", "xpack.aiAssistant.askAssistantButton.buttonLabel": "Demander à l'assistant", "xpack.aiAssistant.askAssistantButton.popoverContent": "Obtenez des informations relatives à vos données grâce à l'assistant d'Elastic", - "xpack.aiAssistant.assistantSetup.title": "Bienvenue sur l'assistant d'intelligence artificielle d'Elastic", + "xpack.aiAssistant.assistantSetup.title": "Bienvenue sur Elastic AI Assistant", "xpack.aiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "Menu", "xpack.aiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "Plus d'actions", "xpack.aiAssistant.chatCollapsedItems.hideEvents": "Masquer {count} événements", @@ -9288,6 +10076,7 @@ "xpack.aiAssistant.chatCollapsedItems.toggleButtonLabel": "Afficher/masquer les éléments", "xpack.aiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "Développer la liste des conversations", "xpack.aiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "Nouveau chat", + "xpack.aiAssistant.chatFlyout.euiFlyoutResizable.aiAssistantLabel": "Menu volant Chat de l'assistant d'IA", "xpack.aiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "Réduire la liste des conversations", "xpack.aiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "Développer la liste des conversations", "xpack.aiAssistant.chatFlyout.euiToolTip.newChatLabel": "Nouveau chat", @@ -9312,6 +10101,10 @@ "xpack.aiAssistant.chatTimeline.messages.system.label": "Système", "xpack.aiAssistant.chatTimeline.messages.user.label": "Vous", "xpack.aiAssistant.checkingKbAvailability": "Vérification de la disponibilité de la base de connaissances", + "xpack.aiAssistant.conversationList.deleteConversationIconLabel": "Supprimer", + "xpack.aiAssistant.conversationList.errorMessage": "Échec de chargement", + "xpack.aiAssistant.conversationList.noConversations": "Aucune conversation", + "xpack.aiAssistant.conversationList.title": "Précédemment", "xpack.aiAssistant.conversationStartTitle": "a démarré une conversation", "xpack.aiAssistant.couldNotFindConversationContent": "Impossible de trouver une conversation avec l'ID {conversationId}. Assurez-vous que la conversation existe et que vous y avez accès.", "xpack.aiAssistant.couldNotFindConversationTitle": "Conversation introuvable", @@ -9351,7 +10144,7 @@ "xpack.aiAssistant.technicalPreviewBadgeDescription": "GTP4 est nécessaire pour bénéficier d'une meilleure expérience avec les appels de fonctions (par exemple lors de la réalisation d'analyse de la cause d'un problème, de la visualisation de données et autres). GPT3.5 peut fonctionner pour certains des workflows les plus simples comme les explications d'erreurs ou pour bénéficier d'une expérience comparable à ChatGPT au sein de Kibana à partir du moment où les appels de fonctions ne sont pas fréquents.", "xpack.aiAssistant.userExecutedFunctionEvent": "a exécuté la fonction {functionName}", "xpack.aiAssistant.userSuggestedFunctionEvent": "a demandé la fonction {functionName}", - "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} ou vérifiez {trainedModelsLink} pour vous assurer que {modelName} est déployé et en cours d'exécution.", + "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": "{retryInstallingLink} ou vérifiez {trainedModelsLink} pour vous assurer que {modelName} est déployé et en cours d'exécution.", "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "Configuration de la base de connaissances", "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "Inspecter les problèmes", "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "Problèmes", @@ -9367,9 +10160,19 @@ "xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "Votre base de connaissances n'a pas été configurée.", "xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "Réessayer l'installation", "xpack.aiops.actions.openChangePointInMlAppName": "Ouvrir dans AIOps Labs", + "xpack.aiops.analysis.analysisTypeDipFallbackInfoTitle": "Meilleurs éléments pour la plage temporelle de référence de base", + "xpack.aiops.analysis.analysisTypeDipInfoContent": "Le taux de log médian pour la plage temporelle d'écart-type sélectionnée est inférieur à la référence de base. Le tableau des résultats de l'analyse présente donc des éléments statistiquement significatifs inclus dans la plage temporelle de base qui sont moins nombreux ou manquant dans la plage temporelle d'écart-type. La colonne \"doc count\" (décompte de documents) renvoie à la quantité de documents dans la plage temporelle de base.", + "xpack.aiops.analysis.analysisTypeDipInfoContentFallback": "La plage temporelle de déviation ne contient aucun document. Les résultats montrent donc les catégories de message des meilleurs logs et les valeurs des champs pour la période de référence.", + "xpack.aiops.analysis.analysisTypeDipInfoTitle": "Baisse du taux de log", + "xpack.aiops.analysis.analysisTypeInfoTitlePrefix": "Type d'analyse :", + "xpack.aiops.analysis.analysisTypeSpikeFallbackInfoTitle": "Meilleurs éléments pour la plage temporelle de déviation", + "xpack.aiops.analysis.analysisTypeSpikeInfoContent": "Le taux de log médian pour la plage temporelle d'écart-type sélectionnée est inférieur à la référence de base. Le tableau des résultats de l'analyse présente donc des éléments statistiquement significatifs inclus dans la plage temporelle d'écart-type, qui contribuent au pic. La colonne \"doc count\" (décompte de documents) renvoie à la quantité de documents dans la plage temporelle d'écart-type.", + "xpack.aiops.analysis.analysisTypeSpikeInfoContentFallback": "La plage temporelle de référence de base ne contient aucun document. Les résultats montrent donc les catégories de message des meilleurs logs et les valeurs des champs pour la plage temporelle de déviation.", + "xpack.aiops.analysis.analysisTypeSpikeInfoTitle": "Pic du taux de log", "xpack.aiops.analysis.columnSelectorAriaLabel": "Filtrer les colonnes", "xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected": "Au moins une colonne doit être sélectionnée.", "xpack.aiops.analysis.errorCallOutTitle": "Génération {errorCount, plural, one {de l'erreur suivante} other {des erreurs suivantes}} au cours de l'analyse.", + "xpack.aiops.analysis.fieldsButtonLabel": "Champs", "xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected": "Le regroupement nécessite la sélection d'au moins 2 champs.", "xpack.aiops.analysis.fieldSelectorPlaceholder": "Recherche", "xpack.aiops.analysisCompleteLabel": "Analyse terminée", @@ -9383,7 +10186,7 @@ "xpack.aiops.changePointDetection.actions.filterOutValueAction": "Exclure la valeur", "xpack.aiops.changePointDetection.actionsColumn": "Actions", "xpack.aiops.changePointDetection.addButtonLabel": "Ajouter", - "xpack.aiops.changePointDetection.aggregationIntervalTitle": "Intervalle d'agrégation : ", + "xpack.aiops.changePointDetection.aggregationIntervalTitle": "Intervalle d'agrégation :", "xpack.aiops.changePointDetection.applyTimeRangeLabel": "Appliquer la plage temporelle", "xpack.aiops.changePointDetection.attachChartsLabel": "Attacher les graphiques", "xpack.aiops.changePointDetection.attachmentTitle": "Point de modification : {function}({metric}){splitBy}", @@ -9429,7 +10232,7 @@ "xpack.aiops.changePointDetection.selectMetricFieldLabel": "Champ d'indicateur", "xpack.aiops.changePointDetection.selectSpitFieldLabel": "Diviser le champ", "xpack.aiops.changePointDetection.spikeDescription": "Un pic significatif existe au niveau de ce point.", - "xpack.aiops.changePointDetection.splitByTitle": " diviser par \"{splitField}\"", + "xpack.aiops.changePointDetection.splitByTitle": "diviser par \"{splitField}\"", "xpack.aiops.changePointDetection.stepChangeDescription": "La modification indique une hausse ou une baisse statistiquement significative dans la distribution des valeurs.", "xpack.aiops.changePointDetection.submitDashboardAttachButtonLabel": "Attacher", "xpack.aiops.changePointDetection.timeColumn": "Heure", @@ -9460,6 +10263,15 @@ "xpack.aiops.embeddableChangePointChart.viewTypeSelector.chartsLabel": "Graphiques", "xpack.aiops.embeddableChangePointChart.viewTypeSelector.tableLabel": "Tableau", "xpack.aiops.embeddableChangePointChartDisplayName": "Modifier la détection du point", + "xpack.aiops.embeddablePatternAnalysis.attachmentTitle": "Analyse du modèle : {fieldName}", + "xpack.aiops.embeddablePatternAnalysis.config.applyAndCloseLabel": "Appliquer et fermer", + "xpack.aiops.embeddablePatternAnalysis.config.applyFlyoutAriaLabel": "Appliquer les modifications", + "xpack.aiops.embeddablePatternAnalysis.config.cancelButtonLabel": "Annuler", + "xpack.aiops.embeddablePatternAnalysis.config.dataViewLabel": "Vue de données", + "xpack.aiops.embeddablePatternAnalysis.config.dataViewSelectorPlaceholder": "Sélectionner la vue de données", + "xpack.aiops.embeddablePatternAnalysis.config.title.edit": "Modifier l'analyse des modèles", + "xpack.aiops.embeddablePatternAnalysis.config.title.new": "Créer une analyse de modèle", + "xpack.aiops.embeddablePatternAnalysisDisplayName": "Analyse du modèle", "xpack.aiops.fieldContextPopover.descriptionTooltipContent": "Afficher les principales valeurs de champ", "xpack.aiops.fieldContextPopover.descriptionTooltipLogPattern": "La valeur du champ pour ce champ montre un exemple du modèle de champ de texte important identifié.", "xpack.aiops.fieldContextPopover.notTopTenValueMessage": "Le terme sélectionné n'est pas dans le top 10", @@ -9482,11 +10294,15 @@ "xpack.aiops.logCategorization.counts": "{count} {count, plural, one {Modèle trouvé} other {Modèles trouvés}}", "xpack.aiops.logCategorization.embeddableMenu.aria": "Options d'analyse de modèles", "xpack.aiops.logCategorization.embeddableMenu.minimumTimeRange.tooltip": "Ajoute une plage temporelle plus large à l’analyse afin d’améliorer la précision du modèle.", + "xpack.aiops.logCategorization.embeddableMenu.minimumTimeRangeOptionsRowAriaLabel": "Sélectionnez une plage temporelle minimale", "xpack.aiops.logCategorization.embeddableMenu.minimumTimeRangeOptionsRowLabel": "Plage temporelle minimale", - "xpack.aiops.logCategorization.embeddableMenu.patternAnalysisSettingsTitle": " Paramètres d’analyse du modèle", + "xpack.aiops.logCategorization.embeddableMenu.patternAnalysisSettingsTitle": "Paramètres d’analyse du modèle", "xpack.aiops.logCategorization.embeddableMenu.selectedFieldRowLabel": "Champ sélectionné", + "xpack.aiops.logCategorization.embeddableMenu.textFieldWarning.title": "La vue de données sélectionnée ne contient aucun champ de texte.", + "xpack.aiops.logCategorization.embeddableMenu.textFieldWarning.title.description": "L'analyse de modèle ne peut être exécutée que sur des vues de données comportant un champ de texte.", "xpack.aiops.logCategorization.embeddableMenu.tooltip": "Options", "xpack.aiops.logCategorization.embeddableMenu.totalPatternsMessage": "Modèles totaux dans {minimumTimeRangeOption} : {categoryCount}", + "xpack.aiops.logCategorization.embeddableMenu.totalPatternsMessage2": "Aucun temps supplémentaire ne sera ajouté à la plage que vous avez spécifiée avec le sélecteur de temps.", "xpack.aiops.logCategorization.emptyPromptBody": "L'analyse de modèle de log regroupe les messages dans des modèles courants.", "xpack.aiops.logCategorization.emptyPromptTitle": "Sélectionner un champ de texte et cliquer sur exécuter l'analyse du modèle pour lancer l'analyse", "xpack.aiops.logCategorization.errorLoadingCategories": "Erreur lors du chargement des catégories", @@ -9499,6 +10315,11 @@ "xpack.aiops.logCategorization.filterOut": "Exclure {values, plural, one {modèle} other {modèles}} dans Discover", "xpack.aiops.logCategorization.flyout.filterIn": "Filtrer sur {values, plural, one {modèle} other {modèles}}", "xpack.aiops.logCategorization.flyout.filterOut": "Exclure {values, plural, one {modèle} other {modèles}}", + "xpack.aiops.logCategorization.minimumTimeRange.1month": "1 mois", + "xpack.aiops.logCategorization.minimumTimeRange.1week": "1 semaine", + "xpack.aiops.logCategorization.minimumTimeRange.3months": "3 mois", + "xpack.aiops.logCategorization.minimumTimeRange.6months": "6 mois", + "xpack.aiops.logCategorization.minimumTimeRange.noMin": "Utiliser la plage spécifiée dans le sélecteur de temps", "xpack.aiops.logCategorization.noCategoriesBody": "Assurez-vous que le champ sélectionné est rempli dans la plage temporelle sélectionnée.", "xpack.aiops.logCategorization.noCategoriesTitle": "Aucun modèle n'a été trouvé", "xpack.aiops.logCategorization.noDocsBody": "Assurez-vous que la plage temporelle sélectionnée contient des documents.", @@ -9520,13 +10341,16 @@ "xpack.aiops.logCategorization.randomSamplerSettingsPopUp.randomSamplerPercentageRowLabel": "Pourcentage d'échantillonnage", "xpack.aiops.logCategorization.randomSamplerSettingsPopUp.randomSamplerRowLabel": "Échantillonnage aléatoire", "xpack.aiops.logCategorization.runButton": "Exécuter l'analyse du modèle", - "xpack.aiops.logCategorization.selectedCounts": " | {count} sélectionné(s)", + "xpack.aiops.logCategorization.selectedCounts": "| {count} sélectionné(s)", "xpack.aiops.logCategorization.selectedResultsButtonLabel": "Sélectionné", "xpack.aiops.logCategorization.tabs.bucket": "Compartiment", "xpack.aiops.logCategorization.tabs.bucket.tooltip": "Modèles apparaissant dans le compartiment anormal.", "xpack.aiops.logCategorization.tabs.fullTimeRange": "Plage temporelle entière", "xpack.aiops.logCategorization.tabs.fullTimeRange.tooltip": "Modèles apparaissant dans la plage temporelle choisie pour la page.", "xpack.aiops.logCategorizationTimeSeriesWarning.description": "L'analyse du modèle de log ne fonctionne que sur des index temporels.", + "xpack.aiops.logRateAnalysis.fieldCandidates.ecsIdentifiedMessage": "Les documents sources ont été identifiés comme étant conformes à ECS.", + "xpack.aiops.logRateAnalysis.fieldCandidates.fieldsDropdownHintMessage": "Utilisez le menu déroulant \"Champs\" pour modifier la sélection.", + "xpack.aiops.logRateAnalysis.fieldCandidates.fieldsSelectedMessage": "{selectedItemsCount} champs sur {allItemsCount} ont été présélectionnés pour l'analyse.", "xpack.aiops.logRateAnalysis.loadingState.doneMessage": "Terminé.", "xpack.aiops.logRateAnalysis.loadingState.groupingResults": "Transformation de paires champ/valeur significatives en groupes.", "xpack.aiops.logRateAnalysis.loadingState.identifiedFieldCandidates": "{fieldCandidatesCount, plural, one {# candidat de champ identifié} other {# candidats de champs identifiés}}.", @@ -9544,7 +10368,7 @@ "xpack.aiops.logRateAnalysis.page.emptyPromptBody": "La fonction d'analyse des pics de taux de log identifie les combinaisons champ/valeur statistiquement significatives qui contribuent à un pic ou une baisse de taux de log.", "xpack.aiops.logRateAnalysis.page.emptyPromptTitle": "Commencez par cliquer sur un pic ou une baisse dans l'histogramme.", "xpack.aiops.logRateAnalysis.page.fieldFilterApplyButtonLabel": "Appliquer", - "xpack.aiops.logRateAnalysis.page.fieldFilterHelpText": "Désélectionnez les champs non pertinents pour les supprimer des groupes et cliquez sur le bouton Appliquer pour réexécuter le regroupement. Utilisez la barre de recherche pour filtrer la liste, puis sélectionnez/désélectionnez plusieurs champs avec les actions ci-dessous.", + "xpack.aiops.logRateAnalysis.page.fieldFilterHelpText": "Désélectionnez les champs non pertinents pour les supprimer de l'analyse et cliquez sur le bouton Appliquer pour réexécuter l'analyse. Utilisez la barre de recherche pour filtrer la liste, puis sélectionnez/désélectionnez plusieurs champs avec les actions ci-dessous.", "xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllItems": "Tout désélectionner", "xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllSearchedItems": "Désélectionner les éléments filtrés", "xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllItems": "Tout sélectionner", @@ -9605,11 +10429,15 @@ "xpack.aiops.logRateAnalysis.resultsTableGroups.impactLabelColumnTooltip": "Niveau d'impact du groupe sur la différence de taux de messages", "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateChangeLabelColumnTooltip": "Le facteur par lequel le taux de journalisation a changé. Cette valeur est normalisée afin de tenir compte des différentes longueurs des plages temporelles de référence et d’écart.", "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateColumnTooltip": "Représentation visuelle de l'impact du groupe sur la différence de taux de messages.", - "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateDocDecreaseLabel": "les documents descendent jusqu'à 0 de {baselineBucketRate} au niveau de référence", - "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateDocIncreaseLabel": "{deviationBucketRate} {deviationBucketRate, plural, one {doc} other {docs}} remontent de 0 au niveau de référence", + "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateDocDecreaseLabel": "jusqu'à 0 depuis {baselineBucketRate} au niveau de référence", + "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateDocIncreaseLabel": "jusqu'à {deviationBucketRate} depuis 0 au niveau de référence", + "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateFactorDecreaseLabel": "{roundedFactor} fois inférieur", + "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateFactorIncreaseLabel": "{roundedFactor} fois supérieur", "xpack.aiops.logRateAnalysisTimeSeriesWarning.description": "L'analyse des taux de log ne fonctionne que sur des index temporels.", "xpack.aiops.miniHistogram.noDataLabel": "S. O.", + "xpack.aiops.navMenu.mlAppNameText": "Machine Learning et Analytique", "xpack.aiops.observabilityAIAssistantContextualInsight.logRateAnalysisTitle": "Causes possibles et résolutions", + "xpack.aiops.patternAnalysis.typeDisplayName": "analyse du modèle", "xpack.aiops.progressAriaLabel": "Progression", "xpack.aiops.progressTitle": "Progression : {progress} % — {progressMessage}", "xpack.aiops.rerunAnalysisButtonTitle": "Lancer l'analyse", @@ -9788,7 +10616,7 @@ "xpack.alerting.rulesClient.validateActions.actionsWithInvalidThrottles": "La fréquence de l'action ne peut pas être inférieure à l'intervalle de planification de {scheduleIntervalText} : {groups}", "xpack.alerting.rulesClient.validateActions.actionsWithInvalidTimeRange": "La plage temporelle du filtre d'alertes de l'action a une valeur non valide : {hours}", "xpack.alerting.rulesClient.validateActions.actionWithInvalidTimeframe": "La durée du filtre d'alertes de l'action a des champs manquants : jours, heures ou fuseau horaire : {uuids}", - "xpack.alerting.rulesClient.validateActions.errorSummary": "Impossible de valider les actions en raison {errorNum, plural, one {de l'erreur suivante :} other {des # erreurs suivantes :\n-}} {errorList}", + "xpack.alerting.rulesClient.validateActions.errorSummary": "Impossible de valider les actions en raison {errorNum, plural, one {de l'erreur suivante :} other {des # erreurs suivantes : -}} {errorList}", "xpack.alerting.rulesClient.validateActions.hasDuplicatedUuid": "Les actions ont des UUID en double", "xpack.alerting.rulesClient.validateActions.invalidGroups": "Groupes d'actions non valides : {groups}", "xpack.alerting.rulesClient.validateActions.misconfiguredConnector": "Connecteurs non valides : {groups}", @@ -9815,27 +10643,29 @@ "xpack.alerting.taskRunner.warning.maxQueuedActions": "Le nombre maximal d'actions en file d'attente a été atteint ; les actions excédentaires n'ont pas été déclenchées.", "xpack.apm..breadcrumb.apmLabel": "APM", "xpack.apm.a.thresholdMet": "Seuil atteint", + "xpack.apm.add.apm.agent.button.": "Ajouter un APM", "xpack.apm.addDataButtonLabel": "Ajouter des données", + "xpack.apm.addDataContextMenu.link": "Ajouter des données", "xpack.apm.agent_explorer.error.missing_configuration": "Pour utiliser la toute dernière version de l’agent, vous devez définir xpack.apm.latestAgentVersionsUrl.", "xpack.apm.agentConfig.allOptionLabel": "Tous", - "xpack.apm.agentConfig.apiRequestSize.description": "Taille totale compressée maximale du corps de la requête envoyé à l'API d'ingestion du serveur APM depuis un encodage fragmenté (diffusion HTTP).\nVeuillez noter qu'un léger dépassement est possible.\n\nLes unités d'octets autorisées sont `b`, `kb` et `mb`. `1kb` correspond à `1024b`.", + "xpack.apm.agentConfig.apiRequestSize.description": "Taille totale compressée maximale du corps de la requête envoyé à l'API d'ingestion du serveur APM depuis un encodage fragmenté (diffusion HTTP). Veuillez noter qu'un léger dépassement est possible. Les unités d'octets autorisées sont `b`, `kb` et `mb`. `1kb` correspond à `1024b`.", "xpack.apm.agentConfig.apiRequestSize.label": "Taille de la requête API", - "xpack.apm.agentConfig.apiRequestTime.description": "Durée maximale de l'ouverture d'une requête HTTP sur le serveur APM.\n\nREMARQUE : cette valeur doit être inférieure à celle du paramètre `read_timeout` du serveur APM.", + "xpack.apm.agentConfig.apiRequestTime.description": "Durée maximale de l'ouverture d'une requête HTTP sur le serveur APM. REMARQUE : cette valeur doit être inférieure à celle du paramètre `read_timeout` du serveur APM.", "xpack.apm.agentConfig.apiRequestTime.label": "Heure de la requête API", - "xpack.apm.agentConfig.applicationPackages.description": "Permet de déterminer si un cadre de trace de pile est un cadre dans l'application ou un cadre de bibliothèque. Cela permet à l'application APM de réduire les cadres de pile du code de la bibliothèque et de mettre en surbrillance les cadres de pile qui proviennent de votre application. Plusieurs packages racine peuvent être définis sous forme de liste séparée par des virgules ; il n'est pas nécessaire de configurer des sous-packages. Étant donné que ce paramètre aide à déterminer les classes à analyser au démarrage, la définition de cette option peut également améliorer le temps de démarrage.\n\nVous devez définir cette option afin d'utiliser les annotations d'API `@CaptureTransaction` et `@CaptureSpan`.", + "xpack.apm.agentConfig.applicationPackages.description": "Permet de déterminer si un cadre de trace de pile est un cadre dans l'application ou un cadre de bibliothèque. Cela permet à l'application APM de réduire les cadres de pile du code de la bibliothèque et de mettre en surbrillance les cadres de pile qui proviennent de votre application. Plusieurs packages racine peuvent être définis sous forme de liste séparée par des virgules ; il n'est pas nécessaire de configurer des sous-packages. Étant donné que ce paramètre aide à déterminer les classes à analyser au démarrage, la définition de cette option peut également améliorer le temps de démarrage. Vous devez définir cette option afin d'utiliser les annotations d'API `@CaptureTransaction` et `@CaptureSpan`.", "xpack.apm.agentConfig.applicationPackages.label": "Packages de l'application", - "xpack.apm.agentConfig.captureBody.description": "Pour les transactions qui sont des requêtes HTTP, l'agent peut éventuellement capturer le corps de la requête (par ex., variables POST).\nPour les transactions qui sont initiées par la réception d'un message depuis un agent de message, l'agent peut capturer le corps du message texte.", + "xpack.apm.agentConfig.captureBody.description": "Pour les transactions qui sont des requêtes HTTP, l'agent peut éventuellement capturer le corps de la requête (par ex., variables POST). Pour les transactions qui sont initiées par la réception d'un message depuis un agent de message, l'agent peut capturer le corps du message texte.", "xpack.apm.agentConfig.captureBody.label": "Capturer le corps", - "xpack.apm.agentConfig.captureBodyContentTypes.description": "Configure les types de contenu qui doivent être enregistrés.\n\nLes valeurs par défaut se terminent par un caractère générique afin que les types de contenu tels que `text/plain; charset=utf-8` soient également capturés.", + "xpack.apm.agentConfig.captureBodyContentTypes.description": "Configure les types de contenu qui doivent être enregistrés. Les valeurs par défaut se terminent par un caractère générique afin que les types de contenu tels que `text/plain; charset=utf-8` soient également capturés.", "xpack.apm.agentConfig.captureBodyContentTypes.label": "Capturer les types de contenu du corps", - "xpack.apm.agentConfig.captureHeaders.description": "Si cette option est définie sur `true`, l'agent capturera les en-têtes de la requête HTTP et de la réponse (y compris les cookies), ainsi que les en-têtes/les propriétés du message lors de l'utilisation de frameworks de messagerie (tels que Kafka).\n\nREMARQUE : Si `false` est défini, cela permet de réduire la bande passante du réseau, l'espace disque et les allocations d'objets.", + "xpack.apm.agentConfig.captureHeaders.description": "Si cette option est définie sur `true`, l'agent capturera les en-têtes de la requête HTTP et de la réponse (y compris les cookies), ainsi que les en-têtes/les propriétés du message lors de l'utilisation de frameworks de messagerie (tels que Kafka). REMARQUE : Si `false` est défini, cela permet de réduire la bande passante du réseau, l'espace disque et les allocations d'objets.", "xpack.apm.agentConfig.captureHeaders.label": "Capturer les en-têtes", - "xpack.apm.agentConfig.captureJmxMetrics.description": "Enregistrer les indicateurs de JMX sur le serveur APM\n\nPeut contenir plusieurs définitions d'indicateurs JMX séparées par des virgules :\n\n`object_name[] attribute[:metric_name=]`\n\nPour en savoir plus, consultez la documentation de l'agent Java.", + "xpack.apm.agentConfig.captureJmxMetrics.description": "Les indicateurs du rapport de JMX vers le serveur APM peuvent contenir plusieurs définitions d’indicateurs JMX séparés par des virgules : `object_name[] attribute[:metric_name=]` Consultez la documentation de l'agent Java pour plus de détails.", "xpack.apm.agentConfig.captureJmxMetrics.label": "Capturer les indicateurs JMX", "xpack.apm.agentConfig.chooseService.editButton": "Modifier", "xpack.apm.agentConfig.chooseService.service.environment.label": "Environnement", "xpack.apm.agentConfig.chooseService.service.name.label": "Nom de service", - "xpack.apm.agentConfig.circuitBreakerEnabled.description": "Nombre booléen spécifiant si le disjoncteur doit être activé ou non. Lorsqu'il est activé, l'agent interroge régulièrement les monitorings de tension pour détecter l'état de tension du système/du processus/de la JVM. Si L'UN des monitorings détecte un signe de tension, l'agent s'interrompt, comme si l'option de configuration `recording` était définie sur `false`, réduisant ainsi la consommation des ressources au minimum. Pendant l'interruption, l'agent continue à interroger les mêmes monitorings pour vérifier si l'état de tension a été allégé. Si TOUS les monitorings indiquent que le système, le processus et la JVM ne sont plus en état de tension, l'agent reprend son activité et redevient entièrement fonctionnel.", + "xpack.apm.agentConfig.circuitBreakerEnabled.description": "Nombre booléen spécifiant si le disjoncteur doit être activé ou non. Lorsqu'il est activé, l'agent interroge régulièrement les monitorings de tension pour détecter l'état de tension du système/du processus/de la JVM. Si L'UN des monitorings détecte un signe de tension, l'agent s'interrompt, comme si l'option de configuration `recording` était définie sur `false`, réduisant ainsi la consommation des ressources au minimum. Pendant l'interruption, l'agent continue à interroger les mêmes monitorings pour vérifier si l'état de tension a été allégé. Si TOUS les monitorings indiquent que le système, le processus et la JVM ne sont plus en état de tension, l'agent reprend son activité et redevient entièrement fonctionnel.", "xpack.apm.agentConfig.circuitBreakerEnabled.label": "Disjoncteur activé", "xpack.apm.agentConfig.configTable.appliedTooltipMessage": "Appliqué par au moins un agent", "xpack.apm.agentConfig.configTable.configTable.failurePromptText": "La liste des configurations d'agent n'a pas pu être récupérée. Votre utilisateur ne dispose peut-être pas d'autorisations suffisantes.", @@ -9851,7 +10681,7 @@ "xpack.apm.agentConfig.context_propagation_only.label": "Propagation du contexte seulement", "xpack.apm.agentConfig.createConfigButtonLabel": "Créer une configuration", "xpack.apm.agentConfig.createConfigTitle": "Créer une configuration", - "xpack.apm.agentConfig.dedotCustomMetrics.description": "Remplace les points par des traits de soulignement dans les noms des indicateurs personnalisés.\n\nAVERTISSEMENT : L'attribution de la valeur `false` peut entraîner des conflits de mapping car les points indiquent une imbrication dans Elasticsearch.\nUn tel conflit peut se produire par exemple entre deux indicateurs si l'un se nomme `foo` et l'autre `foo.bar`.\nLe premier mappe `foo` sur un nombre, et le second indicateur mappe `foo` en tant qu'objet.", + "xpack.apm.agentConfig.dedotCustomMetrics.description": "Remplace les points par des traits de soulignement dans les noms des indicateurs personnalisés. AVERTISSEMENT : L'attribution de la valeur `false` peut entraîner des conflits de mapping car les points indiquent une imbrication dans Elasticsearch. Un tel conflit peut se produire par exemple entre deux indicateurs si l'un se nomme `foo` et l'autre `foo.bar`. Le premier mappe `foo` sur un nombre, et le second indicateur mappe `foo` en tant qu'objet.", "xpack.apm.agentConfig.dedotCustomMetrics.label": "Retirer les points des indicateurs personnalisés", "xpack.apm.agentConfig.deleteModal.cancel": "Annuler", "xpack.apm.agentConfig.deleteModal.confirm": "Supprimer", @@ -9861,30 +10691,30 @@ "xpack.apm.agentConfig.deleteSection.deleteConfigFailedTitle": "La configuration n'a pas pu être supprimée", "xpack.apm.agentConfig.deleteSection.deleteConfigSucceededText": "Vous avez supprimé une configuration de \"{serviceName}\". La propagation jusqu'aux agents pourra prendre un certain temps.", "xpack.apm.agentConfig.deleteSection.deleteConfigSucceededTitle": "La configuration a été supprimée", - "xpack.apm.agentConfig.disableInstrumentations.description": "Liste séparée par des virgules de modules pour lesquels désactiver l'instrumentation.\nLorsque l'instrumentation est désactivée pour un module, aucun intervalle n'est collecté pour ce module.\n\nLa liste à jour des modules pour lesquels l'instrumentation peut être désactivée est spécifique du langage et peut être trouvée en cliquant sur les liens suivants : [Java](https://www.elastic.co/guide/en/apm/agent/java/current/config-core.html#config-disable-instrumentations)", + "xpack.apm.agentConfig.disableInstrumentations.description": "Liste séparée par des virgules de modules pour lesquels désactiver l'instrumentation. Lorsque l'instrumentation est désactivée pour un module, aucun intervalle n'est collecté pour ce module. La liste à jour des modules pour lesquels l'instrumentation peut être désactivée est spécifique du langage et peut être trouvée en cliquant sur les liens suivants : [Java](https://www.elastic.co/guide/en/apm/agent/java/current/config-core.html#config-disable-instrumentations)", "xpack.apm.agentConfig.disableInstrumentations.label": "Désactiver les instrumentations", - "xpack.apm.agentConfig.disableOutgoingTracecontextHeaders.description": "Utilisez cette option pour désactiver l'injection d'en-têtes `tracecontext` dans une communication sortante.\n\nAVERTISSEMENT : La désactivation de l'injection d'en-têtes `tracecontext` signifie que le traçage distribué ne fonctionnera pas sur les services en aval.", + "xpack.apm.agentConfig.disableOutgoingTracecontextHeaders.description": "Utilisez cette option pour désactiver l'injection d'en-têtes `tracecontext` dans une communication sortante. AVERTISSEMENT : La désactivation de l'injection d'en-têtes `tracecontext` signifie que le traçage distribué ne fonctionnera pas sur les services en aval.", "xpack.apm.agentConfig.disableOutgoingTracecontextHeaders.label": "Désactiver les en-têtes tracecontext sortants", "xpack.apm.agentConfig.editConfigTitle": "Modifier la configuration", - "xpack.apm.agentConfig.enableExperimentalInstrumentations.description": "Indique s'il faut appliquer des instrumentations expérimentales.\n\nREMARQUE : Le fait de modifier cette valeur au moment de l'exécution peut ralentir temporairement l'application. Définir cette valeur sur true active les instrumentations dans le groupe expérimental.", + "xpack.apm.agentConfig.enableExperimentalInstrumentations.description": "Indique s'il faut appliquer des instrumentations expérimentales. REMARQUE : Le fait de modifier cette valeur au moment de l'exécution peut ralentir temporairement l'application. Définir cette valeur sur true active les instrumentations dans le groupe expérimental.", "xpack.apm.agentConfig.enableExperimentalInstrumentations.label": "Activer les instrumentations expérimentales", - "xpack.apm.agentConfig.enableInstrumentations.description": "Une liste des instrumentations qui doivent être activées de façon sélective. Les options valides sont indiquées dans la [documentation de l’agent Java APM](https://www.elastic.co/guide/en/apm/agent/java/current/config-core.html#config-disable-instrumentations).\n\nLorsqu'une valeur non vide est définie, seules les instrumentations répertoriées sont activées si elles ne sont pas désactivées via `disable_instrumentations` ou `enable_experimental_instrumentations`.\nLorsque cette option n'est pas définie ou est vide (par défaut), toutes les instrumentations activées par défaut sont activées, sauf si elles sont désactivées via `disable_instrumentations` ou `enable_experimental_instrumentations`.", + "xpack.apm.agentConfig.enableInstrumentations.description": "Une liste des instrumentations qui doivent être activées de façon sélective. Les options valides sont indiquées dans la [documentation de l’agent Java APM](https://www.elastic.co/guide/en/apm/agent/java/current/config-core.html#config-disable-instrumentations). Lorsqu'une valeur non vide est définie, seules les instrumentations répertoriées sont activées si elles ne sont pas désactivées via `disable_instrumentations` ou `enable_experimental_instrumentations`. Lorsque cette option n'est pas définie ou est vide (par défaut), toutes les instrumentations activées par défaut sont activées, sauf si elles sont désactivées via `disable_instrumentations` ou `enable_experimental_instrumentations`.", "xpack.apm.agentConfig.enableInstrumentations.label": "Désactiver les instrumentations", "xpack.apm.agentConfig.enableLogCorrelation.description": "Nombre booléen spécifiant si l'agent doit être intégré au MDC de SLF4J pour activer la corrélation de logs de suivi. Si cette option est configurée sur `true`, l'agent définira `trace.id` et `transaction.id` pour les intervalles et transactions actifs sur le MDC. Depuis la version 1.16.0 de l'agent Java, l'agent ajoute également le `error.id` de l'erreur capturée au MDC juste avant le logging du message d'erreur. REMARQUE : bien qu'il soit autorisé d'activer ce paramètre au moment de l'exécution, vous ne pouvez pas le désactiver sans redémarrage.", "xpack.apm.agentConfig.enableLogCorrelation.label": "Activer la corrélation de logs", - "xpack.apm.agentConfig.exitSpanMinDuration.description": "Les intervalles de sortie sont des intervalles qui représentent un appel à un service externe, tel qu'une base de données. Si de tels appels sont très courts, ils ne sont généralement pas pertinents et ils peuvent être ignorés.\n\nREMARQUE : Si un intervalle propage des ID de traçage distribué, il ne sera pas ignoré, même s'il est plus court que le seuil configuré. Cela permet de s'assurer qu'aucune trace interrompue n'est enregistrée.", + "xpack.apm.agentConfig.exitSpanMinDuration.description": "Les intervalles de sortie sont des intervalles qui représentent un appel à un service externe, tel qu'une base de données. Si de tels appels sont très courts, ils ne sont généralement pas pertinents et ils peuvent être ignorés. REMARQUE : Si un intervalle propage des ID de traçage distribué, il ne sera pas ignoré, même s'il est plus court que le seuil configuré. Cela permet de s'assurer qu'aucune trace interrompue n'est enregistrée.", "xpack.apm.agentConfig.exitSpanMinDuration.label": "Durée min. d'intervalle de sortie", - "xpack.apm.agentConfig.ignoreExceptions.description": "Liste d'exceptions qui doivent être ignorées et non signalées comme des erreurs.\nCela permet d'ignorer les exceptions qui ont été lancées dans le flux de contrôle normal mais qui ne sont pas de réelles erreurs.", + "xpack.apm.agentConfig.ignoreExceptions.description": "Liste d'exceptions qui doivent être ignorées et non signalées comme des erreurs. Cela permet d'ignorer les exceptions qui ont été lancées dans le flux de contrôle normal mais qui ne sont pas de réelles erreurs.", "xpack.apm.agentConfig.ignoreExceptions.label": "Ignorer les exceptions", - "xpack.apm.agentConfig.ignoreMessageQueues.description": "Utilisé pour exclure les files d'attente/sujets de messagerie spécifiques du traçage. \n\nCette propriété doit être définie sur un tableau contenant une ou plusieurs chaînes.\nUne fois définie, les envois vers et les réceptions depuis les files d'attente/sujets spécifiés seront ignorés.", + "xpack.apm.agentConfig.ignoreMessageQueues.description": "Utilisé pour exclure les files d'attente/sujets de messagerie spécifiques du traçage. Cette propriété doit être définie sur un tableau contenant une ou plusieurs chaînes. Une fois définie, les envois vers et les réceptions depuis les files d'attente/sujets spécifiés seront ignorés.", "xpack.apm.agentConfig.ignoreMessageQueues.label": "Ignorer les files d'attente des messages", "xpack.apm.agentConfig.logEcsReformatting.description": "Spécifier si et comment l'agent doit reformater automatiquement les logs d'application en [JSON compatible avec ECS](https://www.elastic.co/guide/en/ecs-logging/overview/master/intro.html), compatible avec l'ingestion dans Elasticsearch à des fins d'analyse de log plus poussée.", "xpack.apm.agentConfig.logEcsReformatting.label": "Reformatage ECS des logs", "xpack.apm.agentConfig.logLevel.description": "Définit le niveau de logging pour l'agent", "xpack.apm.agentConfig.logLevel.label": "Niveau du log", - "xpack.apm.agentConfig.logSending.description": "Expérimental, requiert la version la plus récente de l'agent Java.\n\nSi `true` est défini,\nL'agent envoie les logs directement au serveur APM.", + "xpack.apm.agentConfig.logSending.description": "Expérimental, requiert la version la plus récente de l'agent Java. Si défini sur `true`, l'agent enverra les logs directement au serveur APM.", "xpack.apm.agentConfig.logSending.label": "Envoi de logs (expérimental)", - "xpack.apm.agentConfig.mongodbCaptureStatementCommands.description": "Les noms de commande MongoDB pour lesquels le document de commande est capturé, limité aux opérations en lecture seule courantes par défaut. Définissez cette option sur `\"\"` (vide) pour désactiver la capture, et sur `*` pour tout capturer (ce qui est déconseillé, car cela peut entraîner la capture d'informations sensibles).\n\nCette option prend en charge le caractère générique `*` qui correspond à zéro caractère ou plus. Exemples : `/foo/*/bar/*/baz*`, `*foo*`. La correspondance n'est pas sensible à la casse par défaut. L'ajout de `(?-i)` au début d'un élément rend la correspondance sensible à la casse.", + "xpack.apm.agentConfig.mongodbCaptureStatementCommands.description": "Les noms de commande MongoDB pour lesquels le document de commande est capturé, limité aux opérations en lecture seule courantes par défaut. Définissez cette option sur `\"\"` (vide) pour désactiver la capture, et sur `*` pour tout capturer (ce qui est déconseillé, car cela peut entraîner la capture d'informations sensibles). Cette option prend en charge le caractère générique `*` qui correspond à zéro caractère ou plus. Exemples : `/foo/*/bar/*/baz*`, `*foo*`. La correspondance n'est pas sensible à la casse par défaut. L'ajout de `(?-i)` au début d'un élément rend la correspondance sensible à la casse.", "xpack.apm.agentConfig.mongodbCaptureStatementCommands.label": "Commandes d'instruction pour la capture MongoDB", "xpack.apm.agentConfig.newConfig.description": "Affinez votre configuration d'agent depuis l'application APM. Les modifications sont automatiquement propagées à vos agents APM, ce qui vous évite d'effectuer un redéploiement.", "xpack.apm.agentConfig.profilingInferredSpansEnabled.description": "Définissez cette option sur `true` afin que l'agent crée des intervalles pour des exécutions de méthodes basées sur async-profiler, un profiler d'échantillonnage (ou profiler statistique). En raison de la nature du fonctionnement des profilers d'échantillonnage, la durée des intervalles générés n'est pas exacte, il ne s'agit que d'estimations. `profiling_inferred_spans_sampling_interval` vous permet d'ajuster avec exactitude le compromis entre précision et surcharge. Les intervalles générés sont créés à la fin d'une session de profilage. Cela signifie qu'il existe un délai entre les intervalles réguliers et les intervalles générés visibles dans l'interface utilisateur. REMARQUE : cette fonctionnalité n'est pas disponible sous Windows.", @@ -9897,7 +10727,7 @@ "xpack.apm.agentConfig.profilingInferredSpansMinDuration.label": "Durée minimale des intervalles générés par le profilage", "xpack.apm.agentConfig.profilingInferredSpansSamplingInterval.description": "Fréquence à laquelle les traces de pile sont rassemblées au cours d'une session de profilage. Plus vous définissez un chiffre bas, plus les durées seront précises. Cela induit une surcharge plus élevée et un plus grand nombre d'intervalles, pour des opérations potentiellement non pertinentes. La durée minimale d'un intervalle généré par le profilage est identique à la valeur de ce paramètre.", "xpack.apm.agentConfig.profilingInferredSpansSamplingInterval.label": "Intervalle d'échantillonnage des intervalles générés par le profilage", - "xpack.apm.agentConfig.range.errorText": "{rangeType, select,\n between {doit être compris entre {min} et {max}}\n gt {doit être supérieur à {min}}\n lt {doit être inférieur à {max}}\n other {doit être un entier}\n }", + "xpack.apm.agentConfig.range.errorText": "{rangeType, select, between {Doit être compris entre {min} et {max}} gt {Doit être supérieur à {min}} lt {Doit être inférieur à {max}} other {Doit être un entier} }", "xpack.apm.agentConfig.recording.description": "Lorsque l'enregistrement est activé, l'agent instrumente les requêtes HTTP entrantes, effectue le suivi des erreurs, et collecte et envoie les indicateurs. Lorsque l'enregistrement n'est pas activé, l'agent agit comme un noop, sans collecter de données ni communiquer avec le serveur AMP, sauf pour rechercher la configuration mise à jour. Puisqu'il s'agit d'un commutateur réversible, les threads d'agents ne sont pas détruits lorsque le mode sans enregistrement est défini. Ils restent principalement inactifs, de sorte que la surcharge est négligeable. Vous pouvez utiliser ce paramètre pour contrôler dynamiquement si Elastic APM doit être activé ou désactivé.", "xpack.apm.agentConfig.recording.label": "Enregistrement", "xpack.apm.agentConfig.sanitizeFiledNames.description": "Il est parfois nécessaire d'effectuer un nettoyage, c'est-à-dire de supprimer les données sensibles envoyées à Elastic APM. Cette configuration accepte une liste de modèles de caractères génériques de champs de noms qui doivent être nettoyés. Ils s'appliquent aux en-têtes HTTP (y compris les cookies) et aux données `application/x-www-form-urlencoded` (champs de formulaire POST). La chaîne de la requête et le corps de la requête capturé (comme des données `application/json`) ne seront pas nettoyés.", @@ -9907,7 +10737,7 @@ "xpack.apm.agentConfig.saveConfig.succeeded.text": "La configuration de \"{serviceName}\" a été enregistrée. La propagation jusqu'aux agents pourra prendre un certain temps.", "xpack.apm.agentConfig.saveConfig.succeeded.title": "Configuration enregistrée", "xpack.apm.agentConfig.saveConfigurationButtonLabel": "Étape suivante", - "xpack.apm.agentConfig.serverTimeout.description": "Si une requête au serveur APM prend plus de temps que le délai d'expiration configuré,\nla requête est annulée et l'événement (exception ou transaction) est abandonné.\nDéfinissez sur 0 pour désactiver les délais d'expiration.\n\nAVERTISSEMENT : si les délais d'expiration sont désactivés ou définis sur une valeur élevée, il est possible que votre application rencontre des problèmes de mémoire en cas d'expiration du serveur APM.", + "xpack.apm.agentConfig.serverTimeout.description": "Si une requête adressée au serveur APM prend plus de temps que le délai d'expiration configuré, la requête est annulée et l'événement (exception ou transaction) est ignoré. Définissez sur 0 pour désactiver les délais d'expiration. AVERTISSEMENT : si les délais d'expiration sont désactivés ou définis sur une valeur élevée, il est possible que votre application rencontre des problèmes de mémoire en cas d'expiration du serveur APM.", "xpack.apm.agentConfig.serverTimeout.label": "Délai d'expiration du serveur", "xpack.apm.agentConfig.servicePage.alreadyConfiguredOption": "déjà configuré", "xpack.apm.agentConfig.servicePage.cancelButton": "Annuler", @@ -9924,17 +10754,17 @@ "xpack.apm.agentConfig.settingsPage.notFound.message": "La configuration demandée n'existe pas", "xpack.apm.agentConfig.settingsPage.notFound.title": "Désolé, une erreur est survenue", "xpack.apm.agentConfig.settingsPage.saveButton": "Enregistrer la configuration", - "xpack.apm.agentConfig.spanCompressionEnabled.description": "L'attribution de la valeur \"true\" à cette option activera la fonctionnalité de compression de l'intervalle.\nLa compression d'intervalle réduit la surcharge de collecte, de traitement et de stockage, et supprime l'encombrement dans l'interface utilisateur. Le compromis est que certaines informations, telles que les instructions de base de données de tous les intervalles compressés, ne seront pas collectées.", + "xpack.apm.agentConfig.spanCompressionEnabled.description": "L'attribution de la valeur \"true\" à cette option activera la fonctionnalité de compression de l'intervalle. La compression d'intervalle réduit la surcharge de collecte, de traitement et de stockage, et supprime l'encombrement dans l'interface utilisateur. Le compromis est que certaines informations, telles que les instructions de base de données de tous les intervalles compressés, ne seront pas collectées.", "xpack.apm.agentConfig.spanCompressionEnabled.label": "Compression d'intervalle activée", "xpack.apm.agentConfig.spanCompressionExactMatchMaxDuration.description": "Les intervalles consécutifs qui sont des correspondances parfaites et qui se trouvent sous ce seuil seront compressés en un seul intervalle composite. Cette option ne s'applique pas aux intervalles composites. Cela réduit la surcharge de collecte, de traitement et de stockage, et supprime l'encombrement dans l'interface utilisateur. Le compromis est que les instructions de base de données de tous les intervalles compressés ne seront pas collectées.", "xpack.apm.agentConfig.spanCompressionExactMatchMaxDuration.label": "Durée maximale de compression d'intervalles en correspondance parfaite", "xpack.apm.agentConfig.spanCompressionSameKindMaxDuration.description": "Les intervalles consécutifs qui ont la même destination et qui se trouvent sous ce seuil seront compressés en un seul intervalle composite. Cette option ne s'applique pas aux intervalles composites. Cela réduit la surcharge de collecte, de traitement et de stockage, et supprime l'encombrement dans l'interface utilisateur. Le compromis est que les instructions de base de données de tous les intervalles compressés ne seront pas collectées.", "xpack.apm.agentConfig.spanCompressionSameKindMaxDuration.label": "Durée maximale de compression d'intervalles de même genre", - "xpack.apm.agentConfig.spanFramesMinDuration.description": "(déclassé, utilisez `span_stack_trace_min_duration` à la place) Dans ses paramètres par défaut, l'agent APM collectera une trace de la pile avec chaque intervalle enregistré.\nBien qu'il soit très pratique de trouver l'endroit exact dans votre code qui provoque l'intervalle, la collecte de cette trace de la pile provoque une certaine surcharge. \nLorsque cette option est définie sur une valeur négative, telle que `-1ms`, les traces de pile sont collectées pour tous les intervalles. En choisissant une valeur positive, par ex. `5ms`, la collecte des traces de pile se limitera aux intervalles dont la durée est égale ou supérieure à la valeur donnée, c’est-à-dire 5 millisecondes.\n\nPour désactiver complètement la collecte des traces de pile des intervalles, réglez la valeur sur `0ms`.", + "xpack.apm.agentConfig.spanFramesMinDuration.description": "(déclassé, utilisez `span_stack_trace_min_duration` à la place) Dans ses paramètres par défaut, l'agent APM collectera une trace de la pile avec chaque intervalle enregistré. Bien qu'il soit très pratique de trouver l'endroit exact dans votre code qui provoque l'intervalle, la collecte de cette trace de la pile provoque une certaine surcharge. Lorsque cette option est définie sur une valeur négative, telle que `-1ms`, les traces de pile sont collectées pour tous les intervalles. En choisissant une valeur positive, par ex. `5ms`, la collecte des traces de pile se limitera aux intervalles dont la durée est égale ou supérieure à la valeur donnée, c’est-à-dire 5 millisecondes. Pour désactiver complètement la collecte des traces de pile des intervalles, réglez la valeur sur `0ms`.", "xpack.apm.agentConfig.spanFramesMinDuration.label": "Durée minimale des cadres des intervalles", - "xpack.apm.agentConfig.spanMinDuration.description": "Définit la durée minimale des intervalles. Une tentative visant à ignorer les intervalles qui s'exécutent plus rapidement que ce seuil peut avoir lieu.\n\nLa tentative échoue si elle mène à un intervalle qui ne peut pas être ignoré. Les intervalles qui propagent le contexte de trace aux services en aval, tels que les requêtes HTTP sortantes, ne peuvent pas être ignorés. De plus, les intervalles qui conduisent à une erreur ou qui peuvent être le parent d'une opération asynchrone ne peuvent pas être ignorés.\n\nCependant, les appels externes qui ne propagent pas le contexte, tels que les appels à une base de données, peuvent être ignorés à l'aide de ce seuil.", + "xpack.apm.agentConfig.spanMinDuration.description": "Définit la durée minimale des intervalles. Une tentative visant à ignorer les intervalles qui s'exécutent plus rapidement que ce seuil peut avoir lieu. La tentative échoue si elle mène à un intervalle qui ne peut pas être ignoré. Les intervalles qui propagent le contexte de trace aux services en aval, tels que les requêtes HTTP sortantes, ne peuvent pas être ignorés. De plus, les intervalles qui conduisent à une erreur ou qui peuvent être le parent d'une opération asynchrone ne peuvent pas être ignorés. Cependant, les appels externes qui ne propagent pas le contexte, tels que les appels à une base de données, peuvent être ignorés à l'aide de ce seuil.", "xpack.apm.agentConfig.spanMinDuration.label": "Durée minimale de l'intervalle", - "xpack.apm.agentConfig.spanStackTraceMinDuration.description": "Bien qu'il soit très pratique de trouver l'endroit exact dans votre code qui provoque l'intervalle, la collecte de cette trace de la pile provoque une certaine surcharge. Lorsque cette option est définie sur la valeur `0ms`, les traces de pile sont collectées pour tous les intervalles. En choisissant une valeur positive, par ex. `5ms`, la collecte des traces de pile se limitera aux intervalles dont la durée est égale ou supérieure à la valeur donnée, c’est-à-dire 5 millisecondes.\n\nPour désactiver complètement la collecte des traces de pile des intervalles, réglez la valeur sur `-1ms`.", + "xpack.apm.agentConfig.spanStackTraceMinDuration.description": "Bien qu'il soit très pratique de trouver l'endroit exact dans votre code qui provoque l'intervalle, la collecte de cette trace de la pile provoque une certaine surcharge. Lorsque cette option est définie sur la valeur `0ms`, les traces de pile sont collectées pour tous les intervalles. En choisissant une valeur positive, par ex. `5ms`, la collecte des traces de pile se limitera aux intervalles dont la durée est égale ou supérieure à la valeur donnée, c’est-à-dire 5 millisecondes. Pour désactiver complètement la collecte des traces de pile des intervalles, réglez la valeur sur `-1ms`.", "xpack.apm.agentConfig.spanStackTraceMinDuration.label": "Durée minimale de la trace de pile de l'intervalle", "xpack.apm.agentConfig.stackTraceLimit.description": "En définissant cette option sur 0, la collecte des traces de pile sera désactivée. Toute valeur entière positive sera utilisée comme nombre maximal de cadres à collecter. La valeur -1 signifie que tous les cadres seront collectés.", "xpack.apm.agentConfig.stackTraceLimit.label": "Limite de trace de pile", @@ -9948,24 +10778,24 @@ "xpack.apm.agentConfig.stressMonitorSystemCpuReliefThreshold.label": "Seuil d'allègement de la tension du monitoring du CPU système", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.description": "Seuil utilisé par le monitoring du CPU du système pour détecter la tension du processeur du système. Si le CPU système dépasse ce seuil pour une durée d'au moins `stress_monitor_cpu_duration_threshold`, le monitoring considère qu'il est en état de tension.", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.label": "Seuil de tension du monitoring du CPU système", - "xpack.apm.agentConfig.traceContinuationStrategy.description": "Cette option permet un certain contrôle sur la façon dont l'agent APM gère les en-tête de contexte de trace W3C dans les requêtes entrantes. Par défaut, les en-têtes `traceparent` et `tracestate` sont utilisés conformément aux spécifications W3C pour le traçage distribué. Cependant, dans certains cas, il peut être utile de ne pas utiliser l'en-tête `traceparent` entrant. Quelques exemples de cas d'utilisation :\n\n* Un service monitoré par Elastic reçoit des requêtes avec les en-têtes `traceparent` de services non monitorés.\n* Un service monitoré par Elastic est publiquement exposé, et ne souhaite pas que les données de traçage (ID de traçage, décisions d'échantillonnage) puissent être usurpées par des requêtes d'utilisateur.\n\nLes valeurs valides sont :\n* \"continue\" : comportement par défaut. Une valeur `traceparent` entrante est utilisée pour continuer le traçage et déterminer la décision d'échantillonnage.\n* \"restart\" : ignore toujours l'en-tête `traceparent` des requêtes entrantes. Un nouveau trace-id sera généré et la décision d'échantillonnage sera prise en fonction de transaction_sample_rate. Une liaison d'intervalle sera effectuée vers le `traceparent` entrant.\n* \"restart_external\" : Si une requête entrante inclut le drapeau de fournisseur `es` dans `tracestate`, tout `traceparent` sera considéré comme interne et sera géré comme décrit pour `continue` ci-dessus. Autrement, tout `traceparent` est considéré comme externe et sera géré comme décrit pour `restart` ci-dessus.\n\nDepuis Elastic Observability 8.2, les liens d'intervalle sont visibles dans les vues de trace.\n\nCette option ne respecte pas la casse.", + "xpack.apm.agentConfig.traceContinuationStrategy.description": "Cette option permet un certain contrôle sur la façon dont l'agent APM gère les en-tête de contexte de trace W3C dans les requêtes entrantes. Par défaut, les en-têtes `traceparent` et `tracestate` sont utilisés conformément aux spécifications W3C pour le traçage distribué. Cependant, dans certains cas, il peut être utile de ne pas utiliser l'en-tête `traceparent` entrant. Quelques exemples de cas d'utilisation : * Un service monitoré par Elastic reçoit des requêtes avec les en-têtes `traceparent` de services non monitorés. * Un service monitoré par Elastic est publiquement exposé, et ne souhaite pas que les données de traçage (ID de traçage, décisions d'échantillonnage) puissent être usurpées par des requêtes d'utilisateur. Les valeurs valides sont : * 'continue' : comportement par défaut. Une valeur `traceparent` entrante est utilisée pour continuer le traçage et déterminer la décision d'échantillonnage. * 'restart' : ignore toujours l'en-tête `traceparent` des requêtes entrantes. Un nouveau trace-id sera généré et la décision d'échantillonnage sera prise en fonction de transaction_sample_rate. Une liaison d'intervalle sera effectuée vers le `traceparent` entrant. * 'restart_external' : Si une requête entrante inclut le drapeau de fournisseur `es` dans `tracestate`, tout `traceparent` sera considéré comme interne et sera géré comme décrit pour `continue` ci-dessus. Autrement, tout `traceparent` est considéré comme externe et sera géré comme décrit pour `restart` ci-dessus. Depuis Elastic Observability 8.2, les liens d'intervalle sont visibles dans les vues de trace. Cette option ne respecte pas la casse.", "xpack.apm.agentConfig.traceContinuationStrategy.label": "Stratégie de poursuite de traçage", - "xpack.apm.agentConfig.traceMethods.description": "Liste de méthodes pour lesquelles une transaction ou un intervalle doivent être créés.\n\nSi vous souhaitez monitorer un nombre important de méthodes,\nutilisez `profiling_inferred_spans_enabled`.\n\nCela fonctionne en instrumentant chaque méthode correspondante pour inclure le code qui crée un intervalle pour la méthode.\nSi la création d'un intervalle est très économique en termes de performances,\nl'instrumentation de toute une base de codes ou d'une méthode qui est exécutée dans une boucle serrée entraîne une surcharge significative.\n\nREMARQUE : utilisez les caractères génériques uniquement si cela est nécessaire.\nPlus vous faites correspondre de méthodes, plus l'agent créera une surcharge.\nNotez également qu'il existe une quantité maximale d'intervalles par transaction, `transaction_max_spans`.\n\nPour en savoir plus, consultez la documentation de l'agent Java.", + "xpack.apm.agentConfig.traceMethods.description": "Liste de méthodes pour lesquelles une transaction ou un intervalle doivent être créés. Si vous souhaitez monitorer un nombre important de méthodes, utilisez `profiling_inferred_spans_enabled`. Cela fonctionne en instrumentant chaque méthode correspondante pour inclure le code qui crée un intervalle pour la méthode. Alors que la création d'un intervalle est très économique en termes de performances, l'instrumentation de toute une base de codes ou d'une méthode qui est exécutée dans une boucle serrée entraîne une surcharge significative. REMARQUE : utilisez les caractères génériques uniquement si cela est nécessaire. Plus vous faites correspondre de méthodes, plus l'agent créera une surcharge. Notez également qu'il existe une quantité maximale d'intervalles par transaction, `transaction_max_spans`. Pour en savoir plus, consultez la documentation de l'agent Java.", "xpack.apm.agentConfig.traceMethods.label": "Méthodes de traçage", "xpack.apm.agentConfig.transactionIgnoreUrl.description": "Utilisé pour limiter l'instrumentation des requêtes vers certaines URL. Cette configuration accepte une liste séparée par des virgules de modèles de caractères génériques de chemins d'URL qui doivent être ignorés. Lorsqu'une requête HTTP entrante sera détectée, son chemin de requête sera confronté à chaque élément figurant dans cette liste. Par exemple, l'ajout de `/home/index` à cette liste permettrait de faire correspondre et de supprimer l'instrumentation de `http://localhost/home/index` ainsi que de `http://whatever.com/home/index?value1=123`", "xpack.apm.agentConfig.transactionIgnoreUrl.label": "Ignorer les transactions basées sur les URL", - "xpack.apm.agentConfig.transactionIgnoreUserAgents.description": "Utilisé pour limiter l'instrumentation des requêtes de certains agents utilisateurs.\n\nLorsqu'une requête HTTP entrante est détectée,\nl'agent utilisateur des en-têtes de la requête sera testé avec chaque élément de cette liste.\nExemple : `curl/*`, `*pingdom*`", + "xpack.apm.agentConfig.transactionIgnoreUserAgents.description": "Utilisé pour limiter l'instrumentation des requêtes de certains agents utilisateurs. Lorsqu'une requête HTTP entrante est détectée, l'agent utilisateur des en-têtes de requête sera vérifié par rapport à chaque élément de cette liste. Exemple : `curl/*`, `*pingdom*`", "xpack.apm.agentConfig.transactionIgnoreUserAgents.label": "La transaction ignore les agents utilisateurs", "xpack.apm.agentConfig.transactionMaxSpans.description": "Limite la quantité d'intervalles enregistrés par transaction.", "xpack.apm.agentConfig.transactionMaxSpans.label": "Nb maxi d'intervalles de transaction", - "xpack.apm.agentConfig.transactionNameGroups.description": "Avec cette option,\nvous pouvez regrouper les noms de transaction contenant des parties dynamiques avec une expression de caractère générique.\nPar exemple,\nle modèle \"GET /user/*/cart\" consolide les transactions,\ntelles que \"GET /users/42/cart\" et \"GET /users/73/cart\", en un même nom de transaction, \"GET /users/*/cart\",\nréduisant ainsi la cardinalité du nom de transaction.", + "xpack.apm.agentConfig.transactionNameGroups.description": "Avec cette option, vous pouvez regrouper les noms de transaction contenant des parties dynamiques avec une expression de caractère générique. Par exemple, le modèle `GET /user/*/cart` consoliderait les transactions telles que `GET /users/42/cart` et `GET /users/73/cart` en un seul nom de transaction `GET /users/*/cart`, réduisant ainsi la cardinalité du nom de transaction.", "xpack.apm.agentConfig.transactionNameGroups.label": "Groupes de noms de transaction", "xpack.apm.agentConfig.transactionSampleRate.description": "Par défaut, l'agent échantillonnera chaque transaction (par ex. requête à votre service). Pour réduire la surcharge et les exigences de stockage, vous pouvez définir le taux d'échantillonnage sur une valeur comprise entre 0,0 et 1,0. La durée globale et le résultat des transactions non échantillonnées seront toujours enregistrés, mais pas les informations de contexte, les étiquettes ni les intervalles.", "xpack.apm.agentConfig.transactionSampleRate.label": "Taux d'échantillonnage des transactions", - "xpack.apm.agentConfig.unnestExceptions.description": "Lors du reporting d'exceptions,\ndésimbrique les exceptions correspondant au modèle de caractère générique.\nCela peut s'avérer pratique pour Spring avec \"org.springframework.web.util.NestedServletException\",\npar exemple.", + "xpack.apm.agentConfig.unnestExceptions.description": "Lors du reporting d'exceptions, désimbrique les exceptions correspondant au modèle de caractère générique. Par exemple, cela peut s'avérer pratique pour Spring avec `org.springframework.web.util.NestedServletException`.", "xpack.apm.agentConfig.unnestExceptions.label": "Désimbriquer les exceptions", "xpack.apm.agentConfig.unsavedSetting.tooltip": "Non enregistré", - "xpack.apm.agentConfig.usePathAsTransactionName.description": "Si `true` est défini,\nles noms de transaction de frameworks non pris en charge ou partiellement pris en charge seront au format `$method $path` au lieu de simplement `$method unknown route`.\n\nAVERTISSEMENT : si vos URL contiennent des paramètres de chemin tels que `/user/$userId`,\nsoyez très prudent en activant cet indicateur,\ncar cela peut entraîner une explosion de groupes de transactions.\nConsultez l'option `transaction_name_groups` pour savoir comment atténuer ce problème en regroupant les URL ensemble.", + "xpack.apm.agentConfig.usePathAsTransactionName.description": "Lorsque défini sur `true`, les noms de transaction de frameworks non pris en charge ou partiellement pris en charge seront au format `$method $path` au lieu de simplement `$method unknown route`. AVERTISSEMENT : Si vos URL contiennent des paramètres de chemin tels que `/user/$userId`, vous devez être très prudent lorsque vous activez cet indicateur, car cela peut entraîner une explosion des groupes de transactions. Consultez l'option `transaction_name_groups` pour savoir comment atténuer ce problème en regroupant les URL ensemble.", "xpack.apm.agentConfig.usePathAsTransactionName.label": "Utiliser le chemin comme nom de transaction", "xpack.apm.agentExplorer.agentLanguageSelect.label": "Langage de l'agent", "xpack.apm.agentExplorer.agentLanguageSelect.placeholder": "Tous", @@ -10033,13 +10863,14 @@ "xpack.apm.aiAssistant.starterPrompts.explainNoData.title": "Expliquer", "xpack.apm.alertDetails.error.toastDescription": "Impossible de charger les graphiques de la page de détails d’alerte. Veuillez essayer d’actualiser la page si l’alerte vient d’être créée", "xpack.apm.alertDetails.error.toastTitle": "Une erreur s’est produite lors de l’identification de la plage temporelle de l’alerte.", + "xpack.apm.alertDetails.thresholdTitle": "Seuil dépassé", "xpack.apm.alertDetails.viewInApm": "Afficher dans APM", "xpack.apm.alerting.fields.environment": "Environnement", "xpack.apm.alerting.fields.error.group.id": "Clé du groupe d'erreurs", "xpack.apm.alerting.fields.service": "Service", "xpack.apm.alerting.fields.transaction.name": "Nom", "xpack.apm.alerting.fields.type": "Type", - "xpack.apm.alerting.transaction.name.custom.text": "Ajouter \\{searchValue\\} en tant que nouveau nom de transaction", + "xpack.apm.alerting.transaction.name.custom.text": "Ajouter '{searchValue}' en tant que nouveau nom de transaction", "xpack.apm.alertingEmbeddables.serviceName.error.toastDescription": "Impossible de charger les visualisations d’APM.", "xpack.apm.alertingEmbeddables.serviceName.error.toastTitle": "Une erreur s'est produite lors de l'identification du nom du service APM ou du type de transaction.", "xpack.apm.alertingEmbeddables.timeRange.error.toastTitle": "Une erreur s’est produite lors de l’identification de la plage temporelle de l’alerte.", @@ -10073,20 +10904,20 @@ "xpack.apm.alerts.timeLabels.minutes": "minutes", "xpack.apm.alerts.timeLabels.seconds": "secondes", "xpack.apm.alertTypes.anomaly.description": "Alerte lorsque la latence, le rendement ou le taux de transactions ayant échoué d'un service est anormal.", - "xpack.apm.alertTypes.errorCount.defaultActionMessage": "'{{context.reason}}'\n\n'{{rule.name}}' est active selon les conditions suivantes :\n\n- Nom de service : '{{context.serviceName}}'\n- Environnement : '{{context.environment}}'\n- Nombre d’erreurs : '{{context.triggerValue}}' erreurs sur la dernière période de '{{context.interval}}'\n- Seuil : '{{context.threshold}}'\n\n[Afficher les détails de l’alerte]('{{context.alertDetailsUrl}}')\n", - "xpack.apm.alertTypes.errorCount.defaultRecoveryMessage": "'{{context.reason}}'\n\n'{{rule.name}}' s’est rétablie.\n\n- Nom de service : '{{context.serviceName}}'\n- Environnement : '{{context.environment}}'\n- Nombre d’erreurs : '{{context.triggerValue}}' erreurs sur la dernière période de '{{context.interval}}'\n- Seuil : '{{context.threshold}}'\n\n[Afficher les détails de l’alerte]('{{context.alertDetailsUrl}}')\n", + "xpack.apm.alertTypes.errorCount.defaultActionMessage": "'{{context.reason}}' '{{rule.name}}' est actif avec les conditions suivantes : - Nom du service : '{{context.serviceName}}' - Environnement : '{{context.environment}}' - Nombre d'erreurs : '{{context.triggerValue}}' erreurs au cours des dernières '{{context.interval}}' - Seuil : '{{context.threshold}}' [Afficher les détails de l'alerte]('{{context.alertDetailsUrl}}')", + "xpack.apm.alertTypes.errorCount.defaultRecoveryMessage": "'{{context.reason}}' '{{rule.name}}' s'est rétabli : - Nom du service : '{{context.serviceName}}' - Environnement : '{{context.environment}}' - Nombre d'erreurs : '{{context.triggerValue}}' erreurs au cours des dernières '{{context.interval}}' - Seuil : '{{context.threshold}}' [Afficher les détails de l'alerte]('{{context.alertDetailsUrl}}')", "xpack.apm.alertTypes.errorCount.description": "Alerte lorsque le nombre d'erreurs d'un service dépasse un seuil défini.", "xpack.apm.alertTypes.errorCount.reason": "Le nombre d'erreurs est {measured} au cours des derniers/dernières {interval} pour {group}. Alerte lorsque > {threshold}.", "xpack.apm.alertTypes.minimumWindowSize.description": "La valeur minimale requise est {sizeValue} {sizeUnit}. Elle permet de s'assurer que l'alerte comporte suffisamment de données à évaluer. Si vous choisissez une valeur trop basse, l'alerte ne se déclenchera peut-être pas comme prévu.", - "xpack.apm.alertTypes.transactionDuration.defaultActionMessage": "'{{context.reason}}'\n\n'{{rule.name}}' est active selon les conditions suivantes :\n\n- Nom de service : '{{context.serviceName}}'\n- Type de transaction : '{{context.transactionType}}'\n- Nom de transaction : '{{context.transactionName}}'\n- Environnement : '{{context.environment}}'\n- Latence : '{{context.triggerValue}}' sur la dernière période de '{{context.interval}}'\n- Seuil : '{{context.threshold}}' ms\n\n[Afficher les détails de l’alerte]('{{context.alertDetailsUrl}}')\n", - "xpack.apm.alertTypes.transactionDuration.defaultRecoveryMessage": "'{{context.reason}}'\n\n'{{rule.name}}' s’est rétablie.\n\n- Nom de service : '{{context.serviceName}}'\n- Type de transaction : '{{context.transactionType}}'\n- Nom de transaction : '{{context.transactionName}}'\n- Environnement : '{{context.environment}}'\n- Latence : '{{context.triggerValue}}' sur la dernière période de '{{context.interval}}'\n- Seuil : '{{context.threshold}}' ms\n\n[Afficher les détails de l’alerte]('{{context.alertDetailsUrl}}')\n", + "xpack.apm.alertTypes.transactionDuration.defaultActionMessage": "'{{context.reason}}' '{{rule.name}}' est actif avec les conditions suivantes : - Nom du service : '{{context.serviceName}}' - Type de transaction : '{{context.transactionType}}' - Nom de transaction : '{{context.transactionName}}' - Environnement : '{{context.environment}}' - Latence : '{{context.triggerValue}}' sur les dernières '{{context.interval}}' - Seuil : '{{context.threshold}}' ms [Afficher les détails de l'alerte]('{{context.alertDetailsUrl}}')", + "xpack.apm.alertTypes.transactionDuration.defaultRecoveryMessage": "'{{context.reason}}' '{{rule.name}}' s'est rétabli : - Nom du service : '{{context.serviceName}}' - Type de transaction : '{{context.transactionType}}' - Nom de transaction : '{{context.transactionName}}' - Environnement : '{{context.environment}}' - Latence : '{{context.triggerValue}}' sur les dernières '{{context.interval}}' - Seuil : '{{context.threshold}}' ms [Afficher les détails de l'alerte]('{{context.alertDetailsUrl}}')", "xpack.apm.alertTypes.transactionDuration.description": "Alerte lorsque la latence d'un type de transaction spécifique dans un service dépasse le seuil défini.", "xpack.apm.alertTypes.transactionDuration.reason": "La latence de {aggregationType} est {measured} au cours des derniers/dernières {interval} pour {group}. Alerte lorsque > {threshold}.", - "xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage": "'{{context.reason}}'\n\n'{{rule.name}}' est active selon les conditions suivantes :\n\n- Nom de service : '{{context.serviceName}}'\n- Type de transaction : '{{context.transactionType}}'\n- Environnement : '{{context.environment}}'\n- Sévérité : '{{context.triggerValue}}'\n- Seuil : '{{context.threshold}}'\n\n[Afficher les détails de l’alerte]('{{context.alertDetailsUrl}}')\n", - "xpack.apm.alertTypes.transactionDurationAnomaly.defaultRecoveryMessage": "'{{context.reason}}'\n\n'{{rule.name}}' s’est rétablie.\n\n- Nom de service : '{{context.serviceName}}'\n- Type de transaction : '{{context.transactionType}}'\n- Environnement : '{{context.environment}}'\n- Sévérité : '{{context.triggerValue}}'\n- Seuil : '{{context.threshold}}'\n\n[Afficher les détails de l’alerte]('{{context.alertDetailsUrl}}')\n", + "xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage": "'{{context.reason}}' '{{rule.name}}' est actif avec les conditions suivantes : - Nom du service : '{{context.serviceName}}' - Type de transaction : '{{context.transactionType}}' - Environnement : '{{context.environment}}' - Gravité : '{{context.triggerValue}}' - Seuil : '{{context.threshold}}' [Afficher les détails de l'alerte]('{{context.alertDetailsUrl}}')", + "xpack.apm.alertTypes.transactionDurationAnomaly.defaultRecoveryMessage": "'{{context.reason}}' '{{rule.name}}' s'est rétabli : - Nom du service : '{{context.serviceName}}' - Type de transaction : '{{context.transactionType}}' - Environnement : '{{context.environment}}' - Gravité : '{{context.triggerValue}}' - Seuil : '{{context.threshold}}' [Afficher les détails de l'alerte]('{{context.alertDetailsUrl}}')", "xpack.apm.alertTypes.transactionDurationAnomaly.reason": "Une anomalie {severityLevel} {detectorTypeLabel} avec un score de {anomalyScore} a été détectée dans le dernier {interval} pour {serviceName}.", - "xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage": "'{{context.reason}}'\n\n'{{rule.name}}' est active selon les conditions suivantes :\n\n- Nom de service : '{{context.serviceName}}'\n- Type de transaction : '{{context.transactionType}}'\n- Environnement : '{{context.environment}}'\n- Taux de transactions ayant échoué : '{{context.triggerValue}}' % des erreurs sur la dernière période de '{{context.interval}}'\n- Seuil : '{{context.threshold}}'%\n\n[Afficher les détails de l’alerte]('{{context.alertDetailsUrl}}')\n", - "xpack.apm.alertTypes.transactionErrorRate.defaultRecoveryMessage": "'{{context.reason}}'\n\n'{{rule.name}}' s’est rétablie.\n\n- Nom de service : '{{context.serviceName}}'\n- Type de transaction : '{{context.transactionType}}'\n- Environnement : '{{context.environment}}'\n- Taux de transactions ayant échoué : '{{context.triggerValue}}' % des erreurs sur la dernière période de '{{context.interval}}'\n- Seuil : '{{context.threshold}}'%\n\n[Afficher les détails de l’alerte]('{{context.alertDetailsUrl}}')\n", + "xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage": "'{{context.reason}}' '{{rule.name}}' est actif avec les conditions suivantes : - Nom du service : '{{context.serviceName}}' - Type de transaction : '{{context.transactionType}}' - Environnement : '{{context.environment}}' - Taux de transactions échouées : '{{context.triggerValue}}'% d'erreurs au cours des dernières '{{context.interval}}' - Seuil : '{{context.threshold}}'% [Afficher les détails de l'alerte]('{{context.alertDetailsUrl}}')", + "xpack.apm.alertTypes.transactionErrorRate.defaultRecoveryMessage": "'{{context.reason}}' '{{rule.name}}' s'est rétabli : - Nom du service : '{{context.serviceName}}' - Type de transaction : '{{context.transactionType}}' - Environnement : '{{context.environment}}' - Taux de transactions échouées : '{{context.triggerValue}}'% d'erreurs au cours des dernières '{{context.interval}}' - Seuil : '{{context.threshold}}'% [Afficher les détails de l'alerte]('{{context.alertDetailsUrl}}')", "xpack.apm.alertTypes.transactionErrorRate.description": "Alerte lorsque le taux d'erreurs de transaction d'un service dépasse un seuil défini.", "xpack.apm.alertTypes.transactionErrorRate.reason": "L'échec des transactions est {measured} au cours des derniers/dernières {interval} pour {group}. Alerte lorsque > {threshold}.", "xpack.apm.analyzeDataButton.label": "Explorer les données", @@ -10120,8 +10951,10 @@ "xpack.apm.apmSettings.save.error": "Une erreur s'est produite lors de l'enregistrement des paramètres", "xpack.apm.apmSettings.saveButton": "Enregistrer les modifications", "xpack.apm.appName": "APM", + "xpack.apm.associate.service.logs.button": "Associer les logs de service existants", "xpack.apm.betaBadgeDescription": "Cette fonctionnalité est actuellement en version bêta. Si vous rencontrez des bugs ou si vous souhaitez apporter des commentaires, ouvrez un ticket de problème ou visitez notre forum de discussion.", "xpack.apm.betaBadgeLabel": "Bêta", + "xpack.apm.button.exploreLogs": "Explorer les logs", "xpack.apm.chart.annotation.version": "Version", "xpack.apm.chart.comparison.defaultPreviousPeriodLabel": "Période précédente", "xpack.apm.chart.cpuSeries.processAverageLabel": "Moyenne de processus", @@ -10134,6 +10967,7 @@ "xpack.apm.clickArialLabel.": "Cliquez pour afficher plus de détails", "xpack.apm.coldstartRate": "Taux de démarrage à froid", "xpack.apm.coldstartRate.chart.coldstartRate": "Taux de démarrage à froid (moy.)", + "xpack.apm.collect.service.logs.button": "Collecter de nouveaux logs de service", "xpack.apm.comparison.expectedBoundsTitle": "Limites attendues", "xpack.apm.comparison.mlExpectedBoundsDisabledText": "Limites attendues (la détection d'anomalie doit être activée pour l’environnement)", "xpack.apm.comparison.mlExpectedBoundsText": "Limites attendues", @@ -10282,14 +11116,21 @@ "xpack.apm.durationDistributionChartWithScrubber.emptySelectionText": "Glisser et déposer pour sélectionner une plage", "xpack.apm.durationDistributionChartWithScrubber.panelTitle": "Distribution de la latence", "xpack.apm.durationDistributionChartWithScrubber.selectionText": "Selection : {formattedSelection}", + "xpack.apm.eemFeedback.button.openSurvey": "Dites-nous ce que vous pensez !", + "xpack.apm.eemFeedback.title": "Faites-nous part de vos réflexions !", "xpack.apm.emptyMessage.noDataFoundDescription": "Essayez avec une autre plage temporelle ou réinitialisez le filtre de recherche.", "xpack.apm.emptyMessage.noDataFoundLabel": "Aucune donnée trouvée.", - "xpack.apm.environmentsSelectCustomOptionText": "Ajouter \\{searchValue\\} en tant que nouvel environnement", + "xpack.apm.entitiesInventoryCallout.linklabel": "Essayez notre nouvel inventaire !", + "xpack.apm.entitiesInventoryCallout.linkTooltip": "Cacher ceci", + "xpack.apm.entityLink.eemGuide.description": "Désolé, nous ne pouvons pas vous donner plus de détails sur ce service pour le moment dû au motif suivant : {limitationsLink}.", + "xpack.apm.entityLink.eemGuide.description.link": "limitations du modèle d'entité d'Elastic", + "xpack.apm.entityLink.eemGuide.goBackButtonLabel": "Retour", + "xpack.apm.entityLink.eemGuide.title": "Service non pris en charge", "xpack.apm.environmentsSelectPlaceholder": "Sélectionner l'environnement", "xpack.apm.error.prompt.body": "Veuillez consulter la console de développeur de votre navigateur pour plus de détails.", "xpack.apm.error.prompt.title": "Désolé, une erreur s'est produite :(", "xpack.apm.errorCountAlert.name": "Seuil de nombre d'erreurs", - "xpack.apm.errorCountRuleType.errors": " erreurs", + "xpack.apm.errorCountRuleType.errors": "erreurs", "xpack.apm.errorGroup.chart.ocurrences": "Occurrences d'erreurs", "xpack.apm.errorGroup.tabs.exceptionStacktraceLabel": "Trace de pile d'exception", "xpack.apm.errorGroup.tabs.logStacktraceLabel": "Trace de pile des logs", @@ -10310,7 +11151,6 @@ "xpack.apm.errorGroupTopTransactions.loading": "Chargement...", "xpack.apm.errorGroupTopTransactions.noResults": "Aucune erreur trouvée associée à des transactions", "xpack.apm.errorGroupTopTransactions.title": "5 principales transactions affectées", - "xpack.apm.errorKeySelectCustomOptionText": "Ajouter \\{searchValue\\} comme nouvelle clé de groupe d'erreurs", "xpack.apm.errorOverview.treemap.dropdown.devices.subtitle": "Cet affichage sous forme de compartimentage permet de visualiser plus facilement et rapidement les appareils les plus affectés", "xpack.apm.errorOverview.treemap.dropdown.versions.subtitle": "Cet affichage sous forme de compartimentage permet de visualiser plus facilement et rapidement les versions les plus affectées.", "xpack.apm.errorOverview.treemap.subtitle": "Compartimentage {currentTreemap} affichant le nombre total et les plus affectés", @@ -10348,6 +11188,7 @@ "xpack.apm.failure_badge.tooltip": "event.outcome = échec", "xpack.apm.featureRegistry.apmFeatureName": "APM et expérience utilisateur", "xpack.apm.feedbackMenu.appName": "APM", + "xpack.apm.feedbackModal.body.thanks": "Merci d'avoir essayé notre nouvelle interface. Nous continuerons à l'améliorer, alors revenez souvent.", "xpack.apm.fetcher.error.status": "Erreur", "xpack.apm.fetcher.error.title": "Erreur lors de la récupération des ressources", "xpack.apm.fetcher.error.url": "URL", @@ -10510,7 +11351,7 @@ "xpack.apm.home.alertsMenu.alerts": "Alertes et règles", "xpack.apm.home.alertsMenu.createAnomalyAlert": "Créer une règle d'anomalie", "xpack.apm.home.alertsMenu.createThresholdAlert": "Créer une règle de seuil", - "xpack.apm.home.alertsMenu.errorCount": " Créer une règle de comptage des erreurs", + "xpack.apm.home.alertsMenu.errorCount": "Créer une règle de comptage des erreurs", "xpack.apm.home.alertsMenu.transactionDuration": "Latence", "xpack.apm.home.alertsMenu.transactionErrorRate": "Taux de transactions ayant échoué", "xpack.apm.home.alertsMenu.viewActiveAlerts": "Gérer les règles", @@ -10554,7 +11395,7 @@ "xpack.apm.jvmsTable.nonHeapMemoryColumnLabel": "Moy. segment de mémoire sans tas", "xpack.apm.jvmsTable.threadCountColumnLabel": "Nombre de threads max", "xpack.apm.keyValueFilterList.actionFilterLabel": "Filtrer par valeur", - "xpack.apm.kueryBar.placeholder": "Rechercher {event, select,\n transaction {des transactions}\n metric {des indicateurs}\n error {des erreurs}\n other {des transactions, des erreurs et des indicateurs}\n } (par ex. {queryExample})", + "xpack.apm.kueryBar.placeholder": "Rechercher {event, select, transaction {des transactions} metric {des indicateurs} error {des erreurs} other {des transactions, des erreurs et des indicateurs} } (p. ex. {queryExample})", "xpack.apm.labs": "Ateliers", "xpack.apm.labs.cancel": "Annuler", "xpack.apm.labs.description": "Essayez les fonctionnalités APM qui sont en version d'évaluation technique et en cours de progression.", @@ -10563,6 +11404,11 @@ "xpack.apm.latencyCorrelations.licenseCheckText": "Pour utiliser les corrélations de latence, vous devez disposer d'une licence Elastic Platinum. Elle vous permettra de découvrir quels champs sont corrélés à de faibles performances.", "xpack.apm.license.button": "Commencer l'essai", "xpack.apm.license.title": "Commencer un essai gratuit de 30 jours", + "xpack.apm.logErrorRate": "Taux d'erreur des logs", + "xpack.apm.logErrorRate.tooltip.description": "Taux de logs d'erreurs par minute observé pour un {serviceName} donné.", + "xpack.apm.logRate": "Taux du log", + "xpack.apm.logs.chart.logRate": "Taux de log", + "xpack.apm.logs.chart.logsErrorRate": "Taux d'erreur des logs", "xpack.apm.managedTable.errorMessage": "Impossible de récupérer", "xpack.apm.managedTable.loadingDescription": "Chargement…", "xpack.apm.metadata.help": "Comment ajouter des étiquettes et d'autres données", @@ -10623,19 +11469,24 @@ "xpack.apm.mobileServiceDetails.serviceMapTabLabel": "Carte des services", "xpack.apm.mobileServiceDetails.transactionsTabLabel": "Transactions", "xpack.apm.mobileServices.breadcrumb.title": "Services", + "xpack.apm.multiSignal.servicesTable.logErrorRate.tooltip.serviceNameLabel": "service.name", + "xpack.apm.multiSignal.servicesTable.logRate.tooltip.description": "Taux de logs par minute observé pour un {serviceName} donné.", + "xpack.apm.multiSignal.servicesTable.logRate.tooltip.serviceNameLabel": "service.name", + "xpack.apm.multiSignal.table.tooltip.formula": "Calcul de la formule :", "xpack.apm.navigation.apmSettingsTitle": "Paramètres", "xpack.apm.navigation.apmStorageExplorerTitle": "Explorateur de stockage", "xpack.apm.navigation.apmTutorialTitle": "Tutoriel", "xpack.apm.navigation.dependenciesTitle": "Dépendances", + "xpack.apm.navigation.rootTitle": "Applications", "xpack.apm.navigation.serviceGroupsTitle": "Groupes de services", "xpack.apm.navigation.serviceMapTitle": "Carte des services", - "xpack.apm.navigation.servicesTitle": "Services", + "xpack.apm.navigation.servicesTitle": "Inventaire de service", "xpack.apm.navigation.tracesTitle": "Traces", "xpack.apm.noDataConfig.addApmIntegrationButtonLabel": "Ajouter l'intégration APM", "xpack.apm.noDataConfig.addDataButtonLabel": "Ajouter des données", "xpack.apm.noDataConfig.solutionName": "Observabilité", "xpack.apm.notAvailableLabel": "S. O.", - "xpack.apm.observabilityAiAssistant.functions.registerGetApmDownstreamDependencies.descriptionForUser": "Obtenez les dépendances en aval (services ou back-ends non instrumentés) pour un \n service. Vous pouvez ainsi mapper le nom d'une dépendance en aval avec un service par le \n renvoi de span.destination.service.resource et de service.name. Utilisez cette fonction pour \n explorer plus avant si nécessaire.", + "xpack.apm.observabilityAiAssistant.functions.registerGetApmDownstreamDependencies.descriptionForUser": "Obtenez les dépendances en aval (services ou back-ends non instrumentés) pour un service. Vous pouvez ainsi mapper le nom d'une dépendance en aval avec un service par le renvoi de span.destination.service.resource et de service.name. Utilisez ceci pour explorer davantage, si nécessaire.", "xpack.apm.observabilityAiAssistant.functions.registerGetApmServicesList.descriptionForUser": "Obtenez la liste des services surveillés, leur statut d'intégrité et les alertes.", "xpack.apm.onboarding.agent.column.configSettings": "Paramètre de configuration", "xpack.apm.onboarding.agent.column.configValue": "Valeur de configuration", @@ -10668,12 +11519,12 @@ "xpack.apm.onboarding.django.install.title": "Installer l'agent APM", "xpack.apm.onboarding.djangoClient.configure.commands.addAgentComment": "Ajouter l'agent aux applications installées", "xpack.apm.onboarding.djangoClient.configure.commands.addTracingMiddlewareComment": "Ajoutez notre intergiciel de traçage pour envoyer des indicateurs de performance", - "xpack.apm.onboarding.dotNet.configureAgent.textPost": "Si vous ne transférez pas une instance `IConfiguration` à l'agent (par ex., pour les applications non ASP.NET Core) vous pouvez également configurer l'agent par le biais de variables d'environnement. \n Pour une utilisation avancée, consultez [the documentation]({documentationLink}), qui comprend notamment le guide de démarrage rapide pour [Profiler Auto instrumentation]({profilerLink}).", + "xpack.apm.onboarding.dotNet.configureAgent.textPost": "Si vous ne transférez pas une instance `IConfiguration` à l'agent (par ex., pour les applications non ASP.NET Core) vous pouvez également configurer l'agent par le biais de variables d'environnement. Pour une utilisation avancée, consultez [the documentation]({documentationLink}), qui comprend notamment le guide de démarrage rapide pour [Profiler Auto instrumentation]({profilerLink}).", "xpack.apm.onboarding.dotNet.configureAgent.title": "Exemple de fichier appsettings.json :", - "xpack.apm.onboarding.dotNet.configureApplication.textPost": "La transmission d'une instance `IConfiguration` est facultative mais si cette opération est effectuée, l'agent lira les paramètres de configuration depuis cette instance `IConfiguration` (par ex. à partir du fichier `appsettings.json`).", - "xpack.apm.onboarding.dotNet.configureApplication.textPre": "Si vous utilisez ASP.NET Core avec le package `Elastic.Apm.NetCoreAll`, appelez la méthode `UseAllElasticApm` dans la méthode \"Configure\" dans le fichier `Startup.cs`.", + "xpack.apm.onboarding.dotNet.configureApplication.textPost": "La transmission d'une instance `IConfiguration` est facultative mais si cette opération est effectuée, l'agent lira les paramètres de configuration depuis cette instance `IConfiguration` (par ex. à partir du fichier `appsettings.json`).", + "xpack.apm.onboarding.dotNet.configureApplication.textPre": "Si vous utilisez ASP.NET Core avec le package `Elastic.Apm.NetCoreAll`, appelez la méthode `UseAllElasticApm` dans la méthode `Configure` dans le fichier `Startup.cs`.", "xpack.apm.onboarding.dotNet.configureApplication.title": "Ajouter l'agent à l'application", - "xpack.apm.onboarding.dotNet.download.textPre": "Ajoutez le ou les packages d'agent depuis [NuGet]({allNuGetPackagesLink}) à votre application .NET. Plusieurs packages NuGet sont disponibles pour différents cas d'utilisation. \n\nPour une application ASP.NET Core avec Entity Framework Core, téléchargez le package [Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}). Ce package ajoutera automatiquement chaque composant d'agent à votre application. \n\n Si vous souhaitez minimiser les dépendances, vous pouvez utiliser le package [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) uniquement pour le monitoring d'ASP.NET Core ou le package [Elastic.Apm.EfCore]({efCorePackageLink}) uniquement pour le monitoring d'Entity Framework Core. \n\n Si vous souhaitez seulement utiliser l'API d'agent publique pour l'instrumentation manuelle, utilisez le package [Elastic.Apm]({elasticApmPackageLink}).", + "xpack.apm.onboarding.dotNet.download.textPre": "Ajoutez le ou les packages d'agent depuis [NuGet]({allNuGetPackagesLink}) à votre application .NET. Plusieurs packages NuGet sont disponibles pour différents cas d'utilisation. Pour une application ASP.NET Core avec Entity Framework Core, téléchargez le package [Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}). Ce package ajoutera automatiquement chaque composant d'agent à votre application. Si vous souhaitez minimiser les dépendances, vous pouvez utiliser le package [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) uniquement pour le monitoring d'ASP.NET Core ou le package [Elastic.Apm.EfCore]({efCorePackageLink}) uniquement pour le monitoring d'Entity Framework Core. Si vous souhaitez seulement utiliser l'API d'agent publique pour l'instrumentation manuelle, utilisez le package [Elastic.Apm]({elasticApmPackageLink}).", "xpack.apm.onboarding.dotNet.download.title": "Télécharger l'agent APM", "xpack.apm.onboarding.dotnetClient.createConfig.commands.defaultServiceName": "La valeur par défaut est l'assemblage d'entrée de l'application.", "xpack.apm.onboarding.flask.configure.textPost": "Consultez la [documentation]({documentationLink}) pour une utilisation avancée.", @@ -10696,26 +11547,26 @@ "xpack.apm.onboarding.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "Initialisez à l'aide des variables d'environnement :", "xpack.apm.onboarding.goClient.configure.commands.usedExecutableNameComment": "En l'absence de spécification, le nom de l'exécutable est utilisé.", "xpack.apm.onboarding.introduction.imageAltDescription": "Capture d'écran du tableau de bord principal.", - "xpack.apm.onboarding.java.download.textPre": "Téléchargez le fichier jar de l'agent depuis [Maven Central]({mavenCentralLink}). N'ajoutez **pas** l'agent comme dépendance de votre application.", + "xpack.apm.onboarding.java.download.textPre": "Téléchargez le fichier jar de l'agent depuis [Maven Central]({mavenCentralLink}). N'ajoutez **pas** l'agent comme dépendance de votre application.", "xpack.apm.onboarding.java.download.title": "Télécharger l'agent APM", "xpack.apm.onboarding.java.startApplication.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.", - "xpack.apm.onboarding.java.startApplication.textPre": "Ajoutez l'indicateur `-javaagent` et configurez l'agent avec les propriétés du système.\n\n * Définir le nom de service requis (caractères autorisés : a-z, A-Z, 0-9, -, _ et espace)\n * Définir l'URL personnalisée du serveur APM (par défaut : {customApmServerUrl})\n * Définir le token secret du serveur APM\n * Définir l'environnement de service\n * Définir le package de base de votre application", + "xpack.apm.onboarding.java.startApplication.textPre": "Ajoutez l'indicateur `-javaagent` et configurez l'agent avec les propriétés du système. * Définir le nom de service requis (caractères autorisés : a-z, A-Z, 0-9, -, _ et espace) * * Définir l'URL personnalisée du serveur APM (par défaut : {customApmServerUrl}) * Définir le jeton secret du serveur APM * Définir l'environnement de service * Définir le package de base de votre application", "xpack.apm.onboarding.java.startApplication.title": "Lancer votre application avec l'indicateur javaagent", "xpack.apm.onboarding.node.configure.textPost": "Consultez [the documentation]({documentationLink}) pour une utilisation avancée, notamment pour connaître l'utilisation avec [Babel/ES Modules]({babelEsModulesLink}).", - "xpack.apm.onboarding.node.configure.textPre": "Les agents sont des bibliothèques exécutées dans les processus de votre application. Les services APM sont créés par programmation à partir du `serviceName`. Cet agent prend en charge de nombreux frameworks, mais peut également être utilisé avec votre pile personnalisée.", + "xpack.apm.onboarding.node.configure.textPre": "Les agents sont des bibliothèques exécutées dans les processus de votre application. Les services APM sont créés par programmation à partir du `serviceName`. Cet agent prend en charge de nombreux frameworks, mais peut également être utilisé avec votre pile personnalisée.", "xpack.apm.onboarding.node.configure.title": "Configurer l'agent", "xpack.apm.onboarding.node.install.textPre": "Installez l'agent APM pour Node.js comme dépendance de votre application.", "xpack.apm.onboarding.node.install.title": "Installer l'agent APM", "xpack.apm.onboarding.nodeClient.configure.commands.addThisToTheFileTopComment": "Ajoutez ceci tout en haut du premier fichier chargé dans votre application", "xpack.apm.onboarding.nodeClient.createConfig.commands.serviceName": "Remplace le nom de service dans package.json.", "xpack.apm.onboarding.otel.column.value.copyIconText": "Copier dans le presse-papiers", - "xpack.apm.onboarding.otel.configureAgent.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.\n\n", + "xpack.apm.onboarding.otel.configureAgent.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.", "xpack.apm.onboarding.otel.configureAgent.textPre": "Spécifiez les paramètres OpenTelemetry suivants dans le cadre du démarrage de votre application. Notez que les SDK OpenTelemetry exigent du code de démarrage en plus de ces paramètres de configuration. Pour plus de détails, consultez la [documentation OpenTelemetry Elastic]({openTelemetryDocumentationLink}) et les [guides d'instrumentation OpenTelemetry]({openTelemetryInstrumentationLink}).", "xpack.apm.onboarding.otel.configureAgent.title": "Configurer OpenTelemetry dans votre application", "xpack.apm.onboarding.otel.download.textPre": "Consultez les [guides d'instrumentation OpenTelemetry]({openTelemetryInstrumentationLink}) pour télécharger l'agent ou le SDK OpenTelemetry pour votre langage.", "xpack.apm.onboarding.otel.download.title": "Télécharger l’agent APM OpenTelemetry ou le SDK", "xpack.apm.onboarding.php.Configure the agent.textPre": "APM se lance automatiquement au démarrage de l'application. Configurez l'agent soit depuis le fichier `php.ini` :", - "xpack.apm.onboarding.php.configureAgent.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.\n\n", + "xpack.apm.onboarding.php.configureAgent.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.", "xpack.apm.onboarding.php.configureAgent.title": "Configurer l'agent", "xpack.apm.onboarding.php.download.textPre": "Téléchargez le package correspondant à votre plateforme depuis [GitHub releases]({githubReleasesLink}).", "xpack.apm.onboarding.php.download.title": "Télécharger l'agent APM", @@ -10724,7 +11575,7 @@ "xpack.apm.onboarding.php.installPackage.title": "Installer le package téléchargé", "xpack.apm.onboarding.rack.configure.commands.optionalComment": "facultatif, la valeur par défaut est config/elastic_apm.yml", "xpack.apm.onboarding.rack.configure.commands.requiredComment": "requis", - "xpack.apm.onboarding.rack.configure.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.\n\n", + "xpack.apm.onboarding.rack.configure.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.", "xpack.apm.onboarding.rack.configure.textPre": "Pour Rack, ou un framework compatible (par ex., Sinatra), intégrez l'intergiciel à votre application et démarrez l'agent.", "xpack.apm.onboarding.rack.configure.title": "Configurer l'agent", "xpack.apm.onboarding.rack.createConfig.textPre": "Créez un fichier config {configFile} :", @@ -10732,7 +11583,7 @@ "xpack.apm.onboarding.rack.install.textPre": "Ajoutez l'agent à votre Gemfile.", "xpack.apm.onboarding.rack.install.title": "Installer l'agent APM", "xpack.apm.onboarding.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment": "La valeur par défaut est le nom de la classe de votre application Rack.", - "xpack.apm.onboarding.rails.configure.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.\n\n", + "xpack.apm.onboarding.rails.configure.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.", "xpack.apm.onboarding.rails.configure.textPre": "APM se lance automatiquement au démarrage de l'application. Configurer l'agent, en créant le fichier config {configFile}", "xpack.apm.onboarding.rails.configure.title": "Configurer l'agent", "xpack.apm.onboarding.rails.install.textPre": "Ajoutez l'agent à votre Gemfile.", @@ -10743,6 +11594,8 @@ "xpack.apm.onboarding.shared_clients.configure.commands.serverUrlHint": "Définir l'URL personnalisée du serveur APM (par défaut : {defaultApmServerUrl}). L'URL doit être complète et inclure le protocole (http ou https) et le port.", "xpack.apm.onboarding.shared_clients.configure.commands.serviceEnvironmentHint": "Le nom de l'environnement dans lequel ce service est déployé, par exemple \"production\" ou \"test\". Les environnements vous permettent de facilement filtrer les données à un niveau global dans l'interface utilisateur APM. Il est important de garantir la cohérence des noms d'environnements entre les différents agents.", "xpack.apm.onboarding.shared_clients.configure.commands.serviceNameHint": "Le nom de service est le filtre principal dans l'interface utilisateur APM et est utilisé pour regrouper les erreurs et suivre les données ensemble. Caractères autorisés : a-z, A-Z, 0-9, -, _ et espace.", + "xpack.apm.onboarding.specProvider.learnMoreAriaLabel": "En savoir plus sur APM", + "xpack.apm.onboarding.specProvider.learnMoreLabel": "En savoir plus", "xpack.apm.onboarding.specProvider.longDescription": "Le monitoring des performances applicatives (APM) collecte les indicateurs et les erreurs de performance approfondies depuis votre application. Cela vous permet de monitorer les performances de milliers d'applications en temps réel. {learnMoreLink}.", "xpack.apm.percentOfParent": "({value} de {parentType, select, transaction { transaction } trace {trace} other {parentType inconnu} })", "xpack.apm.profiling.callout.description": "Universal Profiling fournit une visibilité sans précédent du code au milieu du comportement en cours d'exécution de toutes les applications. La fonctionnalité profile chaque ligne de code chez le ou les hôtes qui exécutent vos services, y compris votre code applicatif, le kernel et même les bibliothèque tierces.", @@ -10877,6 +11730,7 @@ "xpack.apm.serviceGroups.groupDetailsForm.description.optional": "Facultatif", "xpack.apm.serviceGroups.groupDetailsForm.edit.title": "Modifier un groupe", "xpack.apm.serviceGroups.groupDetailsForm.invalidColorError": "Veuillez fournir une valeur HEX de couleur valide", + "xpack.apm.serviceGroups.groupDetailsForm.invalidNameError": "Veuillez fournir une valeur de nom valide", "xpack.apm.serviceGroups.groupDetailsForm.name": "Nom", "xpack.apm.serviceGroups.groupDetailsForm.selectServices": "Sélectionner des services", "xpack.apm.serviceGroups.groupsCount": "{servicesCount} {servicesCount, plural, =0 {groupe} one {groupe} other {groupes}}", @@ -10916,13 +11770,13 @@ "xpack.apm.serviceIcons.serverless": "Sans serveur", "xpack.apm.serviceIcons.service": "Service", "xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "Architecture", - "xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, =0 {Zone de disponibilité} one {Zone de disponibilité} other {Zones de disponibilité}} ", - "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, =0 {Type de déclencheur} one {Type de déclencheur} other {Types de déclencheurs}} ", - "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, =0 {Nom de fonction} one {Nom de fonction} other {Noms de fonction}} ", - "xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, =0{Type de machine} one {Type de machine} other {Types de machines}} ", + "xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, =0 {Zone de disponibilité} one {Zone de disponibilité} other {Zones de disponibilité}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, =0 {Type de déclencheur} one {Type de déclencheur} other {Types de déclencheurs}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, =0 {Nom de fonction} one {Nom de fonction} other {Noms de fonction}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, =0{Type de machine} one {Type de machine} other {Types de machines}}", "xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "ID de projet", "xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "Fournisseur cloud", - "xpack.apm.serviceIcons.serviceDetails.cloud.regionLabel": "{regions, plural, =0 {Region} one {Région} other {Régions}} ", + "xpack.apm.serviceIcons.serviceDetails.cloud.regionLabel": "{regions, plural, =0 {Region} one {Région} other {Régions}}", "xpack.apm.serviceIcons.serviceDetails.cloud.serviceNameLabel": "Service Cloud", "xpack.apm.serviceIcons.serviceDetails.container.image.name": "Images de conteneurs", "xpack.apm.serviceIcons.serviceDetails.container.os.label": "Système d'exploitation", @@ -10973,7 +11827,6 @@ "xpack.apm.serviceMap.zoomIn": "Zoom avant", "xpack.apm.serviceMap.zoomOut": "Zoom arrière", "xpack.apm.serviceMetrics.loading": "Chargement des indicateurs", - "xpack.apm.serviceNamesSelectCustomOptionText": "Ajouter \\{searchValue\\} en tant que nouveau nom de service", "xpack.apm.serviceNamesSelectPlaceholder": "Sélectionner le nom du service", "xpack.apm.serviceNodeMetrics.containerId": "ID de conteneur", "xpack.apm.serviceNodeMetrics.host": "Hôte", @@ -11066,10 +11919,24 @@ "xpack.apm.servicesTable.tooltip.metricsExplanation": "Les indicateurs du services sont agrégés sur le type de transaction, qui peut être une requête ou un chargement de page. Si ni l'un ni l'autre n'existent, les indicateurs sont agrégés sur le type de transaction disponible en premier.", "xpack.apm.servicesTable.transactionColumnLabel": "Type de transaction", "xpack.apm.servicesTable.transactionErrorRate": "Taux de transactions ayant échoué", + "xpack.apm.serviceTabEmptyState.dependenciesContent": "Visualisez les dépendances de votre service sur les services internes et tiers en instrumentant avec APM.", + "xpack.apm.serviceTabEmptyState.dependenciesTitle": "Comprendre les dépendances de votre service", + "xpack.apm.serviceTabEmptyState.errorGroupOverviewContent": "Analysez les erreurs jusqu'à la transaction spécifique pour identifier les erreurs spécifiques au sein de votre service.", + "xpack.apm.serviceTabEmptyState.errorGroupOverviewTitle": "Identifier les erreurs de transaction liées à vos applications", + "xpack.apm.serviceTabEmptyState.infrastructureContent": "Résolvez les problèmes de service en visualisant l'infrastructure sur laquelle votre service s'exécute.", + "xpack.apm.serviceTabEmptyState.infrastructureTitle": "Comprendre sur quoi s'exécute votre service", + "xpack.apm.serviceTabEmptyState.metricsContent": "Collectez des indicateurs tels que l'utilisation du processeur et de la mémoire pour identifier les goulots d'étranglement des performances qui pourraient affecter vos utilisateurs.", + "xpack.apm.serviceTabEmptyState.metricsTitle": "Afficher les indicateurs clés de votre application", + "xpack.apm.serviceTabEmptyState.overviewContent": "Comprendre les performances, les relations et les dépendances de vos applications en instrumentant avec APM.", + "xpack.apm.serviceTabEmptyState.overviewTitle": "Détectez et résolvez les problèmes plus rapidement grâce à une meilleure visibilité sur votre application", + "xpack.apm.serviceTabEmptyState.serviceMapContent": "Consultez en un coup d'œil les dépendances de vos services pour vous aider à identifier celles susceptibles d'affecter votre service.", + "xpack.apm.serviceTabEmptyState.serviceMapTitle": "Visualiser les dépendances reliant vos services", + "xpack.apm.serviceTabEmptyState.transactionsContent": "Résolvez les problèmes liés aux performances de votre service en analysant la latence, le débit et les erreurs jusqu'à la transaction spécifique.", + "xpack.apm.serviceTabEmptyState.transactionsTitle": "Résoudre les problèmes liés à la latence, au débit et aux erreurs", "xpack.apm.settings.agentConfig": "Configuration de l'agent", "xpack.apm.settings.agentConfig.createConfigButton.tooltip": "Vous ne disposez pas d'autorisations pour créer des configurations d'agent", "xpack.apm.settings.agentConfig.descriptionText": "Affinez votre configuration d'agent depuis l'application APM. Les modifications sont automatiquement propagées à vos agents APM, ce qui vous évite d'effectuer un redéploiement.", - "xpack.apm.settings.agentConfiguration.all.option.calloutTitle": "Ce changement de configuration aura un impact sur tous les services, à l'exception de ceux qui utilisent un agent OpenTelemetry. ", + "xpack.apm.settings.agentConfiguration.all.option.calloutTitle": "Ce changement de configuration aura un impact sur tous les services, à l'exception de ceux qui utilisent un agent OpenTelemetry.", "xpack.apm.settings.agentConfiguration.service.otel.error": "Les services sélectionnés utilisent un agent OpenTelemetry, qui n'est pas pris en charge", "xpack.apm.settings.agentExplorer": "Explorateur d'agent", "xpack.apm.settings.agentExplorer.descriptionText": "L'explorateur d'agent fournit un inventaire des agents déployés et des détails les concernant.", @@ -11104,6 +11971,7 @@ "xpack.apm.settings.agentKeys.emptyPromptTitle": "Créer votre première clé", "xpack.apm.settings.agentKeys.invalidate.failed": "Erreur lors de la suppression de la clé de l'agent APM \"{name}\"", "xpack.apm.settings.agentKeys.invalidate.succeeded": "Suppression de la clé de l'agent APM \"{name}\"", + "xpack.apm.settings.agentKeys.noPermissionCreateAgentKeyTooltipLabel": "Votre rôle d'utilisateur ne dispose pas d'autorisations pour créer des clés d'agent.", "xpack.apm.settings.agentKeys.noPermissionToManagelApiKeysDescription": "Contactez votre administrateur système", "xpack.apm.settings.agentKeys.noPermissionToManagelApiKeysTitle": "Vous devez disposer d'une autorisation pour gérer les clés d'API", "xpack.apm.settings.agentKeys.table.creationColumnName": "Créé", @@ -11136,6 +12004,7 @@ "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText": "Pour ajouter la détection des anomalies à un nouvel environnement, créez une tâche de Machine Learning. Vous pouvez gérer les tâches de Machine Learning existantes dans {mlJobsLink}.", "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText": "Machine Learning", "xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText": "Gérer la tâche", + "xpack.apm.settings.anomalyDetection.jobList.noPermissionAddEnvironmentsTooltipLabel": "Votre rôle d'utilisateur ne dispose pas d'autorisations pour créer des tâches", "xpack.apm.settings.anomalyDetection.jobList.okStatusLabel": "OK", "xpack.apm.settings.anomalyDetection.jobList.openAnomalyExplorerrLinkText": "Ouvrir dans Anomaly Explorer", "xpack.apm.settings.anomalyDetection.jobList.showLegacyJobsCheckboxText": "Afficher les tâches héritées", @@ -11343,6 +12212,8 @@ "xpack.apm.storageExplorer.table.samplingColumnName": "Taux d’échantillonnage", "xpack.apm.storageExplorer.table.serviceColumnName": "Service", "xpack.apm.storageExplorerLinkLabel": "Explorateur de stockage", + "xpack.apm.subFeatureRegistry.modifySettings": "Possibilité de modifier les paramètres", + "xpack.apm.subFeatureRegistry.settings": "Paramètres", "xpack.apm.technicalPreviewBadgeDescription": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera de corriger tout problème, mais les fonctionnalités des versions d'évaluation technique ne sont pas soumises aux SLA de support des fonctionnalités officielles en disponibilité générale.", "xpack.apm.technicalPreviewBadgeLabel": "Version d'évaluation technique", "xpack.apm.timeComparison.label": "Comparaison", @@ -11356,6 +12227,7 @@ "xpack.apm.traceExplorer.appName": "APM", "xpack.apm.traceExplorer.criticalPathTab": "Chemin critique agrégé", "xpack.apm.traceExplorer.waterfallTab": "Cascade", + "xpack.apm.traceLink.fetchingTraceLabel": "Récupération des traces...", "xpack.apm.traceOverview.topTracesTab": "Premières traces", "xpack.apm.traceOverview.traceExplorerTab": "Explorer", "xpack.apm.traceSearchBox.refreshButton": "Recherche", @@ -11487,7 +12359,6 @@ "xpack.apm.transactionsTable.tableSearch.placeholder": "Rechercher des transactions par nom", "xpack.apm.transactionsTable.title": "Transactions", "xpack.apm.transactionsTableColumnName.alertsColumnLabel": "Alertes actives", - "xpack.apm.transactionTypesSelectCustomOptionText": "Ajouter \\{searchValue\\} en tant que nouveau type de transaction", "xpack.apm.transactionTypesSelectPlaceholder": "Sélectionner le type de transaction", "xpack.apm.tryItButton.euiButtonIcon.admin": "Veuillez référer à votre administrateur pour rendre cette fonctionnalité {featureEnabled}.", "xpack.apm.tryItButton.euiButtonIcon.admin.off": "désactivé", @@ -11509,7 +12380,7 @@ "xpack.apm.tutorial.apmAgents.statusCheck.text": "Vérifiez que votre application est en cours d'exécution et que les agents envoient les données.", "xpack.apm.tutorial.apmAgents.statusCheck.title": "Statut de l'agent", "xpack.apm.tutorial.apmAgents.title": "Agents APM", - "xpack.apm.tutorial.apmServer.callOut.message": "Assurez-vous de mettre à jour votre serveur APM vers la version 7.0 ou supérieure. Vous pouvez également migrer vos données 6.x à l'aide de l'assistant de migration disponible dans la section de gestion de Kibana.", + "xpack.apm.tutorial.apmServer.callOut.message": "Assurez-vous de mettre à jour votre serveur APM vers la version 7.0 ou supérieure. Vous pouvez également migrer vos données 6.x à l'aide de l'assistant de migration disponible dans la section de gestion de Kibana.", "xpack.apm.tutorial.apmServer.callOut.title": "Important : mise à niveau vers la version 7.0 ou supérieure", "xpack.apm.tutorial.apmServer.fleet.apmIntegration.button": "Intégration APM", "xpack.apm.tutorial.apmServer.fleet.apmIntegration.description": "Fleet vous permet de gérer de manière centralisée les agents Elastic qui exécutent l'intégration APM. L'option par défaut consiste à installer un serveur Fleet sur un hôte dédié. Pour les configurations sans hôte dédié, nous vous recommandons de suivre les instructions pour installer le serveur APM autonome pour votre système d'exploitation en sélectionnant l'onglet correspondant ci-dessus.", @@ -11537,13 +12408,13 @@ "xpack.apm.tutorial.djangoClient.configure.title": "Configurer l'agent", "xpack.apm.tutorial.djangoClient.install.textPre": "Installez l'agent APM pour Python en tant que dépendance.", "xpack.apm.tutorial.djangoClient.install.title": "Installer l'agent APM", - "xpack.apm.tutorial.dotNetClient.configureAgent.textPost": "Si vous ne transférez pas une instance `IConfiguration` à l'agent (par ex., pour les applications non ASP.NET Core) vous pouvez également configurer l'agent par le biais de variables d'environnement. \n Pour une utilisation avancée, consultez [the documentation]({documentationLink}), qui comprend notamment le guide de démarrage rapide pour [Profiler Auto instrumentation]({profilerLink}).", + "xpack.apm.tutorial.dotNetClient.configureAgent.textPost": "Si vous ne transférez pas une instance `IConfiguration` à l'agent (par ex., pour les applications non ASP.NET Core) vous pouvez également configurer l'agent par le biais de variables d'environnement. Pour une utilisation avancée, consultez [the documentation]({documentationLink}), qui comprend notamment le guide de démarrage rapide pour [Profiler Auto instrumentation]({profilerLink}).", "xpack.apm.tutorial.dotNetClient.configureAgent.title": "Exemple de fichier appsettings.json :", - "xpack.apm.tutorial.dotNetClient.configureApplication.textPost": "La transmission d'une instance `IConfiguration` est facultative mais si cette opération est effectuée, l'agent lira les paramètres de configuration depuis cette instance `IConfiguration` (par ex. à partir du fichier `appsettings.json`).", - "xpack.apm.tutorial.dotNetClient.configureApplication.textPre": "Si vous utilisez ASP.NET Core avec le package `Elastic.Apm.NetCoreAll`, appelez la méthode `UseAllElasticApm` dans la méthode \"Configure\" dans le fichier `Startup.cs`.", + "xpack.apm.tutorial.dotNetClient.configureApplication.textPost": "La transmission d'une instance `IConfiguration` est facultative mais si cette opération est effectuée, l'agent lira les paramètres de configuration depuis cette instance `IConfiguration` (par ex. à partir du fichier `appsettings.json`).", + "xpack.apm.tutorial.dotNetClient.configureApplication.textPre": "Si vous utilisez ASP.NET Core avec le package `Elastic.Apm.NetCoreAll`, appelez la méthode `UseAllElasticApm` dans la méthode `Configure` dans le fichier `Startup.cs`.", "xpack.apm.tutorial.dotNetClient.configureApplication.title": "Ajouter l'agent à l'application", "xpack.apm.tutorial.dotnetClient.createConfig.commands.defaultServiceName": "La valeur par défaut est l'assemblage d'entrée de l'application.", - "xpack.apm.tutorial.dotNetClient.download.textPre": "Ajoutez le ou les packages d'agent depuis [NuGet]({allNuGetPackagesLink}) à votre application .NET. Plusieurs packages NuGet sont disponibles pour différents cas d'utilisation. \n\nPour une application ASP.NET Core avec Entity Framework Core, téléchargez le package [Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}). Ce package ajoutera automatiquement chaque composant d'agent à votre application. \n\n Si vous souhaitez minimiser les dépendances, vous pouvez utiliser le package [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) uniquement pour le monitoring d'ASP.NET Core ou le package [Elastic.Apm.EfCore]({efCorePackageLink}) uniquement pour le monitoring d'Entity Framework Core. \n\n Si vous souhaitez seulement utiliser l'API d'agent publique pour l'instrumentation manuelle, utilisez le package [Elastic.Apm]({elasticApmPackageLink}).", + "xpack.apm.tutorial.dotNetClient.download.textPre": "Ajoutez le ou les packages d'agent depuis [NuGet]({allNuGetPackagesLink}) à votre application .NET. Plusieurs packages NuGet sont disponibles pour différents cas d'utilisation. Pour une application ASP.NET Core avec Entity Framework Core, téléchargez le package [Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}). Ce package ajoutera automatiquement chaque composant d'agent à votre application. Si vous souhaitez minimiser les dépendances, vous pouvez utiliser le package [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) uniquement pour le monitoring d'ASP.NET Core ou le package [Elastic.Apm.EfCore]({efCorePackageLink}) uniquement pour le monitoring d'Entity Framework Core. Si vous souhaitez seulement utiliser l'API d'agent publique pour l'instrumentation manuelle, utilisez le package [Elastic.Apm]({elasticApmPackageLink}).", "xpack.apm.tutorial.dotNetClient.download.title": "Télécharger l'agent APM", "xpack.apm.tutorial.downloadServer.title": "Télécharger et décompresser le serveur APM", "xpack.apm.tutorial.downloadServerRpm": "Vous cherchez les packages aarch64 ? Consultez la [Download page]({downloadPageLink}).", @@ -11574,13 +12445,13 @@ "xpack.apm.tutorial.javaClient.download.textPre": "Téléchargez le fichier jar de l'agent depuis [Maven Central]({mavenCentralLink}). N'ajoutez **pas** l'agent comme dépendance de votre application.", "xpack.apm.tutorial.javaClient.download.title": "Télécharger l'agent APM", "xpack.apm.tutorial.javaClient.startApplication.textPost": "Consultez la [documentation]({documentationLink}) pour découvrir les options de configuration et l'utilisation avancée.", - "xpack.apm.tutorial.javaClient.startApplication.textPre": "Ajoutez l'indicateur `-javaagent` et configurez l'agent avec les propriétés du système.\n\n * Définir le nom de service requis (caractères autorisés : a-z, A-Z, 0-9, -, _ et espace)\n * Définir l'URL personnalisée du serveur APM (par défaut : {customApmServerUrl})\n * Définir le token secret du serveur APM\n * Définir l'environnement de service\n * Définir le package de base de votre application", + "xpack.apm.tutorial.javaClient.startApplication.textPre": "Ajoutez l'indicateur `-javaagent` et configurez l'agent avec les propriétés du système. * Définir le nom de service requis (caractères autorisés : a-z, A-Z, 0-9, -, _ et espace) * * Définir l'URL personnalisée du serveur APM (par défaut : {customApmServerUrl}) * Définir le jeton secret du serveur APM * Définir l'environnement de service * Définir le package de base de votre application", "xpack.apm.tutorial.javaClient.startApplication.title": "Lancer votre application avec l'indicateur javaagent", "xpack.apm.tutorial.jsClient.enableRealUserMonitoring.textPre": "Le serveur APM désactive la prise en charge du RUM par défaut. Consultez la [documentation]({documentationLink}) pour obtenir des détails sur l'activation de la prise en charge du RUM. Lorsque vous utilisez l'intégration APM avec Fleet, le support RUM est automatiquement activé.", "xpack.apm.tutorial.jsClient.enableRealUserMonitoring.title": "Activer la prise en charge du Real User Monitoring (monitoring des utilisateurs réels) dans le serveur APM", "xpack.apm.tutorial.jsClient.installDependency.commands.setServiceVersionComment": "Définir la version de service (requis pour la fonctionnalité source map)", "xpack.apm.tutorial.jsClient.installDependency.textPost": "Les intégrations de framework, tel que React ou Angular, ont des dépendances personnalisées. Consultez la [integration documentation]({docLink}) pour plus d'informations.", - "xpack.apm.tutorial.jsClient.installDependency.textPre": "Vous pouvez installer l'Agent comme dépendance de votre application avec `npm install @elastic/apm-rum --save`.\n\nVous pouvez ensuite initialiser l'agent et le configurer dans votre application de cette façon :", + "xpack.apm.tutorial.jsClient.installDependency.textPre": "Vous pouvez installer l'Agent comme dépendance de votre application avec `npm install @elastic/apm-rum --save`. Vous pouvez ensuite initialiser l'agent et le configurer dans votre application de cette façon :", "xpack.apm.tutorial.jsClient.installDependency.title": "Configurer l'agent comme dépendance", "xpack.apm.tutorial.jsClient.scriptTags.textPre": "Vous pouvez également utiliser les balises Script pour configurer l'agent. Ajoutez un indicateur `" }]; - afterEach(() => { - (useUiSetting$ as jest.Mock).mockReset(); - }); test('it filters empty object', () => { - (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksEmptyObj]); + mockUseUiSetting$.mockReturnValue([mockInvalidLinksEmptyObj]); const wrapper = mountWithIntl( @@ -405,7 +456,7 @@ describe('Custom Links', () => { }); test('it filters object without name property', () => { - (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksNoName]); + mockUseUiSetting$.mockReturnValue([mockInvalidLinksNoName]); const wrapper = mountWithIntl( @@ -414,7 +465,7 @@ describe('Custom Links', () => { }); test('it filters object without url_template property', () => { - (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksNoUrl]); + mockUseUiSetting$.mockReturnValue([mockInvalidLinksNoUrl]); const wrapper = mountWithIntl( @@ -423,7 +474,7 @@ describe('Custom Links', () => { }); test('it filters object with invalid url', () => { - (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidUrl]); + mockUseUiSetting$.mockReturnValue([mockInvalidUrl]); const wrapper = mountWithIntl( @@ -434,12 +485,7 @@ describe('Custom Links', () => { describe('external icon', () => { beforeAll(() => { - (useUiSetting$ as jest.Mock).mockReset(); - (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); - }); - - afterEach(() => { - (useUiSetting$ as jest.Mock).mockClear(); + mockUseUiSetting$.mockReturnValue([mockCustomizedReputationLinks]); }); test('it renders correct number of external icons by default', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 9d615d80be63f..0648dd60d84f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -8,11 +8,9 @@ import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiToolTip } from '@elastic/eui'; import type { SyntheticEvent, MouseEvent } from 'react'; -import React, { useMemo, useCallback, useEffect } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { isArray, isNil } from 'lodash/fp'; -import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step'; -import { AlertsCasesTourSteps, SecurityStepId } from '../guided_onboarding_tour/tour_config'; -import { useTourContext } from '../guided_onboarding_tour'; +import type { NavigateToAppOptions } from '@kbn/core-application-browser'; import { IP_REPUTATION_LINKS_SETTING, APP_UI_ID } from '../../../../common/constants'; import { encodeIpv6 } from '../../lib/helpers'; import { @@ -306,27 +304,19 @@ export interface CaseDetailsLinkComponentProps { */ title?: string; /** - * Link index + * If true, will open the app in new tab, will share session information via window.open if base */ - index?: number; + openInNewTab?: NavigateToAppOptions['openInNewTab']; } const CaseDetailsLinkComponent: React.FC = ({ - index, children, detailName, title, + openInNewTab = false, }) => { const { formatUrl, search } = useFormatUrl(SecurityPageName.case); const { navigateToApp } = useKibana().services.application; - const { activeStep, isTourShown } = useTourContext(); - const isTourStepActive = useMemo( - () => - activeStep === AlertsCasesTourSteps.viewCase && - isTourShown(SecurityStepId.alertsCases) && - index === 0, - [activeStep, index, isTourShown] - ); const goToCaseDetails = useCallback( async (ev?: SyntheticEvent) => { @@ -334,32 +324,21 @@ const CaseDetailsLinkComponent: React.FC = ({ return navigateToApp(APP_UI_ID, { deepLinkId: SecurityPageName.case, path: getCaseDetailsUrl({ id: detailName, search }), + openInNewTab, }); }, - [detailName, navigateToApp, search] + [detailName, navigateToApp, openInNewTab, search] ); - useEffect(() => { - if (isTourStepActive) - document.querySelector(`[tour-step="RelatedCases-accordion"]`)?.scrollIntoView(); - }, [isTourStepActive]); - return ( - - - {children ? children : detailName} - - + {children ? children : detailName} + ); }; export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx index 0fa09d4bf4354..60a19f005c53e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx @@ -63,7 +63,7 @@ export const useAddToCaseActions = ({ : []; }, [casesUi.helpers, ecsData, nonEcsData]); - const { activeStep, incrementStep, setStep, isTourShown } = useTourContext(); + const { activeStep, endTourStep, incrementStep, isTourShown } = useTourContext(); const onCaseSuccess = useCallback(() => { if (onSuccess) { @@ -77,9 +77,9 @@ export const useAddToCaseActions = ({ const afterCaseCreated = useCallback(async () => { if (isTourShown(SecurityStepId.alertsCases)) { - setStep(SecurityStepId.alertsCases, AlertsCasesTourSteps.viewCase); + endTourStep(SecurityStepId.alertsCases); } - }, [setStep, isTourShown]); + }, [endTourStep, isTourShown]); const prefillCasesValue = useMemo( () => diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/related_cases.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/related_cases.test.tsx index db9eb7bdfb3ae..48f7a1fbce0a6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/related_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/related_cases.test.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID, @@ -19,13 +18,26 @@ import { EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID, EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, } from '../../../shared/components/test_ids'; +import { SecurityPageName } from '@kbn/deeplinks-security'; +import { TestProviders } from '../../../../common/mock'; +import { APP_UI_ID } from '../../../../../common'; jest.mock('../../shared/hooks/use_fetch_related_cases'); -jest.mock('../../../../common/components/links', () => ({ - CaseDetailsLink: jest - .fn() - .mockImplementation(({ title }) => <>{``}), -})); + +const mockNavigateToApp = jest.fn(); +jest.mock('../../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../../common/lib/kibana'); + return { + ...original, + useKibana: () => ({ + services: { + application: { + navigateToApp: mockNavigateToApp, + }, + }, + }), + }; +}); const eventId = 'eventId'; @@ -41,13 +53,53 @@ const TITLE_TEXT = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID( const renderRelatedCases = () => render( - + - + ); describe('', () => { it('should render many related cases correctly', () => { + (useFetchRelatedCases as jest.Mock).mockReturnValue({ + loading: false, + error: false, + data: [ + { + id: 'id1', + title: 'title1', + description: 'description1', + status: 'open', + }, + { + id: 'id2', + title: 'title2', + description: 'description2', + status: 'in-progress', + }, + { + id: 'id3', + title: 'title3', + description: 'description3', + status: 'closed', + }, + ], + dataCount: 3, + }); + + const { getByTestId, getByText } = renderRelatedCases(); + expect(getByTestId(TOGGLE_ICON)).toBeInTheDocument(); + expect(getByTestId(TITLE_ICON)).toBeInTheDocument(); + expect(getByTestId(TITLE_TEXT)).toHaveTextContent('3 related cases'); + expect(getByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID)).toBeInTheDocument(); + expect(getByText('title1')).toBeInTheDocument(); + expect(getByText('open')).toBeInTheDocument(); + expect(getByText('title2')).toBeInTheDocument(); + expect(getByText('in-progress')).toBeInTheDocument(); + expect(getByText('title3')).toBeInTheDocument(); + expect(getByText('closed')).toBeInTheDocument(); + }); + + it('should open new tab when clicking on the case link', () => { (useFetchRelatedCases as jest.Mock).mockReturnValue({ loading: false, error: false, @@ -63,10 +115,12 @@ describe('', () => { }); const { getByTestId } = renderRelatedCases(); - expect(getByTestId(TOGGLE_ICON)).toBeInTheDocument(); - expect(getByTestId(TITLE_ICON)).toBeInTheDocument(); - expect(getByTestId(TITLE_TEXT)).toHaveTextContent('1 related case'); - expect(getByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID)).toBeInTheDocument(); + getByTestId('case-details-link').click(); + expect(mockNavigateToApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.case, + path: '/id', + openInNewTab: true, + }); }); it('should render null if error', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/related_cases.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/related_cases.tsx index 13df33a2deb1b..0ce04507b9b05 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/related_cases.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/related_cases.tsx @@ -7,9 +7,10 @@ import React, { useMemo } from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiInMemoryTable } from '@elastic/eui'; +import { EuiIcon, EuiInMemoryTable } from '@elastic/eui'; import type { RelatedCase } from '@kbn/cases-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import { CellTooltipWrapper } from '../../shared/components/cell_tooltip_wrapper'; import { CaseDetailsLink } from '../../../../common/components/links'; import { @@ -20,6 +21,10 @@ import { useFetchRelatedCases } from '../../shared/hooks/use_fetch_related_cases import { ExpandablePanel } from '../../../shared/components/expandable_panel'; const ICON = 'warning'; +const EXPAND_PROPERTIES = { + expandable: true, + expandedOnFirstRender: true, +}; const getColumns: (data: RelatedCase[]) => Array> = (data) => [ { @@ -30,16 +35,21 @@ const getColumns: (data: RelatedCase[]) => Array ), - render: (value: string, caseData: RelatedCase) => { - const index = data.findIndex((d) => d.id === caseData.id); - return ( - - - {caseData.title} - - - ); - }, + render: (_: string, caseData: RelatedCase) => ( + + + {caseData.title} + + + + ), }, { field: 'status', @@ -62,33 +72,38 @@ export interface RelatedCasesProps { } /** - * + * Show related cases in an expandable panel with a table */ export const RelatedCases: React.FC = ({ eventId }) => { const { loading, error, data, dataCount } = useFetchRelatedCases({ eventId }); const columns = useMemo(() => getColumns(data), [data]); + const title = useMemo( + () => ( + + ), + [dataCount] + ); + const header = useMemo( + () => ({ + title, + iconType: ICON, + }), + [title] + ); + if (error) { return null; } return ( - ), - iconType: ICON, - }} - content={{ error }} - expand={{ - expandable: true, - expandedOnFirstRender: true, - }} + header={header} + expand={EXPAND_PROPERTIES} data-test-subj={CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID} > ', () => { jest.mocked(useTourContext).mockReturnValue({ hidden: false, setAllTourStepsHidden: jest.fn(), - activeStep: AlertsCasesTourSteps.viewCase, + activeStep: AlertsCasesTourSteps.submitCase, endTourStep: jest.fn(), incrementStep: jest.fn(), isTourShown: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx index 9ba55f0d041f6..4043230d5269e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx @@ -73,7 +73,7 @@ export const CorrelationsOverview: React.FC = () => { }, [eventId, openLeftPanel, indexName, scopeId]); useEffect(() => { - if (isTourShown(SecurityStepId.alertsCases) && activeStep === AlertsCasesTourSteps.viewCase) { + if (isTourShown(SecurityStepId.alertsCases) && activeStep === AlertsCasesTourSteps.createCase) { goToCorrelationsTab(); } }, [activeStep, goToCorrelationsTab, isTourShown]); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx index 96dff8150e654..c06481c6b2812 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx @@ -171,7 +171,7 @@ describe('', () => { it('should render the component expanded if guided onboarding tour is shown', () => { (useExpandSection as jest.Mock).mockReturnValue(false); - mockUseTourContext.mockReturnValue({ activeStep: 7, isTourShown: jest.fn(() => true) }); + mockUseTourContext.mockReturnValue({ activeStep: 5, isTourShown: jest.fn(() => true) }); const contextValue = { eventId: 'some_Id', diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.tsx index 19c75a77cbabf..c2d71ee37baa8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.tsx @@ -35,7 +35,7 @@ export const InsightsSection = memo(() => { const { activeStep, isTourShown } = useTourContext(); const isGuidedOnboardingTourShown = - isTourShown(SecurityStepId.alertsCases) && activeStep === AlertsCasesTourSteps.viewCase; + isTourShown(SecurityStepId.alertsCases) && activeStep === AlertsCasesTourSteps.createCase; const expanded = useExpandSection({ title: KEY, defaultValue: false }) || isGuidedOnboardingTourShown; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 03206b7d39659..a099dbfe0762f 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -41341,8 +41341,6 @@ "xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourTitle": "Examiner le tableau d'alertes", "xpack.securitySolution.guided_onboarding.tour.submitCase.tourContent": "Appuyez sur \"Créer un cas\" pour continuer.", "xpack.securitySolution.guided_onboarding.tour.submitCase.tourTitle": "Créer un cas", - "xpack.securitySolution.guided_onboarding.tour.viewCase.tourContent": "Les cas s'affichent sous Insights, dans les détails de l'alerte.", - "xpack.securitySolution.guided_onboarding.tour.viewCase.tourTitle": "Afficher le cas", "xpack.securitySolution.handleInputAreaState.inputPlaceholderText": "Soumettre l’action de réponse", "xpack.securitySolution.header.editableTitle.cancel": "Annuler", "xpack.securitySolution.header.editableTitle.editButtonAria": "Vous pouvez modifier {title} en cliquant", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1ebef4af4c8b7..ff458c867ee20 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -41306,8 +41306,6 @@ "xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourTitle": "アラートテーブルの検査", "xpack.securitySolution.guided_onboarding.tour.submitCase.tourContent": "[ケースの作成]を押して続行します。", "xpack.securitySolution.guided_onboarding.tour.submitCase.tourTitle": "ケースを作成", - "xpack.securitySolution.guided_onboarding.tour.viewCase.tourContent": "ケースはアラート詳細の[インサイト]の下に表示されます。", - "xpack.securitySolution.guided_onboarding.tour.viewCase.tourTitle": "ケースを表示", "xpack.securitySolution.handleInputAreaState.inputPlaceholderText": "対応アクションを送信", "xpack.securitySolution.header.editableTitle.cancel": "キャンセル", "xpack.securitySolution.header.editableTitle.editButtonAria": "クリックすると {title} を編集できます", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5929febfb1b65..15c67c3178f60 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -41377,8 +41377,6 @@ "xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourTitle": "检查告警表", "xpack.securitySolution.guided_onboarding.tour.submitCase.tourContent": "按“创建案例”继续。", "xpack.securitySolution.guided_onboarding.tour.submitCase.tourTitle": "创建案例", - "xpack.securitySolution.guided_onboarding.tour.viewCase.tourContent": "在告警详情中,案例在洞见下显示。", - "xpack.securitySolution.guided_onboarding.tour.viewCase.tourTitle": "查看案例", "xpack.securitySolution.handleInputAreaState.inputPlaceholderText": "提交响应操作", "xpack.securitySolution.header.editableTitle.cancel": "取消", "xpack.securitySolution.header.editableTitle.editButtonAria": "通过单击,可以编辑 {title}", diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/guided_onboarding/tour.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/guided_onboarding/tour.cy.ts index 39dd027bdd86d..a2c355caeb842 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/guided_onboarding/tour.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/guided_onboarding/tour.cy.ts @@ -69,7 +69,6 @@ describe('Guided onboarding tour', { tags: ['@ess'] }, () => { const stepsInAlertsFlyout = [ AlertsCasesTourSteps.reviewAlertDetailsFlyout, AlertsCasesTourSteps.addAlertToCase, - AlertsCasesTourSteps.viewCase, ]; const stepsInCasesFlyout = [AlertsCasesTourSteps.createCase, AlertsCasesTourSteps.submitCase]; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/guided_onboarding.ts b/x-pack/test/security_solution_cypress/cypress/tasks/guided_onboarding.ts index fe3170b31e951..d977dbc5cc9c3 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/guided_onboarding.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/guided_onboarding.ts @@ -40,7 +40,6 @@ export const completeTourWithNextButton = () => { goToNextStep(i); } createCase(); - goToNextStep(7); }; export const addToCase = () => { @@ -55,7 +54,6 @@ export const completeTourWithActions = () => { addToCase(); goToNextStep(5); createCase(); - goToNextStep(7); }; export const goToStep = (step: number) => { From 398a6ab00d14f32e306ea4dd333d2d48d5522508 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 06:12:01 +1100 Subject: [PATCH 35/47] [8.x] [ML][AIOPS] Log rate analysis: add basic functional test (#199054) (#199354) # Backport This will backport the following commits from `main` to `8.x`: - [[ML][AIOPS] Log rate analysis: add basic functional test (#199054)](https://github.com/elastic/kibana/pull/199054) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Melissa Alvarez --- x-pack/test/functional/apps/aiops/log_rate_analysis.ts | 6 ++++++ .../test_data/farequote_data_view_test_data_with_query.ts | 1 + x-pack/test/functional/apps/aiops/types.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis.ts index d6acad691b195..8ffbea4f1a0b0 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis.ts @@ -167,6 +167,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { testData.dataGenerator ); + if (testData.editedQuery && testData.query) { + await aiops.logRateAnalysisPage.setQueryInput(testData.editedQuery); + await aiops.logRateAnalysisPage.assertRerunAnalysisButtonExists(true); + await aiops.logRateAnalysisPage.setQueryInput(testData.query); + } + // At this stage the baseline and deviation brush position should be stored in // the url state and a full browser refresh should restore the analysis. await browser.refresh(); diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data_with_query.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data_with_query.ts index e5a8b4783d7bb..8a9957a82b89a 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data_with_query.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data_with_query.ts @@ -23,6 +23,7 @@ export const farequoteDataViewTestDataWithQuery: TestData = { fieldSelectorSearch: 'airline', fieldSelectorApplyAvailable: true, query: 'NOT airline:("SWR" OR "ACA" OR "AWE" OR "BAW" OR "JAL" OR "JBU" OR "JZA" OR "KLM")', + editedQuery: 'NOT airline:("SWR")', expected: { totalDocCountFormatted: '48,799', analysisGroupsTable: [ diff --git a/x-pack/test/functional/apps/aiops/types.ts b/x-pack/test/functional/apps/aiops/types.ts index aeac2ce33f718..bf72b9d987030 100644 --- a/x-pack/test/functional/apps/aiops/types.ts +++ b/x-pack/test/functional/apps/aiops/types.ts @@ -68,6 +68,7 @@ export interface TestData { fieldSelectorSearch: string; fieldSelectorApplyAvailable: boolean; query?: string; + editedQuery?: string; action?: TestDataTableActionLogPatternAnalysis; expected: TestDataExpectedWithSampleProbability | TestDataExpectedWithoutSampleProbability; } From f86b6469fbdcc28e714252c38fed5280831da05e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 06:27:11 +1100 Subject: [PATCH 36/47] skip failing test suite (#181889) --- .../plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts index 2b04a99bd4f9c..6ea1acfebabce 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts @@ -18,7 +18,8 @@ import { import { closeModalIfVisible, closeToastIfVisible } from '../../tasks/integrations'; import { RESULTS_TABLE, RESULTS_TABLE_BUTTON } from '../../screens/live_query'; -describe( +// Failing: See https://github.com/elastic/kibana/issues/181889 +describe.skip( 'Alert Event Details', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'], From b0e7ce2dd4bd08c51653fc6bcb2ce721b1cb8381 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 06:27:52 +1100 Subject: [PATCH 37/47] skip failing test suite (#192128) --- x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts index 4bafc3d173156..d6f9f1a9fe4fd 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts @@ -18,7 +18,8 @@ import { typeInOsqueryFieldInput, } from '../../tasks/live_query'; -describe('EcsMapping', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/192128 +describe.skip('EcsMapping', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { beforeEach(() => { initializeDataViews(); }); From c93641b39f0307d013350c9880630e6670dfed3d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 7 Nov 2024 19:36:34 +0000 Subject: [PATCH 38/47] skip flaky suite (#184600) --- test/functional/apps/discover/group3/_lens_vis.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/group3/_lens_vis.ts b/test/functional/apps/discover/group3/_lens_vis.ts index 0864382cad7a8..03641ee5bcb41 100644 --- a/test/functional/apps/discover/group3/_lens_vis.ts +++ b/test/functional/apps/discover/group3/_lens_vis.ts @@ -110,7 +110,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return seriesType; } - describe('discover lens vis', function () { + // FLAKY: https://github.com/elastic/kibana/issues/184600 + describe.skip('discover lens vis', function () { before(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); From 9378facf001ab9ac4c5a4d922c1f8551f7d1d94d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 06:54:53 +1100 Subject: [PATCH 39/47] [8.x] [Reporting] update puppeteer to version 23.7.0 (#199304) (#199357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Reporting] update puppeteer to version 23.7.0 (#199304)](https://github.com/elastic/kibana/pull/199304) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sébastien Loix --- package.json | 4 +- .../kbn-screenshotting-server/src/paths.ts | 34 +- yarn.lock | 474 +++--------------- 3 files changed, 97 insertions(+), 415 deletions(-) diff --git a/package.json b/package.json index 1a58f5d8c87a1..972023dcf1c33 100644 --- a/package.json +++ b/package.json @@ -1197,13 +1197,13 @@ "p-settle": "4.1.1", "papaparse": "^5.2.0", "pbf": "3.2.1", - "pdfmake": "^0.2.7", + "pdfmake": "^0.2.15", "peggy": "^1.2.0", "polished": "^3.7.2", "pretty-ms": "6.0.0", "prop-types": "^15.8.1", "proxy-from-env": "1.0.0", - "puppeteer": "23.3.1", + "puppeteer": "23.7.0", "query-string": "^6.13.2", "rbush": "^3.0.1", "re-resizable": "^6.9.9", diff --git a/packages/kbn-screenshotting-server/src/paths.ts b/packages/kbn-screenshotting-server/src/paths.ts index 9e8200c0839ab..e4c5a89d77627 100644 --- a/packages/kbn-screenshotting-server/src/paths.ts +++ b/packages/kbn-screenshotting-server/src/paths.ts @@ -46,10 +46,10 @@ export class ChromiumArchivePaths { platform: 'darwin', architecture: 'x64', archiveFilename: 'chrome-mac.zip', - archiveChecksum: '0a3d18efd00b3406f66139a673616b4b2b4b00323776678cb82295996f5a6733', - binaryChecksum: '8bcdaa973ee11110f6b70eaac2418fda3bb64446cf37f964fce331cdc8907a20', + archiveChecksum: '04f0132019c15660eea0b9d261fd14940c33b625c253689fcb5b09d58c4dbfe7', + binaryChecksum: 'a3ada6874ee052c096f09481fba75fcdabb96a8a9ad94a96949946a2485feccf', binaryRelativePath: 'chrome-mac/Chromium.app/Contents/MacOS/Chromium', - revision: 1331485, // 1331488 is not available for Mac_x64 + revision: 1355985, location: 'common', archivePath: 'Mac', isPreInstalled: false, @@ -58,10 +58,10 @@ export class ChromiumArchivePaths { platform: 'darwin', architecture: 'arm64', archiveFilename: 'chrome-mac.zip', - archiveChecksum: '426eddf16acb88b9446a91de53cc4364c7d487414248f33e30f68cf488cea0c0', - binaryChecksum: '827931739bfdd2b6790a81d5ade8886c159cd051581d79b84d1ede447293e9cf', + archiveChecksum: '6c75bb645696aed0e60b17e0e50423b97d21ca11f2c5cdfbaf17edbf582cec94', + binaryChecksum: '2f819f59379917056e07d640f75b1dbe22a830c2655e32ab0543013b7198c139', binaryRelativePath: 'chrome-mac/Chromium.app/Contents/MacOS/Chromium', - revision: 1331488, + revision: 1355985, location: 'common', archivePath: 'Mac_Arm', isPreInstalled: false, @@ -69,22 +69,22 @@ export class ChromiumArchivePaths { { platform: 'linux', architecture: 'x64', - archiveFilename: 'chromium-fe621c5-locales-linux_x64.zip', - archiveChecksum: '12ce2e0eac184072dfcbc7a267328e3eb7fbe10a682997f4111c0378f2397341', - binaryChecksum: '670481cfa8db209401106cd23051009d390c03608724d0822a12c8c0a92b4c25', + archiveFilename: 'chromium-53ac076-locales-linux_x64.zip', + archiveChecksum: '50424bf105710d184198484a8a666db414627596002dacf80e83b00c8da71115', + binaryChecksum: 'afbc87a7f946bd6df763ffffb38dd4d75ee50c28ba705ac177dc893030d20206', binaryRelativePath: 'headless_shell-linux_x64/headless_shell', - revision: 1331488, + revision: 1356013, location: 'custom', isPreInstalled: true, }, { platform: 'linux', architecture: 'arm64', - archiveFilename: 'chromium-fe621c5-locales-linux_arm64.zip', - archiveChecksum: 'f7333eaff5235046c8775f0c1a0b7395b7ebc2e054ea638710cf511c4b6f9daf', - binaryChecksum: '8a3a3371b3d04f4b0880b137a3611c223e0d8e65a218943cb7be1ec4a91f5e35', + archiveFilename: 'chromium-53ac076-locales-linux_arm64.zip', + archiveChecksum: '24ffa183a6bf355209f3960a2377a1f8cc75aef093fe1934fcc72d2a5f9a274b', + binaryChecksum: 'db1c0226e03dfc26a6d61e02a885912906529e8477ac3214962b160d1e99f25c', binaryRelativePath: 'headless_shell-linux_arm64/headless_shell', - revision: 1331488, + revision: 1356013, location: 'custom', isPreInstalled: true, }, @@ -92,10 +92,10 @@ export class ChromiumArchivePaths { platform: 'win32', architecture: 'x64', archiveFilename: 'chrome-win.zip', - archiveChecksum: 'fa62be702f55f37e455bab4291c59ceb40e81e1922d30cf9453a4ee176b909bc', - binaryChecksum: '1345e66583bad1a1f16885f381d1173de8bf931487da9ba155e1b58bf23b2c66', + archiveChecksum: 'f86aadca5d1ab02fc05b580f23a30ee02d34bd348f9a3f0032b7117027676727', + binaryChecksum: 'b7b98dd681dfea2333a0136ba5788e38010730bb2e42eafa291b16931f00449d', binaryRelativePath: path.join('chrome-win', 'chrome.exe'), - revision: 1331487, // 1331488 is not available for win32 + revision: 1355984, location: 'common', archivePath: 'Win', isPreInstalled: true, diff --git a/yarn.lock b/yarn.lock index 1cc7363a54841..a3e81d8a34f26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2327,15 +2327,13 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8" integrity sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ== -"@foliojs-fork/fontkit@^1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@foliojs-fork/fontkit/-/fontkit-1.9.1.tgz#8124649168eb5273f580f66697a139fb5041296b" - integrity sha512-U589voc2/ROnvx1CyH9aNzOQWJp127JGU1QAylXGQ7LoEAF6hMmahZLQ4eqAcgHUw+uyW4PjtCItq9qudPkK3A== +"@foliojs-fork/fontkit@^1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz#94241c195bc6204157bc84c33f34bdc967eca9c3" + integrity sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA== dependencies: "@foliojs-fork/restructure" "^2.0.2" - brfs "^2.0.0" brotli "^1.2.0" - browserify-optional "^1.0.1" clone "^1.0.4" deep-equal "^1.0.0" dfa "^1.2.0" @@ -2343,23 +2341,23 @@ unicode-properties "^1.2.2" unicode-trie "^2.0.0" -"@foliojs-fork/linebreak@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@foliojs-fork/linebreak/-/linebreak-1.1.1.tgz#93ecd695b7d2bb0334b9481058c3e610e019a4eb" - integrity sha512-pgY/+53GqGQI+mvDiyprvPWgkTlVBS8cxqee03ejm6gKAQNsR1tCYCIvN9FHy7otZajzMqCgPOgC4cHdt4JPig== +"@foliojs-fork/linebreak@^1.1.1", "@foliojs-fork/linebreak@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz#32fee03d5431fa73284373439e172e451ae1e2da" + integrity sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg== dependencies: base64-js "1.3.1" - brfs "^2.0.2" unicode-trie "^2.0.0" -"@foliojs-fork/pdfkit@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@foliojs-fork/pdfkit/-/pdfkit-0.13.0.tgz#54f5368d8cf74d8edc81a175ccda1fd9655f2db9" - integrity sha512-YXeG1fml9k97YNC9K8e292Pj2JzGt9uOIiBFuQFxHsdQ45BlxW+JU3RQK6JAvXU7kjhjP8rCcYvpk36JLD33sQ== +"@foliojs-fork/pdfkit@^0.15.1": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@foliojs-fork/pdfkit/-/pdfkit-0.15.1.tgz#ecae3bcb7aad46b58e50493de593317f9b738074" + integrity sha512-4Cq2onHZAhThIfzv3/AFTPALqHzbmV8uNvgRELULWNbsZATgVeqEL4zHOzCyblLfX6tMXVO2BVaPcXboIxGjiw== dependencies: - "@foliojs-fork/fontkit" "^1.9.1" + "@foliojs-fork/fontkit" "^1.9.2" "@foliojs-fork/linebreak" "^1.1.1" - crypto-js "^4.0.0" + crypto-js "^4.2.0" + jpeg-exif "^1.1.4" png-js "^1.0.0" "@foliojs-fork/restructure@^2.0.2": @@ -8461,12 +8459,12 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@puppeteer/browsers@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.4.0.tgz#a0dd0f4e381e53f509109ae83b891db5972750f5" - integrity sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g== +"@puppeteer/browsers@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.4.1.tgz#7afd271199cc920ece2ff25109278be0a3e8a225" + integrity sha512-0kdAbmic3J09I6dT8e9vE2JOCSt13wHCW5x/ly8TSt2bDtuIWe2TgLZZDHdcziw9AVCzflMAXCrVyRIhIs44Ng== dependencies: - debug "^4.3.6" + debug "^4.3.7" extract-zip "^2.0.1" progress "^2.0.3" proxy-agent "^6.4.0" @@ -12215,7 +12213,7 @@ acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-node@^1.3.0, acorn-node@^1.6.1: +acorn-node@^1.6.1: version "1.8.2" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== @@ -12382,11 +12380,6 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.12.0, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - ansi-align@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" @@ -12872,15 +12865,6 @@ ast-module-types@^5.0.0: resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-5.0.0.tgz#32b2b05c56067ff38e95df66f11d6afd6c9ba16b" integrity sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ== -ast-transform@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/ast-transform/-/ast-transform-0.0.0.tgz#74944058887d8283e189d954600947bc98fe0062" - integrity sha1-dJRAWIh9goPhidlUYAlHvJj+AGI= - dependencies: - escodegen "~1.2.0" - esprima "~1.0.4" - through "~2.3.4" - ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" @@ -12907,11 +12891,6 @@ ast-types@^0.16.1: dependencies: tslib "^2.0.1" -ast-types@^0.7.0: - version "0.7.8" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.7.8.tgz#902d2e0d60d071bdcd46dc115e1809ed11c138a9" - integrity sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk= - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -13629,16 +13608,6 @@ breadth-filter@^2.0.0: dependencies: object.entries "^1.0.4" -brfs@^2.0.0, brfs@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/brfs/-/brfs-2.0.2.tgz#44237878fa82aa479ce4f5fe2c1796ec69f07845" - integrity sha512-IrFjVtwu4eTJZyu8w/V2gxU7iLTtcHih67sgEdzrhjLBMHp2uYefUBfdM4k2UvcuWMgV7PQDZHSLeNWnLFKWVQ== - dependencies: - quote-stream "^1.0.1" - resolve "^1.1.5" - static-module "^3.0.2" - through2 "^2.0.0" - brok@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/brok/-/brok-6.0.0.tgz#82f081de7a180802c224955cb6c2888b81a27b28" @@ -13659,13 +13628,6 @@ brotli@^1.2.0: dependencies: base64-js "^1.1.2" -browser-resolve@^1.8.1: - version "1.11.3" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" - integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== - dependencies: - resolve "1.1.7" - browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" @@ -13701,15 +13663,6 @@ browserify-des@^1.0.0: des.js "^1.0.0" inherits "^2.0.1" -browserify-optional@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-optional/-/browserify-optional-1.0.1.tgz#1e13722cfde0d85f121676c2a72ced533a018869" - integrity sha1-HhNyLP3g2F8SFnbCpyztUzoBiGk= - dependencies: - ast-transform "0.0.0" - ast-types "^0.7.0" - browser-resolve "^1.8.1" - browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" @@ -13777,11 +13730,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-equal@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" - integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -14288,10 +14236,10 @@ chromedriver@^130.0.1: proxy-from-env "^1.1.0" tcp-port-used "^1.0.2" -chromium-bidi@0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.6.5.tgz#31be98f9ee5c93fa99d240c680518c9293d8c6bb" - integrity sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA== +chromium-bidi@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.8.0.tgz#ffd79dad7db1fcc874f1c55fcf46ded05a884269" + integrity sha512-uJydbGdTw0DEUjhoogGveneJVWX/9YuqkWePzMmkBYwtdAqo5d3J/ovNKFr+/2hWXYmYCr6it8mSSTIj6SS6Ug== dependencies: mitt "3.0.1" urlpattern-polyfill "10.0.0" @@ -14794,7 +14742,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concat-stream@^1.5.0, concat-stream@~1.6.0: +concat-stream@^1.5.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -14879,7 +14827,7 @@ content-type@~1.0.4, content-type@~1.0.5: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== @@ -15154,7 +15102,7 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-js@^4.0.0: +crypto-js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== @@ -15740,14 +15688,6 @@ d3@3.5.17, d3@^3.5.6: resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g= -d@1, d@^1.0.1, d@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" - integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== - dependencies: - es5-ext "^0.10.64" - type "^2.7.2" - dagre@^0.8.2: version "0.8.5" resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" @@ -15761,11 +15701,6 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -dash-ast@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37" - integrity sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA== - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -15851,7 +15786,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6, debug@^4.3.7: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -15985,7 +15920,7 @@ deep-freeze-strict@^1.1.1: resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0" integrity sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA= -deep-is@^0.1.3, deep-is@~0.1.3: +deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= @@ -16317,10 +16252,10 @@ detective@^5.0.2: defined "^1.0.0" minimist "^1.1.1" -devtools-protocol@0.0.1330662: - version "0.0.1330662" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1330662.tgz#400fe703c2820d6b2d9ebdd1785934310152373e" - integrity sha512-pzh6YQ8zZfz3iKlCvgzVCu22NdpZ8hNmwU6WnQjNVquh0A9iVosPtNLWDwaWVGyrntQlltPFztTMK5Cg6lfCuw== +devtools-protocol@0.0.1354347: + version "0.0.1354347" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1354347.tgz#5cb509610b8f61fc69a31e5c810d5bed002d85ea" + integrity sha512-BlmkSqV0V84E2WnEnoPnwyix57rQxAM5SKJjf4TbYOCGLAWtz8CDH8RIaGOjPgPCXo2Mce3kxSY497OySidY3Q== dezalgo@^1.0.0, dezalgo@^1.0.4: version "1.0.4" @@ -16583,7 +16518,7 @@ downshift@^3.2.10: prop-types "^15.7.2" react-is "^16.9.0" -duplexer2@^0.1.2, duplexer2@~0.1.4: +duplexer2@^0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= @@ -17105,16 +17040,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14: - version "0.10.64" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" - integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== - dependencies: - es6-iterator "^2.0.3" - es6-symbol "^3.1.3" - esniff "^2.0.1" - next-tick "^1.1.0" - es5-shim@^4.5.13: version "4.5.14" resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.5.14.tgz#90009e1019d0ea327447cb523deaff8fe45697ef" @@ -17125,27 +17050,6 @@ es6-error@^4.0.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -es6-iterator@^2.0.3, es6-iterator@~2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-map@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" - integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA= - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-set "~0.1.5" - es6-symbol "~3.1.1" - event-emitter "~0.3.5" - es6-promise@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" @@ -17156,38 +17060,11 @@ es6-promise@^4.2.8: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== -es6-set@^0.1.5, es6-set@~0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" - integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE= - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-symbol "3.1.1" - event-emitter "~0.3.5" - es6-shim@^0.35.5: version "0.35.5" resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.5.tgz#46f59dc0a84a1c5029e8ff1166ca0a902077a9ab" integrity sha512-E9kK/bjtCQRpN1K28Xh4BlmP8egvZBGJJ+9GtnzOwt7mdqtrjHFuVGr7QJfdjBIKqrlU5duPf3pCBoDrkjVYFg== -es6-symbol@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" - integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= - dependencies: - d "1" - es5-ext "~0.10.14" - -es6-symbol@^3.1.1, es6-symbol@^3.1.3, es6-symbol@~3.1.1: - version "3.1.4" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" - integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== - dependencies: - d "^1.0.2" - ext "^1.7.0" - esbuild@^0.19.11: version "0.19.12" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" @@ -17242,18 +17119,6 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^1.11.1: - version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - escodegen@^2.0.0, escodegen@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" @@ -17265,17 +17130,6 @@ escodegen@^2.0.0, escodegen@^2.1.0: optionalDependencies: source-map "~0.6.1" -escodegen@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.2.0.tgz#09de7967791cc958b7f89a2ddb6d23451af327e1" - integrity sha1-Cd55Z3kcyVi3+Jot220jRRrzJ+E= - dependencies: - esprima "~1.0.4" - estraverse "~1.5.0" - esutils "~1.0.0" - optionalDependencies: - source-map "~0.1.30" - eslint-config-prettier@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" @@ -17595,16 +17449,6 @@ eslint@^8.57.0: strip-ansi "^6.0.1" text-table "^0.2.0" -esniff@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" - integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== - dependencies: - d "^1.0.1" - es5-ext "^0.10.62" - event-emitter "^0.3.5" - type "^2.7.2" - espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -17619,11 +17463,6 @@ esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esprima@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.0.4.tgz#9f557e08fc3b4d26ece9dd34f8fbf476b62585ad" - integrity sha1-n1V+CPw7TSbs6d00+Pv0drYlha0= - esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" @@ -17638,7 +17477,7 @@ esrecurse@^4.1.0, esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -17648,39 +17487,16 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -estraverse@~1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.5.1.tgz#867a3e8e58a9f84618afb6c2ddbcd916b7cbaf71" - integrity sha512-FpCjJDfmo3vsc/1zKSeqR5k42tcIhxFIlvq+h9j0fO2q/h2uLKyweq7rYJ+0CoVvrGQOxIS5wyBrW/+vF58BUQ== - -estree-is-function@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/estree-is-function/-/estree-is-function-1.0.0.tgz#c0adc29806d7f18a74db7df0f3b2666702e37ad2" - integrity sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA== - esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= -esutils@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.0.0.tgz#8151d358e20c8acc7fb745e7472c0025fe496570" - integrity sha1-gVHTWOIMisx/t0XnRywAJf5JZXA= - etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -event-emitter@^0.3.5, event-emitter@~0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= - dependencies: - d "1" - es5-ext "~0.10.14" - event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -17909,13 +17725,6 @@ express@^4.17.1, express@^4.17.3, express@^4.18.2, express@^4.21.0: utils-merge "1.0.1" vary "~1.1.2" -ext@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" - integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== - dependencies: - type "^2.7.2" - extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -18048,7 +17857,7 @@ fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json- resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -18824,11 +18633,6 @@ get-amd-module-type@^5.0.1: ast-module-types "^5.0.0" node-source-walk "^6.0.1" -get-assigned-identifiers@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" - integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ== - get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -19492,7 +19296,7 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.0, has@^1.0.1, has@^1.0.3, has@^1.0.4: +has@^1.0.0, has@^1.0.3, has@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== @@ -21714,6 +21518,11 @@ jora@1.0.0-beta.8: dependencies: "@discoveryjs/natural-compare" "^1.0.0" +jpeg-exif@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/jpeg-exif/-/jpeg-exif-1.1.4.tgz#781a65b6cd74f62cb1c493511020f8d3577a1c2b" + integrity sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ== + jquery@^3.5.0: version "3.6.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" @@ -22280,14 +22089,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - license-checker@^25.0.1: version "25.0.1" resolved "https://registry.yarnpkg.com/license-checker/-/license-checker-25.0.1.tgz#4d14504478a5240a857bb3c21cd0491a00d761fa" @@ -22768,13 +22569,6 @@ macos-release@^2.2.0: resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.2.0.tgz#ab58d55dd4714f0a05ad4b0e90f4370fef5cdea8" integrity sha512-iV2IDxZaX8dIcM7fG6cI46uNmHUxHE4yN+Z8tKHAW1TBPMZDIKHf/3L+YnOuj/FK9il14UaVdHmiQ1tsi90ltA== -magic-string@0.25.1: - version "0.25.1" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" - integrity sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg== - dependencies: - sourcemap-codec "^1.4.1" - magic-string@^0.30.0: version "0.30.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.9.tgz#8927ae21bfdd856310e07a1bc8dd5e73cb6c251d" @@ -23189,13 +22983,6 @@ merge-descriptors@1.0.3: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== -merge-source-map@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f" - integrity sha1-pd5GU42uhNQRTMXqArR3KmNGcB8= - dependencies: - source-map "^0.5.6" - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -24037,11 +23824,6 @@ next-line@^1.1.0: resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603" integrity sha1-/K5XhTBStqm66CCOQN19PC0wRgM= -next-tick@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" - integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== - nice-napi@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b" @@ -24495,7 +24277,7 @@ object-identity-map@^1.0.2: dependencies: object.entries "^1.1.0" -object-inspect@^1.13.1, object-inspect@^1.6.0, object-inspect@^1.7.0: +object-inspect@^1.13.1, object-inspect@^1.7.0: version "1.13.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== @@ -24741,18 +24523,6 @@ optional-js@^2.0.0: resolved "https://registry.yarnpkg.com/optional-js/-/optional-js-2.1.1.tgz#c2dc519ad119648510b4d241dbb60b1167c36a46" integrity sha512-mUS4bDngcD5kKzzRUd1HVQkr9Lzzby3fSrrPR9wOHhQiyYo+hDS5NVli5YQzGjQRQ15k5Sno4xH9pfykJdeEUA== -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -25284,15 +25054,15 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" -pdfmake@^0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.2.7.tgz#a7a46532ffde032674929988393c20b075cf65e3" - integrity sha512-ClLpgx30H5G3EDvRW1MrA1Xih6YxEaSgIVFrOyBMgAAt62V+hxsyWAi6JNP7u1Fc5JKYAbpb4RRVw8Rhvmz5cQ== +pdfmake@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.2.15.tgz#86bbc2c854e8a1cc98d4d6394b39dae00cc3a3b0" + integrity sha512-Ryef9mjxo6q8dthhbssAK0zwCsPZ6Pl7kCHnIEXOvQdd79LUGZD6SHGi21YryFXczPjvw6V009uxQwp5iritcA== dependencies: - "@foliojs-fork/linebreak" "^1.1.1" - "@foliojs-fork/pdfkit" "^0.13.0" + "@foliojs-fork/linebreak" "^1.1.2" + "@foliojs-fork/pdfkit" "^0.15.1" iconv-lite "^0.6.3" - xmldoc "^1.1.2" + xmldoc "^1.3.0" peggy@^1.2.0: version "1.2.0" @@ -25959,11 +25729,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -26317,28 +26082,28 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -puppeteer-core@23.3.1: - version "23.3.1" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-23.3.1.tgz#b93d825e586f5f7dc268128a31a31c62bbe378ae" - integrity sha512-m5gTpITEqqpSgAvPUI/Ch9igh5sNJV+BVVbqQMzqirRDVHDCkLGHaydEQZx2NZvSXdwCFrIV///cpSlX/uD0Sg== +puppeteer-core@23.7.0: + version "23.7.0" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-23.7.0.tgz#b737476f8f5e2a36a6683d91595eaa5c0e231a37" + integrity sha512-0kC81k3K6n6Upg/k04xv+Mi8yy62bNAJiK7LCA71zfq2XKEo9WAzas1t6UQiLgaNHtGNKM0d1KbR56p/+mgEiQ== dependencies: - "@puppeteer/browsers" "2.4.0" - chromium-bidi "0.6.5" + "@puppeteer/browsers" "2.4.1" + chromium-bidi "0.8.0" debug "^4.3.7" - devtools-protocol "0.0.1330662" + devtools-protocol "0.0.1354347" typed-query-selector "^2.12.0" ws "^8.18.0" -puppeteer@23.3.1: - version "23.3.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-23.3.1.tgz#830ac4b2c264ae4a610b79be77aff23bb13efa2c" - integrity sha512-BxkuJyCv46ZKW8KEHiVMHgHEC89jKK9FffReWjbw1IfBUmNx+6JIZyqOtaJeSwyolTdVqqb5fiPiXflKeH3dKQ== +puppeteer@23.7.0: + version "23.7.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-23.7.0.tgz#193dcc78bdcc5d3023cc172e9231771c350484bd" + integrity sha512-YTgo0KFe8NtBcI9hCu/xsjPFumEhu8kA7QqLr6Uh79JcEsUcUt+go966NgKYXJ+P3Fuefrzn2SXwV3cyOe/UcQ== dependencies: - "@puppeteer/browsers" "2.4.0" - chromium-bidi "0.6.5" + "@puppeteer/browsers" "2.4.1" + chromium-bidi "0.8.0" cosmiconfig "^9.0.0" - devtools-protocol "0.0.1330662" - puppeteer-core "23.3.1" + devtools-protocol "0.0.1354347" + puppeteer-core "23.7.0" typed-query-selector "^2.12.0" pure-rand@^6.0.0: @@ -26414,15 +26179,6 @@ quickselect@^2.0.0: resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== -quote-stream@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-1.0.2.tgz#84963f8c9c26b942e153feeb53aae74652b7e0b2" - integrity sha1-hJY/jJwmuULhU/7rU6rnRlK34LI= - dependencies: - buffer-equal "0.0.1" - minimist "^1.1.3" - through2 "^2.0.0" - quote-unquote@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/quote-unquote/-/quote-unquote-1.0.0.tgz#67a9a77148effeaf81a4d428404a710baaac8a0b" @@ -27164,7 +26920,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@~2.3.3, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -27849,12 +27605,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= - -resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.22.3, resolve@^1.22.8, resolve@^1.3.2, resolve@^1.9.0: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.22.3, resolve@^1.22.8, resolve@^1.3.2, resolve@^1.9.0: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -28289,10 +28040,10 @@ sass-lookup@^5.0.1: dependencies: commander "^10.0.1" -sax@>=0.6.0, sax@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" - integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== +sax@>=0.6.0, sax@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== saxes@^6.0.0: version "6.0.0" @@ -28361,19 +28112,6 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" -scope-analyzer@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/scope-analyzer/-/scope-analyzer-2.1.1.tgz#5156c27de084d74bf75af9e9506aaf95c6e73dd6" - integrity sha512-azEAihtQ9mEyZGhfgTJy3IbOWEzeOrYbg7NcYEshPKnKd+LZmC3TNd5dmDxbLBsTG/JVWmCp+vDJ03vJjeXMHg== - dependencies: - array-from "^2.1.1" - dash-ast "^1.0.0" - es6-map "^0.1.5" - es6-set "^0.1.5" - es6-symbol "^3.1.1" - estree-is-function "^1.0.0" - get-assigned-identifiers "^1.1.0" - screenfull@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.0.tgz#5c2010c0e84fd4157bf852877698f90b8cbe96f6" @@ -28621,11 +28359,6 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" -shallow-copy@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" - integrity sha1-QV9CcC1z2BAzApLMXuhurhoRoXA= - shallow-equal@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-3.1.0.tgz#e7a54bac629c7f248eff6c2f5b63122ba4320bec" @@ -29101,13 +28834,6 @@ source-map@^0.8.0-beta.0: dependencies: whatwg-url "^7.0.0" -source-map@~0.1.30: - version "0.1.32" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.32.tgz#c8b6c167797ba4740a8ea33252162ff08591b266" - integrity sha1-yLbBZ3l7pHQKjqMyUhYv8IWRsmY= - dependencies: - amdefine ">=0.0.4" - sourcemap-codec@^1.4.1: version "1.4.6" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" @@ -29370,13 +29096,6 @@ state-toggle@^1.0.0: resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425" integrity sha1-0g+aYWu08MO5i5GSLSW2QKorxCU= -static-eval@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.1.0.tgz#a16dbe54522d7fa5ef1389129d813fd47b148014" - integrity sha512-agtxZ/kWSsCkI5E4QifRwsaPs0P0JmZV6dkLz6ILYfFYQGn+5plctanRN+IC8dJRiFkyXHrwEE3W9Wmx67uDbw== - dependencies: - escodegen "^1.11.1" - static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -29385,26 +29104,6 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -static-module@^3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/static-module/-/static-module-3.0.4.tgz#bfbd1d1c38dd1fbbf0bb4af0c1b3ae18a93a2b68" - integrity sha512-gb0v0rrgpBkifXCa3yZXxqVmXDVE+ETXj6YlC/jt5VzOnGXR2C15+++eXuMDUYsePnbhf+lwW0pE1UXyOLtGCw== - dependencies: - acorn-node "^1.3.0" - concat-stream "~1.6.0" - convert-source-map "^1.5.1" - duplexer2 "~0.1.4" - escodegen "^1.11.1" - has "^1.0.1" - magic-string "0.25.1" - merge-source-map "1.0.4" - object-inspect "^1.6.0" - readable-stream "~2.3.3" - scope-analyzer "^2.0.1" - shallow-copy "~0.0.1" - static-eval "^2.0.5" - through2 "~2.0.3" - stats-lite@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/stats-lite/-/stats-lite-2.2.0.tgz#278a5571fa1d2e8b1691295dccc0235282393bbf" @@ -30359,7 +30058,7 @@ through2@^0.6.3: readable-stream ">=1.0.33-1 <1.1.0-0" xtend ">=4.0.0 <4.1.0-0" -through2@^2.0.0, through2@~2.0.3: +through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -30796,13 +30495,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - type-detect@4.0.8, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -30851,11 +30543,6 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -type@^2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" - integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== - typed-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" @@ -32542,11 +32229,6 @@ wkt-parser@^1.2.4: resolved "https://registry.yarnpkg.com/wkt-parser/-/wkt-parser-1.3.2.tgz#deeff04a21edc5b170a60da418e9ed1d1ab0e219" integrity sha512-A26BOOo7sHAagyxG7iuRhnKMO7Q3mEOiOT4oGUmohtN/Li5wameeU4S6f8vWw6NADTVKljBs8bzA8JPQgSEMVQ== -word-wrap@~1.2.3: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -32697,12 +32379,12 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldoc@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.2.tgz#6666e029fe25470d599cd30e23ff0d1ed50466d7" - integrity sha512-ruPC/fyPNck2BD1dpz0AZZyrEwMOrWTO5lDdIXS91rs3wtm4j+T8Rp2o+zoOYkkAxJTZRPOSnOGei1egoRmKMQ== +xmldoc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.3.0.tgz#7823225b096c74036347c9ec5924d06b6a3cebab" + integrity sha512-y7IRWW6PvEnYQZNZFMRLNJw+p3pezM4nKYPfr15g4OOW9i8VpeydycFuipE2297OvZnh3jSb2pxOt9QpkZUVng== dependencies: - sax "^1.2.1" + sax "^1.2.4" xpath@^0.0.33: version "0.0.33" From ca3aa4d76b66c030f758e44c18bc14952e1d4f7d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 07:00:44 +1100 Subject: [PATCH 40/47] [8.x] [AI Assistant] Enable Search in Solution View (#199137) (#199359) # Backport This will backport the following commits from `main` to `8.x`: - [[AI Assistant] Enable Search in Solution View (#199137)](https://github.com/elastic/kibana/pull/199137) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sander Philipse <94373878+sphilipse@users.noreply.github.com> --- .../server/capabilities/capabilities_switcher.test.ts | 6 ++---- .../lib/utils/space_solution_disabled_features.test.ts | 4 ++-- .../server/lib/utils/space_solution_disabled_features.ts | 1 - 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index 688f8297271a3..33c32acfef011 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -392,14 +392,12 @@ describe('capabilitiesSwitcher', () => { { space.solution = 'oblt'; - // It should disable enterpriseSearch and securitySolution features - // which correspond to feature_1 and feature_3 + // It should disable securitySolution features + // which corresponds to feature_3 const result = await switcher(request, capabilities, false); const expectedCapabilities = buildCapabilities(); - expectedCapabilities.feature_1.bar = false; - expectedCapabilities.feature_1.foo = false; expectedCapabilities.feature_3.bar = false; expectedCapabilities.feature_3.foo = false; diff --git a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts index 908a4ee2ced57..f19b4d585dc22 100644 --- a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts +++ b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts @@ -58,7 +58,7 @@ describe('#withSpaceSolutionDisabledFeatures', () => { }); describe('when the space solution is "oblt"', () => { - test('it removes the "search" and "security" features', () => { + test('it removes the "security" features', () => { const spaceDisabledFeatures: string[] = []; const spaceSolution = 'oblt'; @@ -68,7 +68,7 @@ describe('#withSpaceSolutionDisabledFeatures', () => { spaceSolution ); - expect(result).toEqual(['feature2', 'feature3']); + expect(result).toEqual(['feature3']); }); }); diff --git a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts index 6d30645325535..2682daf3a1c54 100644 --- a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts +++ b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts @@ -62,7 +62,6 @@ export function withSpaceSolutionDisabledFeatures( ]).filter((featureId) => !enabledFeaturesPerSolution.es.includes(featureId)); } else if (spaceSolution === 'oblt') { disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ - 'enterpriseSearch', 'securitySolution', ]).filter((featureId) => !enabledFeaturesPerSolution.oblt.includes(featureId)); } else if (spaceSolution === 'security') { From 51c3d765dd29f684c09cf99f74b28264fac5f370 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 07:44:09 +1100 Subject: [PATCH 41/47] [8.x] Increase GCP button size to match Azure and AWS (#199228) (#199367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [Increase GCP button size to match Azure and AWS (#199228)](https://github.com/elastic/kibana/pull/199228) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Kyra Cho --- .../gcp_credentials_form/gcp_credential_form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credential_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credential_form.tsx index 638af9617e008..7d6d42c70e767 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credential_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credential_form.tsx @@ -500,7 +500,7 @@ export const GcpCredentialsForm = ({ From fa0bddaac166c1bd17989a54b183677203f1812b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 08:18:51 +1100 Subject: [PATCH 42/47] [8.x] [Security Solution][Data Quality Dashboard] fix pattern state reset on ilm phase filter change (#198549) (#198806) # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution][Data Quality Dashboard] fix pattern state reset on ilm phase filter change (#198549)](https://github.com/elastic/kibana/pull/198549) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Karen Grigoryan --- .../use_stored_pattern_results/index.test.tsx | 108 +++ .../use_stored_pattern_results/index.tsx | 53 ++ .../hooks/use_results_rollup/index.test.tsx | 685 ++++++++++++++++++ .../hooks/use_results_rollup/index.tsx | 66 +- .../impl/data_quality_panel/index.tsx | 1 - .../mock/test_providers/utils/format.ts | 17 + .../get_merged_data_quality_context_props.ts | 11 +- .../stub/get_pattern_rollup_stub/index.ts | 116 +++ 8 files changed, 986 insertions(+), 71 deletions(-) create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.test.tsx create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.tsx create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.test.tsx create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/format.ts create mode 100644 x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_pattern_rollup_stub/index.ts diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.test.tsx new file mode 100644 index 0000000000000..d58bf3af39d58 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.test.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; + +import { getHistoricalResultStub } from '../../../../stub/get_historical_result_stub'; +import { useStoredPatternResults } from '.'; + +describe('useStoredPatternResults', () => { + const httpFetch = jest.fn(); + const mockToasts = notificationServiceMock.createStartContract().toasts; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('when patterns are empty', () => { + it('should return an empty array and not call getStorageResults', () => { + const { result } = renderHook(() => useStoredPatternResults([], mockToasts, httpFetch)); + + expect(result.current).toEqual([]); + expect(httpFetch).not.toHaveBeenCalled(); + }); + }); + + describe('when patterns are provided', () => { + it('should fetch and return stored pattern results correctly', async () => { + const patterns = ['pattern1-*', 'pattern2-*']; + + httpFetch.mockImplementation((path: string) => { + if (path === '/internal/ecs_data_quality_dashboard/results_latest/pattern1-*') { + return Promise.resolve([getHistoricalResultStub('pattern1-index1')]); + } + + if (path === '/internal/ecs_data_quality_dashboard/results_latest/pattern2-*') { + return Promise.resolve([getHistoricalResultStub('pattern2-index1')]); + } + + return Promise.reject(new Error('Invalid path')); + }); + + const { result, waitFor } = renderHook(() => + useStoredPatternResults(patterns, mockToasts, httpFetch) + ); + + await waitFor(() => result.current.length > 0); + + expect(httpFetch).toHaveBeenCalledTimes(2); + + expect(httpFetch).toHaveBeenCalledWith( + '/internal/ecs_data_quality_dashboard/results_latest/pattern1-*', + { + method: 'GET', + signal: expect.any(AbortSignal), + version: '1', + } + ); + expect(httpFetch).toHaveBeenCalledWith( + '/internal/ecs_data_quality_dashboard/results_latest/pattern2-*', + { + method: 'GET', + signal: expect.any(AbortSignal), + version: '1', + } + ); + + expect(result.current).toEqual([ + { + pattern: 'pattern1-*', + results: { + 'pattern1-index1': { + docsCount: expect.any(Number), + error: null, + ilmPhase: expect.any(String), + incompatible: expect.any(Number), + indexName: 'pattern1-index1', + pattern: 'pattern1-*', + markdownComments: expect.any(Array), + sameFamily: expect.any(Number), + checkedAt: expect.any(Number), + }, + }, + }, + { + pattern: 'pattern2-*', + results: { + 'pattern2-index1': { + docsCount: expect.any(Number), + error: null, + ilmPhase: expect.any(String), + incompatible: expect.any(Number), + indexName: 'pattern2-index1', + pattern: 'pattern2-*', + markdownComments: expect.any(Array), + sameFamily: expect.any(Number), + checkedAt: expect.any(Number), + }, + }, + }, + ]); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.tsx new file mode 100644 index 0000000000000..17334c4b4a586 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useEffect, useState } from 'react'; +import { IToasts } from '@kbn/core-notifications-browser'; +import { HttpHandler } from '@kbn/core-http-browser'; +import { isEmpty } from 'lodash/fp'; + +import { DataQualityCheckResult } from '../../../../types'; +import { formatResultFromStorage, getStorageResults } from '../../utils/storage'; + +export const useStoredPatternResults = ( + patterns: string[], + toasts: IToasts, + httpFetch: HttpHandler +) => { + const [storedPatternResults, setStoredPatternResults] = useState< + Array<{ pattern: string; results: Record }> + >([]); + + useEffect(() => { + if (isEmpty(patterns)) { + return; + } + + const abortController = new AbortController(); + const fetchStoredPatternResults = async () => { + const requests = patterns.map((pattern) => + getStorageResults({ pattern, httpFetch, abortController, toasts }).then((results = []) => ({ + pattern, + results: Object.fromEntries( + results.map((storageResult) => [ + storageResult.indexName, + formatResultFromStorage({ storageResult, pattern }), + ]) + ), + })) + ); + + const patternResults = await Promise.all(requests); + if (patternResults?.length) { + setStoredPatternResults(patternResults); + } + }; + + fetchStoredPatternResults(); + }, [httpFetch, patterns, toasts]); + + return storedPatternResults; +}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.test.tsx new file mode 100644 index 0000000000000..bff3c3dd54f12 --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.test.tsx @@ -0,0 +1,685 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// fixing timezone for Date +// so when tests are run in different timezones, the results are consistent +process.env.TZ = 'UTC'; + +import { renderHook, act } from '@testing-library/react-hooks'; +import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; + +import type { TelemetryEvents } from '../../types'; +import { useStoredPatternResults } from './hooks/use_stored_pattern_results'; +import { mockPartitionedFieldMetadata } from '../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { useResultsRollup } from '.'; +import { getPatternRollupStub } from '../../stub/get_pattern_rollup_stub'; +import { formatBytes, formatNumber } from '../../mock/test_providers/utils/format'; + +jest.mock('./hooks/use_stored_pattern_results', () => ({ + ...jest.requireActual('./hooks/use_stored_pattern_results'), + useStoredPatternResults: jest.fn().mockReturnValue([]), +})); + +describe('useResultsRollup', () => { + const httpFetch = jest.fn(); + const toasts = notificationServiceMock.createStartContract().toasts; + + const mockTelemetryEvents: TelemetryEvents = { + reportDataQualityIndexChecked: jest.fn(), + reportDataQualityCheckAllCompleted: jest.fn(), + }; + + const patterns = ['auditbeat-*', 'packetbeat-*']; + const isILMAvailable = true; + + const useStoredPatternResultsMock = useStoredPatternResults as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + useStoredPatternResultsMock.mockReturnValue([]); + }); + + describe('initialization', () => { + it('should initialize with default values', () => { + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns, + isILMAvailable, + telemetryEvents: mockTelemetryEvents, + }) + ); + + expect(result.current.patternIndexNames).toEqual({}); + expect(result.current.patternRollups).toEqual({}); + expect(result.current.totalDocsCount).toBe(0); + expect(result.current.totalIncompatible).toBeUndefined(); + expect(result.current.totalIndices).toBe(0); + expect(result.current.totalIndicesChecked).toBe(0); + expect(result.current.totalSameFamily).toBeUndefined(); + expect(result.current.totalSizeInBytes).toBe(0); + }); + + it('should fetch stored pattern results and update patternRollups from it', () => { + const mockStoredResults = [ + { + pattern: 'auditbeat-*', + results: { + 'auditbeat-7.11.0-2021.01.01': { + indexName: 'auditbeat-7.11.0-2021.01.01', + pattern: 'auditbeat-*', + docsCount: 500, + incompatible: 0, + error: null, + ilmPhase: 'hot', + sameFamily: 0, + markdownComments: [], + checkedAt: Date.now(), + }, + }, + }, + ]; + + useStoredPatternResultsMock.mockReturnValue(mockStoredResults); + + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns: ['auditbeat-*'], + isILMAvailable, + telemetryEvents: mockTelemetryEvents, + }) + ); + + expect(useStoredPatternResultsMock).toHaveBeenCalledWith(['auditbeat-*'], toasts, httpFetch); + + expect(result.current.patternRollups).toEqual({ + 'auditbeat-*': { + pattern: 'auditbeat-*', + results: { + 'auditbeat-7.11.0-2021.01.01': expect.any(Object), + }, + }, + }); + }); + }); + + describe('updatePatternIndexNames', () => { + it('should update pattern index names', () => { + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns, + isILMAvailable, + telemetryEvents: mockTelemetryEvents, + }) + ); + + act(() => { + result.current.updatePatternIndexNames({ + pattern: 'packetbeat-*', + indexNames: ['packetbeat-7.10.0-2021.01.01'], + }); + }); + + expect(result.current.patternIndexNames).toEqual({ + 'packetbeat-*': ['packetbeat-7.10.0-2021.01.01'], + }); + }); + }); + + describe('updatePatternRollup', () => { + it('should update pattern rollup when called', () => { + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns, + isILMAvailable, + telemetryEvents: mockTelemetryEvents, + }) + ); + + const patternRollup = getPatternRollupStub('packetbeat-*', 1); + + expect(result.current.patternRollups).toEqual({}); + + act(() => { + result.current.updatePatternRollup(patternRollup); + }); + + expect(result.current.patternRollups).toEqual({ + 'packetbeat-*': patternRollup, + }); + }); + }); + + describe('onCheckCompleted', () => { + describe('when invoked with successful check data', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2021-10-07T00:00:00Z').getTime()); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should update patternRollup with said data, report to telemetry and persist it in storage', () => { + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns, + isILMAvailable, + telemetryEvents: mockTelemetryEvents, + }) + ); + + const patternRollup = getPatternRollupStub('packetbeat-*', 1); + + act(() => { + result.current.updatePatternRollup(patternRollup); + }); + + expect(result.current.patternRollups['packetbeat-*'].results?.['.ds-packetbeat-1']).toEqual( + { + checkedAt: new Date('2021-10-07T00:00:00Z').getTime(), + docsCount: 1000000, + error: null, + ilmPhase: 'hot', + incompatible: 0, + indexName: '.ds-packetbeat-1', + markdownComments: ['foo', 'bar', 'baz'], + pattern: 'packetbeat-*', + sameFamily: 0, + } + ); + + jest.advanceTimersByTime(1000); + + const mockOnCheckCompletedOpts = { + batchId: 'test-batch', + checkAllStartTime: Date.now(), + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-1', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'packetbeat-*', + requestTime: 1500, + isLastCheck: true, + isCheckAll: true, + }; + + jest.advanceTimersByTime(1000); + + act(() => { + result.current.onCheckCompleted(mockOnCheckCompletedOpts); + }); + + expect(result.current.patternRollups['packetbeat-*'].results?.['.ds-packetbeat-1']).toEqual( + { + checkedAt: new Date('2021-10-07T00:00:02Z').getTime(), + docsCount: 1000000, + error: null, + ilmPhase: 'hot', + incompatible: 3, + indexName: '.ds-packetbeat-1', + markdownComments: expect.any(Array), + pattern: 'packetbeat-*', + sameFamily: 0, + } + ); + + expect(mockTelemetryEvents.reportDataQualityIndexChecked).toHaveBeenCalledWith({ + batchId: 'test-batch', + ecsVersion: '8.11.0', + errorCount: 0, + ilmPhase: 'hot', + indexId: 'uuid-1', + indexName: '.ds-packetbeat-1', + isCheckAll: true, + numberOfCustomFields: 4, + numberOfDocuments: 1000000, + numberOfEcsFields: 2, + numberOfFields: 9, + numberOfIncompatibleFields: 3, + numberOfIndices: 1, + numberOfIndicesChecked: 1, + numberOfSameFamily: 0, + sameFamilyFields: [], + sizeInBytes: 500000000, + timeConsumedMs: 1500, + unallowedMappingFields: ['host.name', 'source.ip'], + unallowedValueFields: ['event.category'], + }); + expect(mockTelemetryEvents.reportDataQualityCheckAllCompleted).toHaveBeenCalledWith({ + batchId: 'test-batch', + ecsVersion: '8.11.0', + isCheckAll: true, + numberOfDocuments: 1000000, + numberOfIncompatibleFields: 3, + numberOfIndices: 1, + numberOfIndicesChecked: 1, + numberOfSameFamily: 0, + sizeInBytes: 500000000, + timeConsumedMs: 1000, + }); + + expect(httpFetch).toHaveBeenCalledWith('/internal/ecs_data_quality_dashboard/results', { + method: 'POST', + version: '1', + signal: expect.any(AbortSignal), + body: expect.any(String), + }); + + const body = JSON.parse(httpFetch.mock.calls[0][1].body); + + expect(body).toEqual({ + batchId: 'test-batch', + indexName: '.ds-packetbeat-1', + indexPattern: 'packetbeat-*', + isCheckAll: true, + checkedAt: new Date('2021-10-07T00:00:02Z').getTime(), + docsCount: 1000000, + totalFieldCount: 9, + ecsFieldCount: 2, + customFieldCount: 4, + incompatibleFieldCount: 3, + incompatibleFieldMappingItems: [ + { + fieldName: 'host.name', + expectedValue: 'keyword', + actualValue: 'text', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + }, + { + fieldName: 'source.ip', + expectedValue: 'ip', + actualValue: 'text', + description: 'IP address of the source (IPv4 or IPv6).', + }, + ], + incompatibleFieldValueItems: [ + { + fieldName: 'event.category', + expectedValues: [ + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + actualValues: [ + { name: 'an_invalid_category', count: 2 }, + { name: 'theory', count: 1 }, + ], + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + }, + ], + sameFamilyFieldCount: 0, + sameFamilyFields: [], + sameFamilyFieldItems: [], + unallowedMappingFields: ['host.name', 'source.ip'], + unallowedValueFields: ['event.category'], + sizeInBytes: 500000000, + ilmPhase: 'hot', + markdownComments: [ + '### .ds-packetbeat-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-1 | 1,000,000 (100.0%) | 3 | `hot` | 476.8MB |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + "#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.11.0.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n#### Incompatible field mappings - .ds-packetbeat-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ], + ecsVersion: '8.11.0', + indexId: 'uuid-1', + error: null, + }); + }); + + describe('when isILMAvailable is false', () => { + it('should omit ilmPhase and nullify sizeInBytes when storing payload', () => { + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns, + isILMAvailable: false, + telemetryEvents: mockTelemetryEvents, + }) + ); + + const patternRollup = getPatternRollupStub('packetbeat-*', 1, false); + + act(() => { + result.current.updatePatternRollup(patternRollup); + }); + + jest.advanceTimersByTime(1000); + + const mockOnCheckCompletedOpts = { + batchId: 'test-batch', + checkAllStartTime: Date.now(), + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-1', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'packetbeat-*', + requestTime: 1500, + isLastCheck: true, + isCheckAll: true, + }; + + jest.advanceTimersByTime(1000); + + act(() => { + result.current.onCheckCompleted(mockOnCheckCompletedOpts); + }); + + expect(mockTelemetryEvents.reportDataQualityIndexChecked).toHaveBeenCalledWith({ + batchId: 'test-batch', + ecsVersion: '8.11.0', + errorCount: 0, + ilmPhase: undefined, + indexId: 'uuid-1', + indexName: '.ds-packetbeat-1', + isCheckAll: true, + numberOfCustomFields: 4, + numberOfDocuments: 1000000, + numberOfEcsFields: 2, + numberOfFields: 9, + numberOfIncompatibleFields: 3, + numberOfIndices: 1, + numberOfIndicesChecked: 1, + numberOfSameFamily: 0, + sameFamilyFields: [], + sizeInBytes: undefined, + timeConsumedMs: 1500, + unallowedMappingFields: ['host.name', 'source.ip'], + unallowedValueFields: ['event.category'], + }); + expect(mockTelemetryEvents.reportDataQualityCheckAllCompleted).toHaveBeenCalledWith({ + batchId: 'test-batch', + ecsVersion: '8.11.0', + isCheckAll: true, + numberOfDocuments: 1000000, + numberOfIncompatibleFields: 3, + numberOfIndices: 1, + numberOfIndicesChecked: 1, + numberOfSameFamily: 0, + sizeInBytes: undefined, + timeConsumedMs: 1000, + }); + + expect(httpFetch).toHaveBeenCalledWith('/internal/ecs_data_quality_dashboard/results', { + method: 'POST', + version: '1', + signal: expect.any(AbortSignal), + body: expect.any(String), + }); + + const body = JSON.parse(httpFetch.mock.calls[0][1].body); + + expect(body).toEqual({ + batchId: 'test-batch', + indexName: '.ds-packetbeat-1', + indexPattern: 'packetbeat-*', + isCheckAll: true, + checkedAt: new Date('2021-10-07T00:00:02Z').getTime(), + docsCount: 1000000, + totalFieldCount: 9, + ecsFieldCount: 2, + customFieldCount: 4, + incompatibleFieldCount: 3, + incompatibleFieldMappingItems: [ + { + fieldName: 'host.name', + expectedValue: 'keyword', + actualValue: 'text', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + }, + { + fieldName: 'source.ip', + expectedValue: 'ip', + actualValue: 'text', + description: 'IP address of the source (IPv4 or IPv6).', + }, + ], + incompatibleFieldValueItems: [ + { + fieldName: 'event.category', + expectedValues: [ + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + actualValues: [ + { name: 'an_invalid_category', count: 2 }, + { name: 'theory', count: 1 }, + ], + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + }, + ], + sameFamilyFieldCount: 0, + sameFamilyFields: [], + sameFamilyFieldItems: [], + unallowedMappingFields: ['host.name', 'source.ip'], + unallowedValueFields: ['event.category'], + ilmPhase: undefined, + sizeInBytes: 0, + markdownComments: [ + '### .ds-packetbeat-1\n', + '| Result | Index | Docs | Incompatible fields |\n|--------|-------|------|---------------------|\n| ❌ | .ds-packetbeat-1 | 1,000,000 (100.0%) | 3 |\n\n', + '### **Incompatible fields** `3` **Same family** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + "#### 3 incompatible fields\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.11.0.\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n#### Incompatible field mappings - .ds-packetbeat-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ], + ecsVersion: '8.11.0', + indexId: 'uuid-1', + error: null, + }); + }); + }); + }); + + describe('when check fails with error message and no partitionedFieldMetadata', () => { + it('should update patternRollup with error message, reset state without persisting in storage', () => { + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns, + isILMAvailable, + telemetryEvents: mockTelemetryEvents, + }) + ); + + const patternRollup = getPatternRollupStub('packetbeat-*', 1); + + act(() => { + result.current.updatePatternRollup(patternRollup); + }); + + const mockOnCheckCompletedOpts = { + batchId: 'test-batch', + checkAllStartTime: Date.now(), + error: 'Something went wrong', + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-1', + partitionedFieldMetadata: null, + pattern: 'packetbeat-*', + requestTime: 1500, + isLastCheck: true, + isCheckAll: true, + }; + + act(() => { + result.current.onCheckCompleted(mockOnCheckCompletedOpts); + }); + + expect(result.current.patternRollups['packetbeat-*'].results?.['.ds-packetbeat-1']).toEqual( + { + checkedAt: undefined, + docsCount: 1000000, + error: 'Something went wrong', + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-1', + markdownComments: expect.any(Array), + pattern: 'packetbeat-*', + sameFamily: undefined, + } + ); + + expect(mockTelemetryEvents.reportDataQualityIndexChecked).not.toHaveBeenCalled(); + + expect(httpFetch).not.toHaveBeenCalledWith( + '/internal/ecs_data_quality_dashboard/results', + expect.any(Object) + ); + }); + }); + + describe('edge cases', () => { + describe('given no error nor partitionedFieldMetadata', () => { + it('should reset result state accordingly and not invoke telemetry report nor persist in storage', () => { + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns, + isILMAvailable, + telemetryEvents: mockTelemetryEvents, + }) + ); + + const patternRollup = getPatternRollupStub('packetbeat-*', 1); + + act(() => { + result.current.updatePatternRollup(patternRollup); + }); + + const mockOnCheckCompletedOpts = { + batchId: 'test-batch', + checkAllStartTime: Date.now(), + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-1', + partitionedFieldMetadata: null, + pattern: 'packetbeat-*', + requestTime: 1500, + isLastCheck: true, + isCheckAll: true, + }; + + act(() => { + result.current.onCheckCompleted(mockOnCheckCompletedOpts); + }); + + expect( + result.current.patternRollups['packetbeat-*'].results?.['.ds-packetbeat-1'] + ).toEqual({ + checkedAt: undefined, + docsCount: 1000000, + error: null, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-1', + markdownComments: expect.any(Array), + pattern: 'packetbeat-*', + sameFamily: undefined, + }); + + expect(mockTelemetryEvents.reportDataQualityIndexChecked).not.toHaveBeenCalled(); + + expect(httpFetch).not.toHaveBeenCalledWith( + '/internal/ecs_data_quality_dashboard/results', + expect.any(Object) + ); + }); + }); + }); + }); + + describe('calculating totals', () => { + describe('when patternRollups change', () => { + it('should update totals', () => { + const { result } = renderHook(() => + useResultsRollup({ + httpFetch, + toasts, + patterns: ['packetbeat-*', 'auditbeat-*'], + isILMAvailable, + telemetryEvents: mockTelemetryEvents, + }) + ); + + const patternRollup1 = getPatternRollupStub('packetbeat-*', 1); + const patternRollup2 = getPatternRollupStub('auditbeat-*', 1); + + expect(result.current.totalIndices).toBe(0); + expect(result.current.totalDocsCount).toBe(0); + expect(result.current.totalSizeInBytes).toBe(0); + + act(() => { + result.current.updatePatternRollup(patternRollup1); + }); + + expect(result.current.totalIndices).toEqual(1); + expect(result.current.totalDocsCount).toEqual(1000000); + expect(result.current.totalSizeInBytes).toEqual(500000000); + + act(() => { + result.current.updatePatternRollup(patternRollup2); + }); + + expect(result.current.totalIndices).toEqual(2); + expect(result.current.totalDocsCount).toEqual(2000000); + expect(result.current.totalSizeInBytes).toEqual(1000000000); + }); + }); + }); +}); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx index 28b36765a245b..d95f1d1b7f20f 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx @@ -21,83 +21,29 @@ import { getTotalPatternSameFamily, getIndexId, } from './utils/stats'; -import { - getStorageResults, - postStorageResult, - formatStorageResult, - formatResultFromStorage, -} from './utils/storage'; +import { postStorageResult, formatStorageResult } from './utils/storage'; import { getPatternRollupsWithLatestCheckResult } from './utils/get_pattern_rollups_with_latest_check_result'; -import type { - DataQualityCheckResult, - OnCheckCompleted, - PatternRollup, - TelemetryEvents, -} from '../../types'; +import type { OnCheckCompleted, PatternRollup, TelemetryEvents } from '../../types'; import { getEscapedIncompatibleMappingsFields, getEscapedIncompatibleValuesFields, getEscapedSameFamilyFields, } from './utils/metadata'; import { UseResultsRollupReturnValue } from './types'; -import { useIsMountedRef } from '../use_is_mounted_ref'; import { getDocsCount, getIndexIncompatible, getSizeInBytes } from '../../utils/stats'; import { getIlmPhase } from '../../utils/get_ilm_phase'; +import { useStoredPatternResults } from './hooks/use_stored_pattern_results'; interface Props { - ilmPhases: string[]; patterns: string[]; toasts: IToasts; httpFetch: HttpHandler; telemetryEvents: TelemetryEvents; isILMAvailable: boolean; } -const useStoredPatternResults = (patterns: string[], toasts: IToasts, httpFetch: HttpHandler) => { - const { isMountedRef } = useIsMountedRef(); - const [storedPatternResults, setStoredPatternResults] = useState< - Array<{ pattern: string; results: Record }> - >([]); - - useEffect(() => { - if (isEmpty(patterns)) { - return; - } - - let ignore = false; - const abortController = new AbortController(); - const fetchStoredPatternResults = async () => { - const requests = patterns.map((pattern) => - getStorageResults({ pattern, httpFetch, abortController, toasts }).then((results = []) => ({ - pattern, - results: Object.fromEntries( - results.map((storageResult) => [ - storageResult.indexName, - formatResultFromStorage({ storageResult, pattern }), - ]) - ), - })) - ); - const patternResults = await Promise.all(requests); - if (patternResults?.length && !ignore) { - if (isMountedRef.current) { - setStoredPatternResults(patternResults); - } - } - }; - - fetchStoredPatternResults(); - return () => { - ignore = true; - }; - }, [httpFetch, isMountedRef, patterns, toasts]); - - return storedPatternResults; -}; - export const useResultsRollup = ({ httpFetch, toasts, - ilmPhases, patterns, isILMAvailable, telemetryEvents, @@ -247,12 +193,6 @@ export const useResultsRollup = ({ [httpFetch, isILMAvailable, telemetryEvents, toasts] ); - useEffect(() => { - // reset all state - setPatternRollups({}); - setPatternIndexNames({}); - }, [ilmPhases, patterns]); - const useResultsRollupReturnValue = useMemo( () => ({ onCheckCompleted, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.tsx index 7d1a106d83570..b6d2736d7e175 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.tsx @@ -104,7 +104,6 @@ const DataQualityPanelComponent: React.FC = ({ ); const resultsRollupHookReturnValue = useResultsRollup({ - ilmPhases, patterns, httpFetch, toasts, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/format.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/format.ts new file mode 100644 index 0000000000000..844b573b61cad --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/format.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import numeral from '@elastic/numeral'; + +import { EMPTY_STAT } from '../../../constants'; + +const defaultBytesFormat = '0,0.[0]b'; +export const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +export const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/get_merged_data_quality_context_props.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/get_merged_data_quality_context_props.ts index 264198e510b5e..a8df6818605a1 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/get_merged_data_quality_context_props.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/utils/get_merged_data_quality_context_props.ts @@ -5,10 +5,9 @@ * 2.0. */ -import numeral from '@elastic/numeral'; - import { DataQualityProviderProps } from '../../../data_quality_context'; -import { EMPTY_STAT } from '../../../constants'; + +import { formatBytes as formatBytesMock, formatNumber as formatNumberMock } from './format'; export const getMergedDataQualityContextProps = ( dataQualityContextProps?: Partial @@ -36,10 +35,8 @@ export const getMergedDataQualityContextProps = ( addSuccessToast: jest.fn(), canUserCreateAndReadCases: jest.fn(() => true), endDate: null, - formatBytes: (value: number | undefined) => - value != null ? numeral(value).format('0,0.[0]b') : EMPTY_STAT, - formatNumber: (value: number | undefined) => - value != null ? numeral(value).format('0,0.[000]') : EMPTY_STAT, + formatBytes: formatBytesMock, + formatNumber: formatNumberMock, isAssistantEnabled: true, lastChecked: '2023-03-28T22:27:28.159Z', openCreateCaseFlyout: jest.fn(), diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_pattern_rollup_stub/index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_pattern_rollup_stub/index.ts new file mode 100644 index 0000000000000..38aa129a6ec9a --- /dev/null +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/stub/get_pattern_rollup_stub/index.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PatternRollup } from '../../types'; + +const phases = ['hot', 'warm', 'cold', 'frozen'] as const; + +/** + * + * This function derives ilmExplain, results, stats and ilmExplainPhaseCounts + * from the provided pattern and indicesCount for the purpose of simplifying + * stubbing of resultsRollup in tests. + * + * @param pattern - The index pattern to simulate. Defaults to `'packetbeat-*'`. + * @param indicesCount - The number of indices to generate. Defaults to `2`. + * @param isILMAvailable - Whether ILM is available. Defaults to `true`. + * @returns An object containing stubbed pattern rollup data + */ +export const getPatternRollupStub = ( + pattern = 'packetbeat-*', + indicesCount = 2, + isILMAvailable = true +): PatternRollup => { + // Derive ilmExplain from isILMAvailable, pattern and indicesCount + const ilmExplain = isILMAvailable + ? Object.fromEntries( + Array.from({ length: indicesCount }).map((_, i) => { + const indexName = pattern.replace('*', `${i + 1}`); + const dsIndexName = `.ds-${indexName}`; + // Cycle through phases + const phase = phases[i % phases.length]; + return [ + dsIndexName, + { + index: dsIndexName, + managed: true, + policy: pattern, + phase, + }, + ]; + }) + ) + : null; + + // Derive ilmExplainPhaseCounts from ilmExplain + const ilmExplainPhaseCounts = ilmExplain + ? phases.reduce( + (counts, phase) => ({ + ...counts, + [phase]: Object.values(ilmExplain).filter((explain) => explain.phase === phase).length, + }), + { hot: 0, warm: 0, cold: 0, frozen: 0, unmanaged: 0 } + ) + : undefined; + + // Derive results from pattern and indicesCount + const results = Object.fromEntries( + Array.from({ length: indicesCount }, (_, i) => { + const indexName = pattern.replace('*', `${i + 1}`); + const dsIndexName = `.ds-${indexName}`; + return [ + dsIndexName, + { + docsCount: 1000000 + i * 100000, // Example doc count + error: null, + ilmPhase: ilmExplain?.[dsIndexName].phase, + incompatible: i, + indexName: dsIndexName, + markdownComments: ['foo', 'bar', 'baz'], + pattern, + sameFamily: i, + checkedAt: Date.now(), + }, + ]; + }) + ); + + // Derive stats from isILMAvailable, pattern and indicesCount + const stats = Object.fromEntries( + Array.from({ length: indicesCount }, (_, i) => { + const indexName = pattern.replace('*', `${i + 1}`); + const dsIndexName = `.ds-${indexName}`; + return [ + dsIndexName, + { + uuid: `uuid-${i + 1}`, + size_in_bytes: isILMAvailable ? 500000000 + i * 10000000 : null, + name: dsIndexName, + num_docs: results[dsIndexName].docsCount, + }, + ]; + }) + ); + + // Derive total docsCount and sizeInBytes from stats + const totalDocsCount = Object.values(stats).reduce((sum, stat) => sum + stat.num_docs, 0); + const totalSizeInBytes = isILMAvailable + ? Object.values(stats).reduce((sum, stat) => sum + (stat.size_in_bytes ?? 0), 0) + : undefined; + + return { + docsCount: totalDocsCount, + error: null, + pattern, + ilmExplain, + ilmExplainPhaseCounts, + indices: indicesCount, + results, + sizeInBytes: totalSizeInBytes, + stats, + }; +}; From 21cc331bfc1b08b2166b1dcbf05e624c239a7974 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 08:59:34 +1100 Subject: [PATCH 43/47] [8.x] [Canvas] Remove expression lifecycle docs (#199006) (#199386) # Backport This will backport the following commits from `main` to `8.x`: - [[Canvas] Remove expression lifecycle docs (#199006)](https://github.com/elastic/kibana/pull/199006) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Catherine Liu --- .../canvas-expression-lifecycle.asciidoc | 263 ------------------ docs/user/canvas.asciidoc | 2 - 2 files changed, 265 deletions(-) delete mode 100644 docs/canvas/canvas-expression-lifecycle.asciidoc diff --git a/docs/canvas/canvas-expression-lifecycle.asciidoc b/docs/canvas/canvas-expression-lifecycle.asciidoc deleted file mode 100644 index a20181c4b3808..0000000000000 --- a/docs/canvas/canvas-expression-lifecycle.asciidoc +++ /dev/null @@ -1,263 +0,0 @@ -[role="xpack"] -[[canvas-expression-lifecycle]] -== Canvas expression lifecycle - -Elements in Canvas are all created using an *expression language* that defines how to retrieve, manipulate, and ultimately visualize data. The goal is to allow you to do most of what you need without understanding the *expression language*, but learning how it works unlocks a lot of Canvas's power. - - -[[canvas-expressions-always-start-with-a-function]] -=== Expressions always start with a function - -Expressions simply execute <> in a specific order, which produce some output value. That output can then be inserted into another function, and another after that, until it produces the output you need. - -To use demo dataset available in Canvas to produce a table, run the following expression: - -[source,text] ----- -/* Simple demo table */ -filters -| demodata -| table -| render ----- - -This expression starts out with the <> function, which provides the value of any time filters or dropdown filters in the workpad. This is then inserted into <>, a function that returns exactly what you expect, demo data. Because the <> function receives the filter information from the <> function before it, it applies those filters to reduce the set of data it returns. We call the output from the previous function _context_. - -The filtered <> becomes the _context_ of the next function, <>, which creates a table visualization from this data set. The <> function isn’t strictly required, but by being explicit, you have the option of providing arguments to control things like the font used in the table. The output of the <> function becomes the _context_ of the <> function. Like the <>, the <> function isn’t required either, but it allows access to other arguments, such as styling the border of the element or injecting custom CSS. - -It is possible to add comments to the expression by starting them with a `//` sequence or by using `/*` and `*/` to enclose multi-line comments. - -[[canvas-function-arguments]] -=== Function arguments - -Let’s look at another expression, which uses the same <> function, but instead produces a pie chart. - -image::images/canvas-functions-can-take-arguments-pie-chart.png[Pie chart showing output of demodata function] -[source,text] ----- -filters -| demodata -| pointseries color="state" size="max(price)" -| pie -| render ----- - -To produce a filtered set of random data, the expression uses the <> and <> functions. This time, however, the output becomes the context for the <> function, which is a way to aggregate your data, similar to how Elasticsearch works, but more generalized. In this case, the data is split up using the `color` and `size` dimensions, using arguments on the <> function. Each unique value in the state column will have an associated size value, which in this case, will be the maximum value of the price column. - -If the expression stopped there, it would produce a `pointseries` data type as the output of this expression. But instead of looking at the raw values, the result is inserted into the <> function, which will produce an output that will render a pie visualization. And just like before, this is inserted into the <> function, which is useful for its arguments. - -The end result is a simple pie chart that uses the default color palette, but the <> function can take additional arguments that control how it gets rendered. For example, you can provide a `hole` argument to turn your pie chart into a donut chart by changing the expression to: - - -image::images/canvas-functions-can-take-arguments-donut-chart.png[Alternative output as donut chart] -[source,text] ----- -filters -| demodata -| pointseries color="state" size="max(price)" -| pie hole=50 -| render ----- - - -[[canvas-aliases-and-unnamed-arguments]] -=== Aliases and unnamed arguments - -Argument definitions have one canonical name, which is always provided in the underlying code. When argument definitions are used in an expression, they often include aliases that make them easier or faster to type. - -For example, the <> function has 2 arguments: - -* `expression` - Produces a calculated value. -* `name` - The name of column. - -The `expression` argument includes some aliases, namely `exp`, `fn`, and `function`. That means that you can use any of those four options to provide that argument’s value. - -So `mapColumn name=newColumn fn={string example}` is equal to `mapColumn name=newColumn expression={string example}`. - -There’s also a special type of alias which allows you to leave off the argument’s name entirely. The alias for this is an underscore, which indicates that the argument is an _unnamed_ argument and can be provided without explicitly naming it in the expression. The `name` argument here uses the _unnamed_ alias, which means that you can further simplify our example to `mapColumn newColumn fn={string example}`. - -NOTE: There can only be one _unnamed_ argument for each function. - - -[[canvas-change-your-expression-change-your-output]] -=== Change your expression, change your output -You can substitute one function for another to change the output. For example, you could change the visualization by swapping out the <> function for another renderer, a function that returns a `render` data type. - -Let’s change that last pie chart into a bubble chart by replacing the <> function with the <> function. This is possible because both functions can accept a `pointseries` data type as their _context_. Switching the functions will work, but it won’t produce a useful visualization on its own since you don’t have the x-axis and y-axis defined. You will also need to modify the <> function to change its output. In this case, you can change the `size` argument to `y`, so the maximum price values are plotted on the y-axis, and add an `x` argument using the `@timestamp` field in the data to plot those values over time. This leaves you with the following expression and produces a bubble chart showing the max price of each state over time: - -image::images/canvas-change-your-expression-chart.png[Bubble Chart, with price along x axis, and time along y axis] -[source,text] ----- -filters -| demodata -| pointseries color="state" y="max(price)" x="@timestamp" -| plot -| render ----- - -Similar to the <> function, the <> function takes arguments that control the design elements of the visualization. As one example, passing a `legend` argument with a value of `false` to the function will hide the legend on the chart. - -image::images/canvas-change-your-expression-chart-no-legend.png[Bubble Chart Without Legend] -[source,text,subs=+quotes] ----- -filters -| demodata -| pointseries color="state" y="max(price)" x="@timestamp" -| plot *legend=false* -| render ----- - - -[[canvas-fetch-and-manipulate-data]] -=== Fetch and manipulate data -So far, you have only seen expressions as a way to produce visualizations, but that’s not really what’s happening. Expressions only produce data, which is then used to create something, which in the case of Canvas, means rendering an element. An element can be a visualization, driven by data, but it can also be something much simpler, like a static image. Either way, an expression is used to produce an output that is used to render the desired result. For example, here’s an expression that shows an image: - -[source,text] ----- -image dataurl=https://placekitten.com/160/160 mode="cover" ----- - -But as mentioned, this doesn’t actually _render that image_, but instead it _produces some output that can be used to render that image_. That’s an important distinction, and you can see the actual output by adding in the render function and telling it to produce debug output. For example: - -[source,text] ----- -image dataurl=https://placekitten.com/160/160 mode="cover" -| render as=debug ----- - -The follow appears as JSON output: - -[source,JSON] ----- -{ - "type": "image", - "mode": "cover", - "dataurl": "https://placekitten.com/160/160" -} ----- - -NOTE: You may need to expand the element’s size to see the whole output. - -Canvas uses this output’s data type to map to a specific renderer and passes the entire output into it. It’s up to the image render function to produce an image on the workpad’s page. In this case, the expression produces some JSON output, but expressions can also produce other, simpler data, like a string or a number. Typically, useful results use JSON. - -Canvas uses the output to render an element, but other applications can use expressions to do pretty much anything. As stated previously, expressions simply execute functions, and the functions are all written in Javascript. That means if you can do something in Javascript, you can do it with an expression. - -This can include: - -* Sending emails -* Sending notifications -* Reading from a file -* Writing to a file -* Controlling devices with WebUSB or Web Bluetooth -* Consuming external APIs - -If your Javascript works in the environment where the code will run, such as in Node.js or in a browser, you can do it with an expression. - -[[canvas-expressions-compose-functions-with-subexpressions]] -=== Compose functions with sub-expressions - -You may have noticed another syntax in examples from other sections, namely expressions inside of curly brackets. These are called sub-expressions, and they can be used to provide a calculated value to another expression, instead of just a static one. - -A simple example of this is when you upload your own images to a Canvas workpad. That upload becomes an asset, and that asset can be retrieved using the `asset` function. Usually you’ll just do this from the UI, adding an image element to the page and uploading your image from the control in the sidebar, or picking an existing asset from there as well. In both cases, the system will consume that asset via the `asset` function, and you’ll end up with an expression similar to this: - -[source,text] ----- -image dataurl={asset 3cb3ec3a-84d7-48fa-8709-274ad5cc9e0b} ----- - -Sub-expressions are executed before the function that uses them is executed. In this case, `asset` will be run first, it will produce a value, the base64-encoded value of the image and that value will be used as the value for the `dataurl` argument in the <> function. After the asset function executes, you will get the following output: - -[source,text] ----- -image dataurl="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0" ----- - -Since all of the sub-expressions are now resolved into actual values, the <> function can be executed to produce its JSON output, just as it’s explained previously. In the case of images, the ability to nest sub-expressions is particularly useful to show one of several images conditionally. For example, you could swap between two images based on some calculated value by mixing in the <> function, like in this example expression: - -[source,text] ----- -demodata -| image dataurl={ - if condition={getCell price | gte 100} - then={asset "asset-3cb3ec3a-84d7-48fa-8709-274ad5cc9e0b"} - else={asset "asset-cbc11a1f-8f25-4163-94b4-2c3a060192e7"} -} ----- - -NOTE: The examples in this section can’t be copy and pasted directly, since the values used throughout will not exist in your workpad. - -Here, the expression to use for the value of the `condition` argument, `getCell price | gte 100`, runs first since it is nested deeper. - -The expression does the following: - -* Retrieves the value from the *price* column in the first row of the `demodata` data table -* Inputs the value to the `gte` function -* Compares the value to `100` -* Returns `true` if the value is 100 or greater, and `false` if the value is 100 or less - -That boolean value becomes the value for the `condition` argument. The output from the `then` expression is used as the output when `condition` is `true`. The output from the `else` expression is used when `condition` is false. In both cases, a base64-encoded image will be returned, and one of the two images will be displayed. - -You might be wondering how the <> function in the sub-expression accessed the data from the <> function, even though <> was not being directly inserted into <>. The answer is simple, but important to understand. When nested sub-expressions are executed, they automatically receive the same _context_, or output of the previous function that its parent function receives. In this specific expression, demodata’s data table is automatically provided to the nested expression’s `getCell` function, which allows that expression to pull out a value and compare it to another value. - -The passing of the _context_ is automatic, and it happens no matter how deeply you nest your sub-expressions. To demonstrate this, let’s modify the expression slightly to compare the value of the price against multiple conditions using the <> function. - -[source,text] ----- -demodata -| image dataurl={ - if condition={getCell price | all {gte 100} {neq 105}} - then={asset 3cb3ec3a-84d7-48fa-8709-274ad5cc9e0b} - else={asset cbc11a1f-8f25-4163-94b4-2c3a060192e7} -} ----- - -This time, `getCell price` is run, and the result is passed into the next function as the context. Then, each sub-expression of the <> function is run, with the context given to their parent, which in this case is the result of `getCell price`. If `all` of these sub-expressions evaluate to `true`, then the `if` condition argument will be true. - -Sub-expressions can seem a little foreign, especially if you aren’t a developer, but they’re worth getting familiar with, since they provide a ton of power and flexibility. Since you can nest any expression you want, you can also use this behavior to mix data from multiple indices, or even data from multiple sources. As an example, you could query an API for a value to use as part of the query provided to <>. - -This whole section is really just scratching the surface, but hopefully after reading it, you at least understand how to read expressions and make sense of what they are doing. With a little practice, you’ll get the hang of mixing _context_ and sub-expressions together to turn any input into your desired output. - -[[canvas-handling-context-and-argument-types]] -=== Handling context and argument types -If you look through the <>, you may notice that all of them define what a function accepts and what it returns. Additionally, every argument includes a type property that specifies the kind of data that can be used. These two types of values are actually the same, and can be used as a guide for how to deal with piping to other functions and using subexpressions for argument values. - -To explain how this works, consider the following expression from the previous section: - -[source,text] ----- -image dataurl={asset 3cb3ec3a-84d7-48fa-8709-274ad5cc9e0b} ----- - -If you <> for the `image` function, you’ll see that it accepts the `null` data type and returns an `image` data type. Accepting `null` effectively means that it does not use context at all, so if you insert anything to `image`, the value that was produced previously will be ignored. When the function executes, it will produce an `image` output, which is simply an object of type `image` that contains the information required to render an image. - -NOTE: The function does not render an image itself. - -As explained in the "<>" section, the output of an expression is just data. So the `image` type here is just a specific shape of data, not an actual image. - -Next, let’s take a look at the `asset` function. Like `image`, it accepts `null`, but it returns something different, a `string` in this case. Because `asset` will produce a string, its output can be used as the input for any function or argument that accepts a string. - -<> for the `dataurl` argument, its type is `string`, meaning it will accept any kind of string. There are some rules about the value of the string that the function itself enforces, but as far as the interpreter is concerned, that expression is valid because the argument accepts a string and the output of `asset` is a string. - -The interpreter also attempts to cast some input types into others, which allows you to use a string input even when the function or argument calls for a number. Keep in mind that it’s not able to convert any string value, but if the string is a number, it can easily be cast into a `number` type. Take the following expression for example: - -[source,text] ----- -string "0.4" -| revealImage image={asset asset-06511b39-ec44-408a-a5f3-abe2da44a426} ----- - -If you <> for the `revealImage` function, you’ll see that it accepts a `number` but the `string` function returns a `string` type. In this case, because the string value is a number, it can be converted into a `number` type and used without you having to do anything else. - -Most `primitive` types can be converted automatically, as you might expect. You just saw that a `string` can be cast into a `number`, but you can also pretty easily cast things into `boolean` too, and you can cast anything to `null`. - -There are other useful type casting options available. For example, something of type `datatable` can be cast to a type `pointseries` simply by only preserving specific columns from the data (namely x, y, size, color, and text). This allows you to treat your source data, which is generally of type `datatable`, like a `pointseries` type simply by convention. - -You can fetch data from Elasticsearch using `essql`, which allows you to aggregate the data, provide a custom name for the value, and insert that data directly to another function that only accepts `pointseries` even though `essql` will output a `datatable` type. This makes the following example expression valid: - -[source,text] ----- -essql "SELECT user AS x, sum(cost) AS y FROM index GROUP BY user" -| plot ----- - -In the docs you can see that `essql` returns a `datatable` type, but `plot` expects a `pointseries` context. This works because the `datatable` output will have the columns `x` and `y` as a result of using `AS` in the sql statement to name them. Because the data follows the convention of the `pointseries` data type, casting it into `pointseries` is possible, and it can be passed directly to `plot` as a result. diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index e7b4fdaf20921..3767e59c56b74 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -185,8 +185,6 @@ include::{kibana-root}/docs/canvas/canvas-present-workpad.asciidoc[] include::{kibana-root}/docs/canvas/canvas-tutorial.asciidoc[] -include::{kibana-root}/docs/canvas/canvas-expression-lifecycle.asciidoc[] - include::{kibana-root}/docs/canvas/canvas-function-reference.asciidoc[] include::{kibana-root}/docs/canvas/canvas-tinymath-functions.asciidoc[] From de032ca798311a8db33c0597b6a3591466992a44 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:07:53 +1100 Subject: [PATCH 44/47] [8.x] [DataUsage][Serverless] Add missing tests (#198007) (#199375) # Backport This will backport the following commits from `main` to `8.x`: - [[DataUsage][Serverless] Add missing tests (#198007)](https://github.com/elastic/kibana/pull/198007) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Ash <1849116+ashokaditya@users.noreply.github.com> --- .../test_utils/test_query_client_options.ts | 20 + .../public/app/components/charts.tsx | 7 +- .../components/data_usage_metrics.test.tsx | 380 ++++++++++++++++++ .../app/components/data_usage_metrics.tsx | 360 +++++++++-------- .../app/components/filters/charts_filter.tsx | 7 +- .../filters/charts_filter_popover.tsx | 2 +- .../app/components/filters/charts_filters.tsx | 9 +- .../app/components/filters/date_picker.tsx | 13 +- .../app/hooks/use_charts_url_params.test.tsx | 79 ++++ .../hooks/use_get_data_streams.test.tsx | 120 ++++++ .../public/hooks/use_get_data_streams.ts | 5 +- .../hooks/use_get_usage_metrics.test.tsx | 102 +++++ .../public/hooks/use_get_usage_metrics.ts | 2 +- .../public/utils/format_bytes.test.ts | 24 ++ .../plugins/data_usage/server/mocks/index.ts | 37 ++ .../routes/internal/data_streams.test.ts | 124 ++++++ .../routes/internal/data_streams_handler.ts | 35 +- .../routes/internal/usage_metrics.test.ts | 208 ++++++++++ .../server/utils/get_metering_stats.ts | 24 ++ x-pack/plugins/data_usage/tsconfig.json | 1 + 20 files changed, 1344 insertions(+), 215 deletions(-) create mode 100644 x-pack/plugins/data_usage/common/test_utils/test_query_client_options.ts create mode 100644 x-pack/plugins/data_usage/public/app/components/data_usage_metrics.test.tsx create mode 100644 x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.test.tsx create mode 100644 x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx create mode 100644 x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx create mode 100644 x-pack/plugins/data_usage/public/utils/format_bytes.test.ts create mode 100644 x-pack/plugins/data_usage/server/mocks/index.ts create mode 100644 x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts create mode 100644 x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts create mode 100644 x-pack/plugins/data_usage/server/utils/get_metering_stats.ts diff --git a/x-pack/plugins/data_usage/common/test_utils/test_query_client_options.ts b/x-pack/plugins/data_usage/common/test_utils/test_query_client_options.ts new file mode 100644 index 0000000000000..c674e9b342eea --- /dev/null +++ b/x-pack/plugins/data_usage/common/test_utils/test_query_client_options.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* eslint-disable no-console */ +export const dataUsageTestQueryClientOptions = { + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, + logger: { + log: console.log, + warn: console.warn, + error: () => {}, + }, +}; diff --git a/x-pack/plugins/data_usage/public/app/components/charts.tsx b/x-pack/plugins/data_usage/public/app/components/charts.tsx index 8d04324fb2246..56857e7a63ff9 100644 --- a/x-pack/plugins/data_usage/public/app/components/charts.tsx +++ b/x-pack/plugins/data_usage/public/app/components/charts.tsx @@ -9,18 +9,21 @@ import { EuiFlexGroup } from '@elastic/eui'; import { MetricTypes } from '../../../common/rest_types'; import { ChartPanel } from './chart_panel'; import { UsageMetricsResponseSchemaBody } from '../../../common/rest_types'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; interface ChartsProps { data: UsageMetricsResponseSchemaBody; + 'data-test-subj'?: string; } -export const Charts: React.FC = ({ data }) => { +export const Charts: React.FC = ({ data, 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); const [popoverOpen, setPopoverOpen] = useState(null); const togglePopover = useCallback((streamName: string | null) => { setPopoverOpen((prev) => (prev === streamName ? null : streamName)); }, []); return ( - + {Object.entries(data.metrics).map(([metricType, series], idx) => ( { + return { + useBreadcrumbs: jest.fn(), + }; +}); + +jest.mock('../../utils/use_kibana', () => { + return { + useKibanaContextForPlugin: () => ({ + services: mockServices, + }), + }; +}); + +jest.mock('../../hooks/use_get_usage_metrics', () => { + const original = jest.requireActual('../../hooks/use_get_usage_metrics'); + return { + ...original, + useGetDataUsageMetrics: jest.fn(original.useGetDataUsageMetrics), + }; +}); + +const mockUseLocation = jest.fn(() => ({ pathname: '/' })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => mockUseLocation(), + useHistory: jest.fn().mockReturnValue({ + push: jest.fn(), + listen: jest.fn(), + location: { + search: '', + }, + }), +})); + +jest.mock('../../hooks/use_get_data_streams', () => { + const original = jest.requireActual('../../hooks/use_get_data_streams'); + return { + ...original, + useGetDataUsageDataStreams: jest.fn(original.useGetDataUsageDataStreams), + }; +}); + +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + useKibana: () => ({ + services: { + uiSettings: { + get: jest.fn().mockImplementation((key) => { + const get = (k: 'dateFormat' | 'timepicker:quickRanges') => { + const x = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', + 'timepicker:quickRanges': [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, + ], + }; + return x[k]; + }; + return get(key); + }), + }, + }, + }), + }; +}); +const mockUseGetDataUsageMetrics = useGetDataUsageMetrics as jest.Mock; +const mockUseGetDataUsageDataStreams = useGetDataUsageDataStreams as jest.Mock; +const mockServices = mockCore.createStart(); + +const getBaseMockedDataStreams = () => ({ + error: undefined, + data: undefined, + isFetching: false, + refetch: jest.fn(), +}); +const getBaseMockedDataUsageMetrics = () => ({ + error: undefined, + data: undefined, + isFetching: false, + refetch: jest.fn(), +}); + +describe('DataUsageMetrics', () => { + let user: UserEvent; + const testId = 'test'; + const testIdFilter = `${testId}-filter`; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime, pointerEventsCheck: 0 }); + mockUseGetDataUsageMetrics.mockReturnValue(getBaseMockedDataUsageMetrics); + mockUseGetDataUsageDataStreams.mockReturnValue(getBaseMockedDataStreams); + }); + + it('renders', () => { + const { getByTestId } = render(); + expect(getByTestId(`${testId}`)).toBeTruthy(); + }); + + it('should show date filter', () => { + const { getByTestId } = render(); + expect(getByTestId(`${testIdFilter}-date-range`)).toBeTruthy(); + expect(getByTestId(`${testIdFilter}-date-range`).textContent).toContain('Last 24 hours'); + expect(getByTestId(`${testIdFilter}-super-refresh-button`)).toBeTruthy(); + }); + + it('should not show data streams filter while fetching API', () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + ...getBaseMockedDataStreams, + isFetching: true, + }); + const { queryByTestId } = render(); + expect(queryByTestId(`${testIdFilter}-dataStreams-popoverButton`)).not.toBeTruthy(); + }); + + it('should show data streams filter', () => { + const { getByTestId } = render(); + expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toBeTruthy(); + }); + + it('should show selected data streams on the filter', () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + error: undefined, + data: [ + { + name: '.ds-1', + storageSizeBytes: 10000, + }, + { + name: '.ds-2', + storageSizeBytes: 20000, + }, + { + name: '.ds-3', + storageSizeBytes: 10300, + }, + { + name: '.ds-4', + storageSizeBytes: 23000, + }, + { + name: '.ds-5', + storageSizeBytes: 23200, + }, + ], + isFetching: false, + }); + const { getByTestId } = render(); + expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent( + 'Data streams5' + ); + }); + + it('should allow de-selecting all but one data stream option', async () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + error: undefined, + data: [ + { + name: '.ds-1', + storageSizeBytes: 10000, + }, + { + name: '.ds-2', + storageSizeBytes: 20000, + }, + { + name: '.ds-3', + storageSizeBytes: 10300, + }, + { + name: '.ds-4', + storageSizeBytes: 23000, + }, + { + name: '.ds-5', + storageSizeBytes: 23200, + }, + ], + isFetching: false, + }); + const { getByTestId, getAllByTestId } = render(); + expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent( + 'Data streams5' + ); + await user.click(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)); + const allFilterOptions = getAllByTestId('dataStreams-filter-option'); + for (let i = 0; i < allFilterOptions.length - 1; i++) { + await user.click(allFilterOptions[i]); + } + + expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent( + 'Data streams1' + ); + }); + + it('should not call usage metrics API if no data streams', async () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + ...getBaseMockedDataStreams, + data: [], + }); + render(); + expect(mockUseGetDataUsageMetrics).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ enabled: false }) + ); + }); + + it('should show charts loading if data usage metrics API is fetching', () => { + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetching: true, + }); + const { getByTestId } = render(); + expect(getByTestId(`${testId}-charts-loading`)).toBeTruthy(); + }); + + it('should show charts', () => { + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetched: true, + data: { + metrics: { + ingest_rate: [ + { + name: '.ds-1', + data: [{ x: new Date(), y: 1000 }], + }, + { + name: '.ds-10', + data: [{ x: new Date(), y: 1100 }], + }, + ], + storage_retained: [ + { + name: '.ds-2', + data: [{ x: new Date(), y: 2000 }], + }, + { + name: '.ds-20', + data: [{ x: new Date(), y: 2100 }], + }, + ], + }, + }, + }); + const { getByTestId } = render(); + expect(getByTestId(`${testId}-charts`)).toBeTruthy(); + }); + + it('should refetch usage metrics with `Refresh` button click', async () => { + const refetch = jest.fn(); + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + data: ['.ds-1', '.ds-2'], + isFetched: true, + }); + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetched: true, + refetch, + }); + const { getByTestId } = render(); + const refreshButton = getByTestId(`${testIdFilter}-super-refresh-button`); + // click refresh 5 times + for (let i = 0; i < 5; i++) { + await user.click(refreshButton); + } + + expect(mockUseGetDataUsageMetrics).toHaveBeenLastCalledWith( + expect.any(Object), + expect.objectContaining({ enabled: false }) + ); + expect(refetch).toHaveBeenCalledTimes(5); + }); + + it('should show error toast if usage metrics API fails', async () => { + mockUseGetDataUsageMetrics.mockReturnValue({ + ...getBaseMockedDataUsageMetrics, + isFetched: true, + error: new Error('Uh oh!'), + }); + render(); + await waitFor(() => { + expect(mockServices.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Error getting usage metrics', + text: 'Uh oh!', + }); + }); + }); + + it('should show error toast if data streams API fails', async () => { + mockUseGetDataUsageDataStreams.mockReturnValue({ + ...getBaseMockedDataStreams, + isFetched: true, + error: new Error('Uh oh!'), + }); + render(); + await waitFor(() => { + expect(mockServices.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Error getting data streams', + text: 'Uh oh!', + }); + }); + }); +}); diff --git a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx index 929ebf7a02490..59354a1746346 100644 --- a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx +++ b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -14,11 +14,12 @@ import { useBreadcrumbs } from '../../utils/use_breadcrumbs'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; import { PLUGIN_NAME } from '../../../common'; import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics'; +import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams'; import { useDataUsageMetricsUrlParams } from '../hooks/use_charts_url_params'; import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from '../hooks/use_date_picker'; import { DEFAULT_METRIC_TYPES, UsageMetricsRequestBody } from '../../../common/rest_types'; import { ChartFilters, ChartFiltersProps } from './filters/charts_filters'; -import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; const EuiItemCss = css` width: 100%; @@ -28,181 +29,188 @@ const FlexItemWithCss = ({ children }: { children: React.ReactNode }) => ( {children} ); -export const DataUsageMetrics = () => { - const { - services: { chrome, appParams, notifications }, - } = useKibanaContextForPlugin(); - useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); - - const { - metricTypes: metricTypesFromUrl, - dataStreams: dataStreamsFromUrl, - startDate: startDateFromUrl, - endDate: endDateFromUrl, - setUrlMetricTypesFilter, - setUrlDataStreamsFilter, - setUrlDateRangeFilter, - } = useDataUsageMetricsUrlParams(); - - const { - error: errorFetchingDataStreams, - data: dataStreams, - isFetching: isFetchingDataStreams, - } = useGetDataUsageDataStreams({ - selectedDataStreams: dataStreamsFromUrl, - options: { - enabled: true, - retry: false, - }, - }); - - const [metricsFilters, setMetricsFilters] = useState({ - metricTypes: [...DEFAULT_METRIC_TYPES], - dataStreams: [], - from: DEFAULT_DATE_RANGE_OPTIONS.startDate, - to: DEFAULT_DATE_RANGE_OPTIONS.endDate, - }); - - useEffect(() => { - if (!metricTypesFromUrl) { - setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); - } - if (!dataStreamsFromUrl && dataStreams) { - setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(',')); - } - if (!startDateFromUrl || !endDateFromUrl) { - setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); - } - }, [ - dataStreams, - dataStreamsFromUrl, - endDateFromUrl, - metricTypesFromUrl, - metricsFilters.dataStreams, - metricsFilters.from, - metricsFilters.metricTypes, - metricsFilters.to, - setUrlDataStreamsFilter, - setUrlDateRangeFilter, - setUrlMetricTypesFilter, - startDateFromUrl, - ]); - - useEffect(() => { - setMetricsFilters((prevState) => ({ - ...prevState, - metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, - dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, - })); - }, [metricTypesFromUrl, dataStreamsFromUrl]); - - const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); - - const { - error: errorFetchingDataUsageMetrics, - data, - isFetching, - isFetched, - refetch: refetchDataUsageMetrics, - } = useGetDataUsageMetrics( - { - ...metricsFilters, - from: dateRangePickerState.startDate, - to: dateRangePickerState.endDate, - }, - { - retry: false, - enabled: !!metricsFilters.dataStreams.length, - } - ); - - const onRefresh = useCallback(() => { - refetchDataUsageMetrics(); - }, [refetchDataUsageMetrics]); - - const onChangeDataStreamsFilter = useCallback( - (selectedDataStreams: string[]) => { - setMetricsFilters((prevState) => ({ ...prevState, dataStreams: selectedDataStreams })); - }, - [setMetricsFilters] - ); - - const onChangeMetricTypesFilter = useCallback( - (selectedMetricTypes: string[]) => { - setMetricsFilters((prevState) => ({ ...prevState, metricTypes: selectedMetricTypes })); - }, - [setMetricsFilters] - ); - - const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => { - const dataStreamsOptions = dataStreams?.reduce>((acc, ds) => { - acc[ds.name] = ds.storageSizeBytes; - return acc; - }, {}); - - return { - dataStreams: { - filterName: 'dataStreams', - options: dataStreamsOptions ? Object.keys(dataStreamsOptions) : metricsFilters.dataStreams, - appendOptions: dataStreamsOptions, - selectedOptions: metricsFilters.dataStreams, - onChangeFilterOptions: onChangeDataStreamsFilter, - isFilterLoading: isFetchingDataStreams, - }, - metricTypes: { - filterName: 'metricTypes', - options: metricsFilters.metricTypes, - onChangeFilterOptions: onChangeMetricTypesFilter, +export const DataUsageMetrics = memo( + ({ 'data-test-subj': dataTestSubj = 'data-usage-metrics' }: { 'data-test-subj'?: string }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + const { + services: { chrome, appParams, notifications }, + } = useKibanaContextForPlugin(); + useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); + + const { + metricTypes: metricTypesFromUrl, + dataStreams: dataStreamsFromUrl, + startDate: startDateFromUrl, + endDate: endDateFromUrl, + setUrlMetricTypesFilter, + setUrlDataStreamsFilter, + setUrlDateRangeFilter, + } = useDataUsageMetricsUrlParams(); + + const { + error: errorFetchingDataStreams, + data: dataStreams, + isFetching: isFetchingDataStreams, + } = useGetDataUsageDataStreams({ + selectedDataStreams: dataStreamsFromUrl, + options: { + enabled: true, + retry: false, }, - }; - }, [ - dataStreams, - isFetchingDataStreams, - metricsFilters.dataStreams, - metricsFilters.metricTypes, - onChangeDataStreamsFilter, - onChangeMetricTypesFilter, - ]); - - if (errorFetchingDataUsageMetrics) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.dataUsage.getMetrics.addFailure.toast.title', { - defaultMessage: 'Error getting usage metrics', - }), - text: errorFetchingDataUsageMetrics.message, }); - } - if (errorFetchingDataStreams) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.dataUsage.getDataStreams.addFailure.toast.title', { - defaultMessage: 'Error getting data streams', - }), - text: errorFetchingDataStreams.message, + + const [metricsFilters, setMetricsFilters] = useState({ + metricTypes: [...DEFAULT_METRIC_TYPES], + dataStreams: [], + from: DEFAULT_DATE_RANGE_OPTIONS.startDate, + to: DEFAULT_DATE_RANGE_OPTIONS.endDate, }); - } - return ( - - - - - - - {isFetched && data?.metrics ? ( - - ) : isFetching ? ( - - ) : null} - - - ); -}; + useEffect(() => { + if (!metricTypesFromUrl) { + setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); + } + if (!dataStreamsFromUrl && dataStreams) { + setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(',')); + } + if (!startDateFromUrl || !endDateFromUrl) { + setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); + } + }, [ + dataStreams, + dataStreamsFromUrl, + endDateFromUrl, + metricTypesFromUrl, + metricsFilters.dataStreams, + metricsFilters.from, + metricsFilters.metricTypes, + metricsFilters.to, + setUrlDataStreamsFilter, + setUrlDateRangeFilter, + setUrlMetricTypesFilter, + startDateFromUrl, + ]); + + useEffect(() => { + setMetricsFilters((prevState) => ({ + ...prevState, + metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, + dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, + })); + }, [metricTypesFromUrl, dataStreamsFromUrl]); + + const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); + + const { + error: errorFetchingDataUsageMetrics, + data, + isFetching, + isFetched, + refetch: refetchDataUsageMetrics, + } = useGetDataUsageMetrics( + { + ...metricsFilters, + from: dateRangePickerState.startDate, + to: dateRangePickerState.endDate, + }, + { + retry: false, + enabled: !!metricsFilters.dataStreams.length, + } + ); + + const onRefresh = useCallback(() => { + refetchDataUsageMetrics(); + }, [refetchDataUsageMetrics]); + + const onChangeDataStreamsFilter = useCallback( + (selectedDataStreams: string[]) => { + setMetricsFilters((prevState) => ({ ...prevState, dataStreams: selectedDataStreams })); + }, + [setMetricsFilters] + ); + + const onChangeMetricTypesFilter = useCallback( + (selectedMetricTypes: string[]) => { + setMetricsFilters((prevState) => ({ ...prevState, metricTypes: selectedMetricTypes })); + }, + [setMetricsFilters] + ); + + const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => { + const dataStreamsOptions = dataStreams?.reduce>((acc, ds) => { + acc[ds.name] = ds.storageSizeBytes; + return acc; + }, {}); + + return { + dataStreams: { + filterName: 'dataStreams', + options: dataStreamsOptions + ? Object.keys(dataStreamsOptions) + : metricsFilters.dataStreams, + appendOptions: dataStreamsOptions, + selectedOptions: metricsFilters.dataStreams, + onChangeFilterOptions: onChangeDataStreamsFilter, + isFilterLoading: isFetchingDataStreams, + }, + metricTypes: { + filterName: 'metricTypes', + options: metricsFilters.metricTypes, + onChangeFilterOptions: onChangeMetricTypesFilter, + }, + }; + }, [ + dataStreams, + isFetchingDataStreams, + metricsFilters.dataStreams, + metricsFilters.metricTypes, + onChangeDataStreamsFilter, + onChangeMetricTypesFilter, + ]); + + if (errorFetchingDataUsageMetrics) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.dataUsage.getMetrics.addFailure.toast.title', { + defaultMessage: 'Error getting usage metrics', + }), + text: errorFetchingDataUsageMetrics.message, + }); + } + if (errorFetchingDataStreams) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.dataUsage.getDataStreams.addFailure.toast.title', { + defaultMessage: 'Error getting data streams', + }), + text: errorFetchingDataStreams.message, + }); + } + + return ( + + + + + + + {isFetched && data?.metrics ? ( + + ) : isFetching ? ( + + ) : null} + + + ); + } +); diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx index 83d417565f012..6b4806537e74b 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx @@ -193,13 +193,10 @@ export const ChartsFilter = memo( > {(list, search) => { return ( -
+
{isSearchable && ( {search} diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx index 2ed96f012c497..3c0237c84a0c9 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx @@ -42,7 +42,7 @@ export const ChartsFilterPopover = memo( const button = useMemo( () => ( ( const filters = useMemo(() => { return ( <> - {showMetricsTypesFilter && } + {showMetricsTypesFilter && ( + + )} {!filterOptions.dataStreams.isFilterLoading && ( - + )} ); - }, [filterOptions, showMetricsTypesFilter]); + }, [dataTestSubj, filterOptions, showMetricsTypesFilter]); const onClickRefreshButton = useCallback(() => onClick(), [onClick]); @@ -68,6 +70,7 @@ export const ChartFilters = memo( onRefresh={onRefresh} onRefreshChange={onRefreshChange} onTimeChange={onTimeChange} + data-test-subj={dataTestSubj} /> diff --git a/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx index 4d9b280d763ce..044a036eea61f 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx @@ -15,6 +15,7 @@ import type { OnRefreshChangeProps, } from '@elastic/eui/src/components/date_picker/types'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; export interface DateRangePickerValues { autoRefreshOptions: { @@ -32,10 +33,19 @@ interface UsageMetricsDateRangePickerProps { onRefresh: () => void; onRefreshChange: (evt: OnRefreshChangeProps) => void; onTimeChange: ({ start, end }: DurationRange) => void; + 'data-test-subj'?: string; } export const UsageMetricsDateRangePicker = memo( - ({ dateRangePickerState, isDataLoading, onRefresh, onRefreshChange, onTimeChange }) => { + ({ + dateRangePickerState, + isDataLoading, + onRefresh, + onRefreshChange, + onTimeChange, + 'data-test-subj': dataTestSubj, + }) => { + const getTestId = useTestIdGenerator(dataTestSubj); const kibana = useKibana(); const { uiSettings } = kibana.services; const [commonlyUsedRanges] = useState(() => { @@ -54,6 +64,7 @@ export const UsageMetricsDateRangePicker = memo { + const getMetricTypesAsArray = (): MetricTypes[] => { + return [...METRIC_TYPE_VALUES]; + }; + + it('should not use invalid `metricTypes` values from URL params', () => { + expect(getDataUsageMetricsFiltersFromUrlParams({ metricTypes: 'bar,foo' })).toEqual({}); + }); + + it('should use valid `metricTypes` values from URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + metricTypes: `${getMetricTypesAsArray().join()},foo,bar`, + }) + ).toEqual({ + metricTypes: getMetricTypesAsArray().sort(), + }); + }); + + it('should use given `dataStreams` values from URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + dataStreams: 'ds-3,ds-1,ds-2', + }) + ).toEqual({ + dataStreams: ['ds-3', 'ds-1', 'ds-2'], + }); + }); + + it('should use valid `metricTypes` along with given `dataStreams` and date values from URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + metricTypes: getMetricTypesAsArray().join(), + dataStreams: 'ds-5,ds-1,ds-2', + startDate: '2022-09-12T08:00:00.000Z', + endDate: '2022-09-12T08:30:33.140Z', + }) + ).toEqual({ + metricTypes: getMetricTypesAsArray().sort(), + endDate: '2022-09-12T08:30:33.140Z', + dataStreams: ['ds-5', 'ds-1', 'ds-2'], + startDate: '2022-09-12T08:00:00.000Z', + }); + }); + + it('should use given relative startDate and endDate values URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + startDate: 'now-24h/h', + endDate: 'now', + }) + ).toEqual({ + endDate: 'now', + startDate: 'now-24h/h', + }); + }); + + it('should use given absolute startDate and endDate values URL params', () => { + expect( + getDataUsageMetricsFiltersFromUrlParams({ + startDate: '2022-09-12T08:00:00.000Z', + endDate: '2022-09-12T08:30:33.140Z', + }) + ).toEqual({ + endDate: '2022-09-12T08:30:33.140Z', + startDate: '2022-09-12T08:00:00.000Z', + }); + }); +}); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx new file mode 100644 index 0000000000000..04cee589a523d --- /dev/null +++ b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode } from 'react'; +import { QueryClient, QueryClientProvider, useQuery as _useQuery } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; +import { useGetDataUsageDataStreams } from './use_get_data_streams'; +import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../common'; +import { coreMock as mockCore } from '@kbn/core/public/mocks'; +import { dataUsageTestQueryClientOptions } from '../../common/test_utils/test_query_client_options'; + +const useQueryMock = _useQuery as jest.Mock; + +jest.mock('@tanstack/react-query', () => { + const actualReactQueryModule = jest.requireActual('@tanstack/react-query'); + + return { + ...actualReactQueryModule, + useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)), + }; +}); + +const mockServices = mockCore.createStart(); +const createWrapper = () => { + const queryClient = new QueryClient(dataUsageTestQueryClientOptions); + return ({ children }: { children: ReactNode }) => ( + {children} + ); +}; + +jest.mock('../utils/use_kibana', () => { + return { + useKibanaContextForPlugin: () => ({ + services: mockServices, + }), + }; +}); + +const defaultDataStreamsRequestParams = { + options: { enabled: true }, +}; + +describe('useGetDataUsageDataStreams', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call the correct API', async () => { + await renderHook(() => useGetDataUsageDataStreams(defaultDataStreamsRequestParams), { + wrapper: createWrapper(), + }); + + expect(mockServices.http.get).toHaveBeenCalledWith(DATA_USAGE_DATA_STREAMS_API_ROUTE, { + signal: expect.any(AbortSignal), + version: '1', + }); + }); + + it('should not send selected data stream names provided in the param when calling the API', async () => { + await renderHook( + () => + useGetDataUsageDataStreams({ + ...defaultDataStreamsRequestParams, + selectedDataStreams: ['ds-1'], + }), + { + wrapper: createWrapper(), + } + ); + + expect(mockServices.http.get).toHaveBeenCalledWith(DATA_USAGE_DATA_STREAMS_API_ROUTE, { + signal: expect.any(AbortSignal), + version: '1', + }); + }); + + it('should not call the API if disabled', async () => { + await renderHook( + () => + useGetDataUsageDataStreams({ + ...defaultDataStreamsRequestParams, + options: { enabled: false }, + }), + { + wrapper: createWrapper(), + } + ); + + expect(mockServices.http.get).not.toHaveBeenCalled(); + }); + + it('should allow custom options to be used', async () => { + await renderHook( + () => + useGetDataUsageDataStreams({ + selectedDataStreams: undefined, + options: { + queryKey: ['test-query-key'], + enabled: true, + retry: false, + }, + }), + { + wrapper: createWrapper(), + } + ); + + expect(useQueryMock).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ['test-query-key'], + enabled: true, + retry: false, + }) + ); + }); +}); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts index 598acca3c1faf..acb41e45f4eb6 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts @@ -31,15 +31,16 @@ export const useGetDataUsageDataStreams = ({ selectedDataStreams?: string[]; options?: UseQueryOptions; }): UseQueryResult => { - const http = useKibanaContextForPlugin().services.http; + const { http } = useKibanaContextForPlugin().services; return useQuery({ queryKey: ['get-data-usage-data-streams'], ...options, keepPreviousData: true, - queryFn: async () => { + queryFn: async ({ signal }) => { const dataStreamsResponse = await http .get(DATA_USAGE_DATA_STREAMS_API_ROUTE, { + signal, version: '1', }) .catch((error) => { diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx new file mode 100644 index 0000000000000..efc3d2a9f4640 --- /dev/null +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.test.tsx @@ -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 React, { ReactNode } from 'react'; +import { QueryClient, QueryClientProvider, useQuery as _useQuery } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; +import { useGetDataUsageMetrics } from './use_get_usage_metrics'; +import { DATA_USAGE_METRICS_API_ROUTE } from '../../common'; +import { coreMock as mockCore } from '@kbn/core/public/mocks'; +import { dataUsageTestQueryClientOptions } from '../../common/test_utils/test_query_client_options'; + +const useQueryMock = _useQuery as jest.Mock; + +jest.mock('@tanstack/react-query', () => { + const actualReactQueryModule = jest.requireActual('@tanstack/react-query'); + + return { + ...actualReactQueryModule, + useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)), + }; +}); + +const mockServices = mockCore.createStart(); +const createWrapper = () => { + const queryClient = new QueryClient(dataUsageTestQueryClientOptions); + return ({ children }: { children: ReactNode }) => ( + {children} + ); +}; + +jest.mock('../utils/use_kibana', () => { + return { + useKibanaContextForPlugin: () => ({ + services: mockServices, + }), + }; +}); + +const defaultUsageMetricsRequestBody = { + from: 'now-15m', + to: 'now', + metricTypes: ['ingest_rate'], + dataStreams: ['ds-1'], +}; + +describe('useGetDataUsageMetrics', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call the correct API', async () => { + await renderHook( + () => useGetDataUsageMetrics(defaultUsageMetricsRequestBody, { enabled: true }), + { + wrapper: createWrapper(), + } + ); + + expect(mockServices.http.post).toHaveBeenCalledWith(DATA_USAGE_METRICS_API_ROUTE, { + signal: expect.any(AbortSignal), + version: '1', + body: JSON.stringify(defaultUsageMetricsRequestBody), + }); + }); + + it('should not call the API if disabled', async () => { + await renderHook( + () => useGetDataUsageMetrics(defaultUsageMetricsRequestBody, { enabled: false }), + { + wrapper: createWrapper(), + } + ); + + expect(mockServices.http.post).not.toHaveBeenCalled(); + }); + + it('should allow custom options to be used', async () => { + await renderHook( + () => + useGetDataUsageMetrics(defaultUsageMetricsRequestBody, { + queryKey: ['test-query-key'], + enabled: true, + retry: false, + }), + { + wrapper: createWrapper(), + } + ); + + expect(useQueryMock).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ['test-query-key'], + enabled: true, + retry: false, + }) + ); + }); +}); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts index 7e7406d72b9c0..6b2ef5316b0f6 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts @@ -21,7 +21,7 @@ export const useGetDataUsageMetrics = ( body: UsageMetricsRequestBody, options: UseQueryOptions> = {} ): UseQueryResult> => { - const http = useKibanaContextForPlugin().services.http; + const { http } = useKibanaContextForPlugin().services; return useQuery>({ queryKey: ['get-data-usage-metrics', body], diff --git a/x-pack/plugins/data_usage/public/utils/format_bytes.test.ts b/x-pack/plugins/data_usage/public/utils/format_bytes.test.ts new file mode 100644 index 0000000000000..ccc7a4c2f0aa2 --- /dev/null +++ b/x-pack/plugins/data_usage/public/utils/format_bytes.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { formatBytes } from './format_bytes'; + +const exponentN = (number: number, exponent: number) => number ** exponent; + +describe('formatBytes', () => { + it('should format bytes to human readable format with decimal', () => { + expect(formatBytes(84 + 5)).toBe('89.0 B'); + expect(formatBytes(1024 + 256)).toBe('1.3 KB'); + expect(formatBytes(1024 + 582)).toBe('1.6 KB'); + expect(formatBytes(exponentN(1024, 2) + 582 * 1024)).toBe('1.6 MB'); + expect(formatBytes(exponentN(1024, 3) + 582 * exponentN(1024, 2))).toBe('1.6 GB'); + expect(formatBytes(exponentN(1024, 4) + 582 * exponentN(1024, 3))).toBe('1.6 TB'); + expect(formatBytes(exponentN(1024, 5) + 582 * exponentN(1024, 4))).toBe('1.6 PB'); + expect(formatBytes(exponentN(1024, 6) + 582 * exponentN(1024, 5))).toBe('1.6 EB'); + expect(formatBytes(exponentN(1024, 7) + 582 * exponentN(1024, 6))).toBe('1.6 ZB'); + expect(formatBytes(exponentN(1024, 8) + 582 * exponentN(1024, 7))).toBe('1.6 YB'); + }); +}); diff --git a/x-pack/plugins/data_usage/server/mocks/index.ts b/x-pack/plugins/data_usage/server/mocks/index.ts new file mode 100644 index 0000000000000..54260f7309fc6 --- /dev/null +++ b/x-pack/plugins/data_usage/server/mocks/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { DeepReadonly } from 'utility-types'; +import { PluginInitializerContext } from '@kbn/core/server'; +import { Observable } from 'rxjs'; +import { DataUsageContext } from '../types'; +import { DataUsageConfigType } from '../config'; + +export interface MockedDataUsageContext extends DataUsageContext { + logFactory: ReturnType['get']>; + config$?: Observable; + configInitialValue: DataUsageConfigType; + serverConfig: DeepReadonly; + kibanaInstanceId: PluginInitializerContext['env']['instanceUuid']; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + kibanaBranch: PluginInitializerContext['env']['packageInfo']['branch']; +} + +export const createMockedDataUsageContext = ( + context: PluginInitializerContext +): MockedDataUsageContext => { + return { + logFactory: loggingSystemMock.create().get(), + config$: context.config.create(), + configInitialValue: context.config.get(), + serverConfig: context.config.get(), + kibanaInstanceId: context.env.instanceUuid, + kibanaVersion: context.env.packageInfo.version, + kibanaBranch: context.env.packageInfo.branch, + }; +}; diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts new file mode 100644 index 0000000000000..7282dbc969fc7 --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { MockedKeys } from '@kbn/utility-types-jest'; +import type { CoreSetup } from '@kbn/core/server'; +import { registerDataStreamsRoute } from './data_streams'; +import { coreMock } from '@kbn/core/server/mocks'; +import { httpServerMock } from '@kbn/core/server/mocks'; +import { DataUsageService } from '../../services'; +import type { + DataUsageRequestHandlerContext, + DataUsageRouter, + DataUsageServerStart, +} from '../../types'; +import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../../common'; +import { createMockedDataUsageContext } from '../../mocks'; +import { getMeteringStats } from '../../utils/get_metering_stats'; +import { CustomHttpRequestError } from '../../utils'; + +jest.mock('../../utils/get_metering_stats'); +const mockGetMeteringStats = getMeteringStats as jest.Mock; + +describe('registerDataStreamsRoute', () => { + let mockCore: MockedKeys>; + let router: DataUsageRouter; + let dataUsageService: DataUsageService; + let context: DataUsageRequestHandlerContext; + + beforeEach(() => { + mockCore = coreMock.createSetup(); + router = mockCore.http.createRouter(); + context = coreMock.createCustomRequestHandlerContext( + coreMock.createRequestHandlerContext() + ) as unknown as DataUsageRequestHandlerContext; + + const mockedDataUsageContext = createMockedDataUsageContext( + coreMock.createPluginInitializerContext() + ); + dataUsageService = new DataUsageService(mockedDataUsageContext); + registerDataStreamsRoute(router, dataUsageService); + }); + + it('should request correct API', () => { + expect(router.versioned.get).toHaveBeenCalledTimes(1); + expect(router.versioned.get).toHaveBeenCalledWith({ + access: 'internal', + path: DATA_USAGE_DATA_STREAMS_API_ROUTE, + }); + }); + + it('should correctly sort response', async () => { + mockGetMeteringStats.mockResolvedValue({ + datastreams: [ + { + name: 'datastream1', + size_in_bytes: 100, + }, + { + name: 'datastream2', + size_in_bytes: 200, + }, + ], + }); + const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: [ + { + name: 'datastream2', + storageSizeBytes: 200, + }, + { + name: 'datastream1', + storageSizeBytes: 100, + }, + ], + }); + }); + + it('should return correct error if metering stats request fails', async () => { + // using custom error for test here to avoid having to import the actual error class + mockGetMeteringStats.mockRejectedValue( + new CustomHttpRequestError('Error getting metring stats!') + ); + const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.customError).toHaveBeenCalledTimes(1); + expect(mockResponse.customError).toHaveBeenCalledWith({ + body: new CustomHttpRequestError('Error getting metring stats!'), + statusCode: 500, + }); + }); + + it.each([ + ['no datastreams', {}, []], + ['empty array', { datastreams: [] }, []], + ['an empty element', { datastreams: [{}] }, [{ name: undefined, storageSizeBytes: 0 }]], + ])('should return empty array when no stats data with %s', async (_, stats, res) => { + mockGetMeteringStats.mockResolvedValue(stats); + const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: res, + }); + }); +}); diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts index bc8c5e898c35e..66c2cc0df3513 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts @@ -5,27 +5,11 @@ * 2.0. */ -import { type ElasticsearchClient, RequestHandler } from '@kbn/core/server'; +import { RequestHandler } from '@kbn/core/server'; import { DataUsageRequestHandlerContext } from '../../types'; import { errorHandler } from '../error_handler'; import { DataUsageService } from '../../services'; - -export interface MeteringStats { - name: string; - num_docs: number; - size_in_bytes: number; -} - -interface MeteringStatsResponse { - datastreams: MeteringStats[]; -} - -const getMeteringStats = (client: ElasticsearchClient) => { - return client.transport.request({ - method: 'GET', - path: '/_metering/stats', - }); -}; +import { getMeteringStats } from '../../utils/get_metering_stats'; export const getDataStreamsHandler = ( dataUsageService: DataUsageService @@ -41,12 +25,15 @@ export const getDataStreamsHandler = ( core.elasticsearch.client.asSecondaryAuthUser ); - const body = meteringStats - .sort((a, b) => b.size_in_bytes - a.size_in_bytes) - .map((stat) => ({ - name: stat.name, - storageSizeBytes: stat.size_in_bytes ?? 0, - })); + const body = + meteringStats && !!meteringStats.length + ? meteringStats + .sort((a, b) => b.size_in_bytes - a.size_in_bytes) + .map((stat) => ({ + name: stat.name, + storageSizeBytes: stat.size_in_bytes ?? 0, + })) + : []; return response.ok({ body, diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts new file mode 100644 index 0000000000000..e95ffd11807a9 --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MockedKeys } from '@kbn/utility-types-jest'; +import type { CoreSetup } from '@kbn/core/server'; +import { registerUsageMetricsRoute } from './usage_metrics'; +import { coreMock } from '@kbn/core/server/mocks'; +import { httpServerMock } from '@kbn/core/server/mocks'; +import { DataUsageService } from '../../services'; +import type { + DataUsageRequestHandlerContext, + DataUsageRouter, + DataUsageServerStart, +} from '../../types'; +import { DATA_USAGE_METRICS_API_ROUTE } from '../../../common'; +import { createMockedDataUsageContext } from '../../mocks'; +import { CustomHttpRequestError } from '../../utils'; +import { AutoOpsError } from '../../services/errors'; + +describe('registerUsageMetricsRoute', () => { + let mockCore: MockedKeys>; + let router: DataUsageRouter; + let dataUsageService: DataUsageService; + let context: DataUsageRequestHandlerContext; + + beforeEach(() => { + mockCore = coreMock.createSetup(); + router = mockCore.http.createRouter(); + context = coreMock.createCustomRequestHandlerContext( + coreMock.createRequestHandlerContext() + ) as unknown as DataUsageRequestHandlerContext; + + const mockedDataUsageContext = createMockedDataUsageContext( + coreMock.createPluginInitializerContext() + ); + dataUsageService = new DataUsageService(mockedDataUsageContext); + }); + + it('should request correct API', () => { + registerUsageMetricsRoute(router, dataUsageService); + + expect(router.versioned.post).toHaveBeenCalledTimes(1); + expect(router.versioned.post).toHaveBeenCalledWith({ + access: 'internal', + path: DATA_USAGE_METRICS_API_ROUTE, + }); + }); + + it('should throw error if no data streams in the request', async () => { + registerUsageMetricsRoute(router, dataUsageService); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + from: 'now-15m', + to: 'now', + metricTypes: ['ingest_rate'], + dataStreams: [], + }, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.customError).toHaveBeenCalledTimes(1); + expect(mockResponse.customError).toHaveBeenCalledWith({ + body: new CustomHttpRequestError('[request body.dataStreams]: no data streams selected'), + statusCode: 400, + }); + }); + + it('should correctly transform response', async () => { + (await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest + .fn() + .mockResolvedValue({ + data_streams: [{ name: '.ds-1' }, { name: '.ds-2' }], + }); + + dataUsageService.getMetrics = jest.fn().mockResolvedValue({ + metrics: { + ingest_rate: [ + { + name: '.ds-1', + data: [ + [1726858530000, 13756849], + [1726862130000, 14657904], + ], + }, + { + name: '.ds-2', + data: [ + [1726858530000, 12894623], + [1726862130000, 14436905], + ], + }, + ], + storage_retained: [ + { + name: '.ds-1', + data: [ + [1726858530000, 12576413], + [1726862130000, 13956423], + ], + }, + { + name: '.ds-2', + data: [ + [1726858530000, 12894623], + [1726862130000, 14436905], + ], + }, + ], + }, + }); + + registerUsageMetricsRoute(router, dataUsageService); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + from: 'now-15m', + to: 'now', + metricTypes: ['ingest_rate', 'storage_retained'], + dataStreams: ['.ds-1', '.ds-2'], + }, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: { + metrics: { + ingest_rate: [ + { + name: '.ds-1', + data: [ + { x: 1726858530000, y: 13756849 }, + { x: 1726862130000, y: 14657904 }, + ], + }, + { + name: '.ds-2', + data: [ + { x: 1726858530000, y: 12894623 }, + { x: 1726862130000, y: 14436905 }, + ], + }, + ], + storage_retained: [ + { + name: '.ds-1', + data: [ + { x: 1726858530000, y: 12576413 }, + { x: 1726862130000, y: 13956423 }, + ], + }, + { + name: '.ds-2', + data: [ + { x: 1726858530000, y: 12894623 }, + { x: 1726862130000, y: 14436905 }, + ], + }, + ], + }, + }, + }); + }); + + it('should throw error if error on requesting auto ops service', async () => { + (await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest + .fn() + .mockResolvedValue({ + data_streams: [{ name: '.ds-1' }, { name: '.ds-2' }], + }); + + dataUsageService.getMetrics = jest + .fn() + .mockRejectedValue(new AutoOpsError('Uh oh, something went wrong!')); + + registerUsageMetricsRoute(router, dataUsageService); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + from: 'now-15m', + to: 'now', + metricTypes: ['ingest_rate'], + dataStreams: ['.ds-1', '.ds-2'], + }, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRouter = mockCore.http.createRouter.mock.results[0].value; + const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls; + await handler(context, mockRequest, mockResponse); + + expect(mockResponse.customError).toHaveBeenCalledTimes(1); + expect(mockResponse.customError).toHaveBeenCalledWith({ + body: new AutoOpsError('Uh oh, something went wrong!'), + statusCode: 503, + }); + }); +}); diff --git a/x-pack/plugins/data_usage/server/utils/get_metering_stats.ts b/x-pack/plugins/data_usage/server/utils/get_metering_stats.ts new file mode 100644 index 0000000000000..4ba30f5bd3601 --- /dev/null +++ b/x-pack/plugins/data_usage/server/utils/get_metering_stats.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { type ElasticsearchClient } from '@kbn/core/server'; + +export interface MeteringStats { + name: string; + num_docs: number; + size_in_bytes: number; +} + +interface MeteringStatsResponse { + datastreams: MeteringStats[]; +} + +export const getMeteringStats = (client: ElasticsearchClient) => { + return client.transport.request({ + method: 'GET', + path: '/_metering/stats', + }); +}; diff --git a/x-pack/plugins/data_usage/tsconfig.json b/x-pack/plugins/data_usage/tsconfig.json index 78c501922f239..66c8a5247858b 100644 --- a/x-pack/plugins/data_usage/tsconfig.json +++ b/x-pack/plugins/data_usage/tsconfig.json @@ -31,6 +31,7 @@ "@kbn/repo-info", "@kbn/cloud-plugin", "@kbn/server-http-tools", + "@kbn/utility-types-jest", ], "exclude": ["target/**/*"] } From f3bb8cec6e2240a9258a648153eb579061dc5a5b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:53:18 +1100 Subject: [PATCH 45/47] [8.x] Unauthorized route migration for routes owned by kibana-security (#198334) (#199382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [Unauthorized route migration for routes owned by kibana-security (#198334)](https://github.com/elastic/kibana/pull/198334) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../server/routes/configure.ts | 7 ++++ .../interactive_setup/server/routes/enroll.ts | 7 ++++ .../interactive_setup/server/routes/ping.ts | 7 ++++ .../interactive_setup/server/routes/status.ts | 7 ++++ .../interactive_setup/server/routes/verify.ts | 7 ++++ .../routes/analytics/authentication_type.ts | 7 ++++ .../routes/analytics/record_violations.ts | 7 ++++ .../anonymous_access/get_capabilities.ts | 12 +++++- .../routes/anonymous_access/get_state.ts | 11 +++++- .../security/server/routes/api_keys/create.ts | 7 ++++ .../server/routes/api_keys/enabled.ts | 7 ++++ .../server/routes/api_keys/has_active.ts | 6 +++ .../server/routes/api_keys/invalidate.ts | 6 +++ .../security/server/routes/api_keys/query.ts | 6 +++ .../security/server/routes/api_keys/update.ts | 6 +++ .../server/routes/authentication/common.ts | 37 +++++++++++++++++-- .../server/routes/authentication/oidc.ts | 18 +++++++++ .../server/routes/authentication/saml.ts | 6 +++ .../routes/authorization/privileges/get.ts | 7 ++++ .../authorization/privileges/get_builtin.ts | 11 +++++- .../routes/authorization/roles/delete.ts | 6 +++ .../server/routes/authorization/roles/get.ts | 6 +++ .../routes/authorization/roles/get_all.ts | 6 +++ .../server/routes/authorization/roles/post.ts | 6 +++ .../server/routes/authorization/roles/put.ts | 6 +++ .../spaces/share_saved_object_permissions.ts | 6 +++ .../routes/deprecations/kibana_user_role.ts | 12 ++++++ .../routes/feature_check/feature_check.ts | 6 +++ .../server/routes/indices/get_fields.ts | 6 +++ .../server/routes/role_mapping/delete.ts | 6 +++ .../server/routes/role_mapping/get.ts | 6 +++ .../server/routes/role_mapping/post.ts | 6 +++ .../routes/security_checkup/get_state.ts | 11 +++++- .../routes/session_management/extend.ts | 7 ++++ .../server/routes/session_management/info.ts | 12 +++++- .../server/routes/user_profile/get_current.ts | 7 ++++ .../server/routes/user_profile/update.ts | 7 ++++ .../server/routes/users/change_password.ts | 6 +++ .../server/routes/users/create_or_update.ts | 6 +++ .../security/server/routes/users/delete.ts | 6 +++ .../security/server/routes/users/disable.ts | 6 +++ .../security/server/routes/users/enable.ts | 6 +++ .../security/server/routes/users/get.ts | 6 +++ .../security/server/routes/users/get_all.ts | 11 +++++- .../server/routes/views/access_agreement.ts | 12 +++++- .../security/server/routes/views/login.ts | 13 ++++++- .../on_post_auth_interceptor.test.ts | 18 +++++++-- .../on_request_interceptor.test.ts | 28 +++++++++++++- .../server/routes/api/external/delete.ts | 7 ++++ .../external/disable_legacy_url_aliases.ts | 7 ++++ .../spaces/server/routes/api/external/get.ts | 7 ++++ .../server/routes/api/external/get_all.ts | 7 ++++ .../api/external/get_shareable_references.ts | 7 ++++ .../spaces/server/routes/api/external/post.ts | 7 ++++ .../spaces/server/routes/api/external/put.ts | 7 ++++ .../api/external/update_objects_spaces.ts | 7 ++++ .../routes/api/internal/get_active_space.ts | 7 ++++ .../routes/api/internal/set_solution_space.ts | 7 ++++ 58 files changed, 483 insertions(+), 16 deletions(-) diff --git a/src/plugins/interactive_setup/server/routes/configure.ts b/src/plugins/interactive_setup/server/routes/configure.ts index 1cdaf588a6cd9..bb5a85800e03b 100644 --- a/src/plugins/interactive_setup/server/routes/configure.ts +++ b/src/plugins/interactive_setup/server/routes/configure.ts @@ -37,6 +37,13 @@ export function defineConfigureRoute({ router.post( { path: '/internal/interactive_setup/configure', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: { body: schema.object({ host: schema.uri({ scheme: ['http', 'https'] }), diff --git a/src/plugins/interactive_setup/server/routes/enroll.ts b/src/plugins/interactive_setup/server/routes/enroll.ts index 1cd0362d2790b..7ee97db592ac5 100644 --- a/src/plugins/interactive_setup/server/routes/enroll.ts +++ b/src/plugins/interactive_setup/server/routes/enroll.ts @@ -40,6 +40,13 @@ export function defineEnrollRoutes({ router.post( { path: '/internal/interactive_setup/enroll', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: { body: schema.object({ hosts: schema.arrayOf(schema.uri({ scheme: 'https' }), { diff --git a/src/plugins/interactive_setup/server/routes/ping.ts b/src/plugins/interactive_setup/server/routes/ping.ts index 4deaeee675404..4c71d9f05bd1b 100644 --- a/src/plugins/interactive_setup/server/routes/ping.ts +++ b/src/plugins/interactive_setup/server/routes/ping.ts @@ -17,6 +17,13 @@ export function definePingRoute({ router, logger, elasticsearch, preboot }: Rout router.post( { path: '/internal/interactive_setup/ping', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: { body: schema.object({ host: schema.uri({ scheme: ['http', 'https'] }), diff --git a/src/plugins/interactive_setup/server/routes/status.ts b/src/plugins/interactive_setup/server/routes/status.ts index 78a97ac862317..14c94411ded53 100644 --- a/src/plugins/interactive_setup/server/routes/status.ts +++ b/src/plugins/interactive_setup/server/routes/status.ts @@ -15,6 +15,13 @@ export function defineStatusRoute({ router, elasticsearch, preboot }: RouteDefin router.get( { path: '/internal/interactive_setup/status', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: false, options: { authRequired: false }, }, diff --git a/src/plugins/interactive_setup/server/routes/verify.ts b/src/plugins/interactive_setup/server/routes/verify.ts index a40e35794fb9e..7fb5bb2e70c18 100644 --- a/src/plugins/interactive_setup/server/routes/verify.ts +++ b/src/plugins/interactive_setup/server/routes/verify.ts @@ -15,6 +15,13 @@ export function defineVerifyRoute({ router, verificationCode }: RouteDefinitionP router.post( { path: '/internal/interactive_setup/verify', + security: { + authz: { + enabled: false, + reason: + 'Interactive setup is strictly a "pre-boot" feature which cannot leverage conventional authorization.', + }, + }, validate: { body: schema.object({ code: schema.string(), diff --git a/x-pack/plugins/security/server/routes/analytics/authentication_type.ts b/x-pack/plugins/security/server/routes/analytics/authentication_type.ts index f2bf76c71b1ab..92094a65da7bb 100644 --- a/x-pack/plugins/security/server/routes/analytics/authentication_type.ts +++ b/x-pack/plugins/security/server/routes/analytics/authentication_type.ts @@ -31,6 +31,13 @@ export function defineRecordAnalyticsOnAuthTypeRoutes({ router.post( { path: '/internal/security/analytics/_record_auth_type', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the scoped ES cluster client of the internal authentication service', + }, + }, validate: { body: schema.nullable( schema.object({ signature: schema.string(), timestamp: schema.number() }) diff --git a/x-pack/plugins/security/server/routes/analytics/record_violations.ts b/x-pack/plugins/security/server/routes/analytics/record_violations.ts index 826a304f1656e..bec224a6d3eeb 100644 --- a/x-pack/plugins/security/server/routes/analytics/record_violations.ts +++ b/x-pack/plugins/security/server/routes/analytics/record_violations.ts @@ -135,6 +135,13 @@ export function defineRecordViolations({ router, analyticsService }: RouteDefini router.post( { path: '/internal/security/analytics/_record_violations', + security: { + authz: { + enabled: false, + reason: + 'This route is used by browsers to report CSP and Permission Policy violations. These requests are sent without authentication per the browser spec.', + }, + }, validate: { /** * Chrome supports CSP3 spec and sends an array of reports. Safari only sends a single diff --git a/x-pack/plugins/security/server/routes/anonymous_access/get_capabilities.ts b/x-pack/plugins/security/server/routes/anonymous_access/get_capabilities.ts index 220fb1515df46..84c8ed17e5963 100644 --- a/x-pack/plugins/security/server/routes/anonymous_access/get_capabilities.ts +++ b/x-pack/plugins/security/server/routes/anonymous_access/get_capabilities.ts @@ -15,7 +15,17 @@ export function defineAnonymousAccessGetCapabilitiesRoutes({ getAnonymousAccessService, }: RouteDefinitionParams) { router.get( - { path: '/internal/security/anonymous_access/capabilities', validate: false }, + { + path: '/internal/security/anonymous_access/capabilities', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the scoped ES cluster client of the anonymous access service', + }, + }, + validate: false, + }, async (_context, request, response) => { const anonymousAccessService = getAnonymousAccessService(); return response.ok({ body: await anonymousAccessService.getCapabilities(request) }); diff --git a/x-pack/plugins/security/server/routes/anonymous_access/get_state.ts b/x-pack/plugins/security/server/routes/anonymous_access/get_state.ts index 28745c80a5f44..8911588b72109 100644 --- a/x-pack/plugins/security/server/routes/anonymous_access/get_state.ts +++ b/x-pack/plugins/security/server/routes/anonymous_access/get_state.ts @@ -18,7 +18,16 @@ export function defineAnonymousAccessGetStateRoutes({ getAnonymousAccessService, }: RouteDefinitionParams) { router.get( - { path: '/internal/security/anonymous_access/state', validate: false }, + { + path: '/internal/security/anonymous_access/state', + security: { + authz: { + enabled: false, + reason: 'This route is used for anonymous access', + }, + }, + validate: false, + }, async (_context, _request, response) => { const anonymousAccessService = getAnonymousAccessService(); const accessURLParameters = anonymousAccessService.accessURLParameters diff --git a/x-pack/plugins/security/server/routes/api_keys/create.ts b/x-pack/plugins/security/server/routes/api_keys/create.ts index 59d743e3726aa..963e6c7ced35b 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.ts @@ -32,6 +32,13 @@ export function defineCreateApiKeyRoutes({ router.post( { path: '/internal/security/api_key', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the scoped ES cluster client of the internal authentication service', + }, + }, validate: { body: schema.oneOf([ restApiKeySchema, diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.ts index c94c8af61e24f..dd06c93a71e88 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.ts @@ -16,6 +16,13 @@ export function defineEnabledApiKeysRoutes({ router.get( { path: '/internal/security/api_key/_enabled', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the scoped ES cluster client of the internal authentication service', + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/api_keys/has_active.ts b/x-pack/plugins/security/server/routes/api_keys/has_active.ts index bf432b1861045..b1cc220f802b1 100644 --- a/x-pack/plugins/security/server/routes/api_keys/has_active.ts +++ b/x-pack/plugins/security/server/routes/api_keys/has_active.ts @@ -22,6 +22,12 @@ export function defineHasApiKeysRoutes({ router.get( { path: '/internal/security/api_key/_has_active', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the scoped ES cluster client of the internal authentication service, and to Core's ES client`, + }, + }, validate: false, options: { access: 'internal', diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts index 1983dbf2344e0..f2d72185d0b1c 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts @@ -21,6 +21,12 @@ export function defineInvalidateApiKeysRoutes({ router }: RouteDefinitionParams) router.post( { path: '/internal/security/api_key/invalidate', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's ES client`, + }, + }, validate: { body: schema.object({ apiKeys: schema.arrayOf(schema.object({ id: schema.string(), name: schema.string() })), diff --git a/x-pack/plugins/security/server/routes/api_keys/query.ts b/x-pack/plugins/security/server/routes/api_keys/query.ts index 9fe8fdbdc734b..382d3a290aa7e 100644 --- a/x-pack/plugins/security/server/routes/api_keys/query.ts +++ b/x-pack/plugins/security/server/routes/api_keys/query.ts @@ -25,6 +25,12 @@ export function defineQueryApiKeysAndAggregationsRoute({ // on behalf of the user making the request and governed by the user's own cluster privileges. { path: '/internal/security/api_key/_query', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the scoped ES cluster client of the internal authentication service, and to Core's ES client`, + }, + }, validate: { body: schema.object({ query: schema.maybe(schema.object({}, { unknowns: 'allow' })), diff --git a/x-pack/plugins/security/server/routes/api_keys/update.ts b/x-pack/plugins/security/server/routes/api_keys/update.ts index a7fe43c46e206..364a0af0b95ad 100644 --- a/x-pack/plugins/security/server/routes/api_keys/update.ts +++ b/x-pack/plugins/security/server/routes/api_keys/update.ts @@ -34,6 +34,12 @@ export function defineUpdateApiKeyRoutes({ router.put( { path: '/internal/security/api_key', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the scoped ES cluster client of the internal authentication service`, + }, + }, validate: { body: schema.oneOf([ updateRestApiKeySchema, diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index b519171fd4fe6..0c91a6c7f3858 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -43,6 +43,12 @@ export function defineCommonRoutes({ router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party IdPs', + }, + }, // Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any // set of query string parameters (e.g. SAML/OIDC logout request/response parameters). validate: { query: schema.object({}, { unknowns: 'allow' }) }, @@ -92,7 +98,17 @@ export function defineCommonRoutes({ ]) { const deprecated = path === '/api/security/v1/me'; router.get( - { path, validate: false, options: { access: deprecated ? 'public' : 'internal' } }, + { + path, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's security service; there must be an authenticated user for this route to return information`, + }, + }, + validate: false, + options: { access: deprecated ? 'public' : 'internal' }, + }, createLicensedRouteHandler(async (context, request, response) => { if (deprecated) { logger.warn( @@ -135,10 +151,16 @@ export function defineCommonRoutes({ } // Register the login route for serverless for the time being. Note: This route will move into the buildFlavor !== 'serverless' block below. See next line. - // ToDo: In the serverless environment, we do not support API login - the only valid authentication methodology (or maybe just method or mechanism?) is SAML + // ToDo: In the serverless environment, we do not support API login - the only valid authentication type is SAML router.post( { path: '/internal/security/login', + security: { + authz: { + enabled: false, + reason: `This route provides basic and token login capbility, which is delegated to the internal authentication service`, + }, + }, validate: { body: schema.object({ providerType: schema.string(), @@ -183,7 +205,16 @@ export function defineCommonRoutes({ if (buildFlavor !== 'serverless') { // In the serverless offering, the access agreement functionality isn't available. router.post( - { path: '/internal/security/access_agreement/acknowledge', validate: false }, + { + path: '/internal/security/access_agreement/acknowledge', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the internal authentication service; there must be an authenticated user for this route to function`, + }, + }, + validate: false, + }, createLicensedRouteHandler(async (context, request, response) => { // If license doesn't allow access agreement we shouldn't handle request. if (!license.getFeatures().allowAccessAgreement) { diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index 69c3ce1700671..bb1ed6959e690 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -87,6 +87,12 @@ export function defineOIDCRoutes({ router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party OIDC providers', + }, + }, validate: { query: schema.object( { @@ -176,6 +182,12 @@ export function defineOIDCRoutes({ router.post( { path, + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party OIDC providers', + }, + }, validate: { body: schema.object( { @@ -221,6 +233,12 @@ export function defineOIDCRoutes({ router.get( { path: '/api/security/oidc/initiate_login', + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party OIDC providers', + }, + }, validate: { query: schema.object( { diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 3c72fd908e6c4..8cee1df2da88b 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -30,6 +30,12 @@ export function defineSAMLRoutes({ router.post( { path, + security: { + authz: { + enabled: false, + reason: 'This route must remain accessible to 3rd-party SAML providers', + }, + }, validate: { body: schema.object( { SAMLResponse: schema.string(), RelayState: schema.maybe(schema.string()) }, diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts index b7204faaa7ca4..23fb7ccd9bf39 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts @@ -14,6 +14,13 @@ export function defineGetPrivilegesRoutes({ router, authz }: RouteDefinitionPara router.get( { path: '/api/security/privileges', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it returns only the global list of Kibana privileges', + }, + }, validate: { query: schema.object({ // We don't use `schema.boolean` here, because all query string parameters are treated as diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts index 9a9c2dd6fcc71..1a35875de72e0 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts @@ -9,7 +9,16 @@ import type { RouteDefinitionParams } from '../..'; export function defineGetBuiltinPrivilegesRoutes({ router }: RouteDefinitionParams) { router.get( - { path: '/internal/security/esPrivileges/builtin', validate: false }, + { + path: '/internal/security/esPrivileges/builtin', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, + validate: false, + }, async (context, request, response) => { const esClient = (await context.core).elasticsearch.client; const privileges = await esClient.asCurrentUser.security.getBuiltinPrivileges(); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts index 07f314da4232b..b4ff278db219f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -25,6 +25,12 @@ export function defineDeleteRolesRoutes({ router }: RouteDefinitionParams) { .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { params: schema.object({ name: schema.string({ minLength: 1 }) }), diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index 63bfa38b76221..c819c5fc36753 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -32,6 +32,12 @@ export function defineGetRolesRoutes({ .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { params: schema.object({ diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index ef4a008ecb708..7d1442cb473ef 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -33,6 +33,12 @@ export function defineGetAllRolesRoutes({ .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { query: schema.maybe( diff --git a/x-pack/plugins/security/server/routes/authorization/roles/post.ts b/x-pack/plugins/security/server/routes/authorization/roles/post.ts index 949553e960c9b..4a41533e93a85 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/post.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/post.ts @@ -52,6 +52,12 @@ export function defineBulkCreateOrUpdateRolesRoutes({ .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { body: getBulkCreateOrUpdatePayloadSchema(() => { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index 268c84ff7420e..ce0b8222d412e 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -35,6 +35,12 @@ export function definePutRolesRoutes({ .addVersion( { version: API_VERSIONS.roles.public.v1, + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { request: { params: schema.object({ diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts index 536220eff03da..4c83455844a26 100644 --- a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts +++ b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts @@ -19,6 +19,12 @@ export function defineShareSavedObjectPermissionRoutes({ router.get( { path: '/internal/security/_share_saved_object_permissions', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the internal authorization service's checkPrivilegesWithRequest function`, + }, + }, validate: { query: schema.object({ type: schema.string() }) }, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts index 638a8f8a1bc7d..e465369ff0911 100644 --- a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts +++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts @@ -23,6 +23,12 @@ export function defineKibanaUserRoleDeprecationRoutes({ router, logger }: RouteD router.post( { path: '/internal/security/deprecations/kibana_user_role/_fix_users', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { @@ -88,6 +94,12 @@ export function defineKibanaUserRoleDeprecationRoutes({ router, logger }: RouteD router.post( { path: '/internal/security/deprecations/kibana_user_role/_fix_role_mappings', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/feature_check/feature_check.ts b/x-pack/plugins/security/server/routes/feature_check/feature_check.ts index b256ee77e55ff..6f4cd5b4b2654 100644 --- a/x-pack/plugins/security/server/routes/feature_check/feature_check.ts +++ b/x-pack/plugins/security/server/routes/feature_check/feature_check.ts @@ -43,6 +43,12 @@ export function defineSecurityFeatureCheckRoute({ router, logger }: RouteDefinit router.get( { path: '/internal/security/_check_security_features', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts index b0ec51339e080..4cfd6845e61bb 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -14,6 +14,12 @@ export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/fields/{query}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ query: schema.string() }) }, }, async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.ts index e305de6e4fcb4..8e331600ba490 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.ts @@ -15,6 +15,12 @@ export function defineRoleMappingDeleteRoutes({ router }: RouteDefinitionParams) router.delete( { path: '/internal/security/role_mapping/{name}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index ac6e7efaa8b0a..2b5ce017fbfb0 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -18,6 +18,12 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { router.get( { path: '/internal/security/role_mapping/{name?}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ name: schema.maybe(schema.string()), diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.ts b/x-pack/plugins/security/server/routes/role_mapping/post.ts index a9a87d4b2be51..e01dd446b6e51 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.ts @@ -15,6 +15,12 @@ export function defineRoleMappingPostRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/role_mapping/{name}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/security/server/routes/security_checkup/get_state.ts b/x-pack/plugins/security/server/routes/security_checkup/get_state.ts index 2946c3fa5dee3..40da0959c7418 100644 --- a/x-pack/plugins/security/server/routes/security_checkup/get_state.ts +++ b/x-pack/plugins/security/server/routes/security_checkup/get_state.ts @@ -29,7 +29,16 @@ export function defineSecurityCheckupGetStateRoutes({ const doesClusterHaveUserData = createClusterDataCheck(); router.get( - { path: '/internal/security/security_checkup/state', validate: false }, + { + path: '/internal/security/security_checkup/state', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, + validate: false, + }, async (context, _request, response) => { const esClient = (await context.core).elasticsearch.client; let displayAlert = false; diff --git a/x-pack/plugins/security/server/routes/session_management/extend.ts b/x-pack/plugins/security/server/routes/session_management/extend.ts index b1626ba4660b3..1180303d48aac 100644 --- a/x-pack/plugins/security/server/routes/session_management/extend.ts +++ b/x-pack/plugins/security/server/routes/session_management/extend.ts @@ -14,6 +14,13 @@ export function defineSessionExtendRoutes({ router, basePath }: RouteDefinitionP router.post( { path: '/internal/security/session', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it only redirects to the /internal/security/session endpoint', + }, + }, validate: false, }, async (_context, _request, response) => { diff --git a/x-pack/plugins/security/server/routes/session_management/info.ts b/x-pack/plugins/security/server/routes/session_management/info.ts index 75fae27e8cb12..c49cb7575399e 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.ts @@ -14,7 +14,17 @@ import type { SessionInfo } from '../../../common/types'; */ export function defineSessionInfoRoutes({ router, getSession }: RouteDefinitionParams) { router.get( - { path: '/internal/security/session', validate: false }, + { + path: '/internal/security/session', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because a valid session is required, and it does not return sensative session information', + }, + }, + validate: false, + }, async (_context, request, response) => { const { value: sessionValue } = await getSession().get(request); if (sessionValue) { diff --git a/x-pack/plugins/security/server/routes/user_profile/get_current.ts b/x-pack/plugins/security/server/routes/user_profile/get_current.ts index 9661570e36b4e..4621d543b49ca 100644 --- a/x-pack/plugins/security/server/routes/user_profile/get_current.ts +++ b/x-pack/plugins/security/server/routes/user_profile/get_current.ts @@ -20,6 +20,13 @@ export function defineGetCurrentUserProfileRoute({ router.get( { path: '/internal/security/user_profile', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the internal authorization service; a currently authenticated user is required', + }, + }, validate: { query: schema.object({ dataPath: schema.maybe(schema.string()) }), }, diff --git a/x-pack/plugins/security/server/routes/user_profile/update.ts b/x-pack/plugins/security/server/routes/user_profile/update.ts index 9a550ada52adc..a400d0db88b89 100644 --- a/x-pack/plugins/security/server/routes/user_profile/update.ts +++ b/x-pack/plugins/security/server/routes/user_profile/update.ts @@ -27,6 +27,13 @@ export function defineUpdateUserProfileDataRoute({ router.post( { path: '/internal/security/user_profile/_data', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the internal authorization service; an authenticated user and valid session are required', + }, + }, validate: { body: schema.recordOf(schema.string(), schema.any()), }, diff --git a/x-pack/plugins/security/server/routes/users/change_password.ts b/x-pack/plugins/security/server/routes/users/change_password.ts index bd71785ab9549..964d3d6fe888b 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.ts @@ -24,6 +24,12 @@ export function defineChangeUserPasswordRoutes({ router.post( { path: '/internal/security/users/{username}/password', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to the internal authorization service and the Security plugin's canUserChangePassword function`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), body: schema.object({ diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts index de6adad78b4e8..c6c0bcbc48415 100644 --- a/x-pack/plugins/security/server/routes/users/create_or_update.ts +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -15,6 +15,12 @@ export function defineCreateOrUpdateUserRoutes({ router }: RouteDefinitionParams router.post( { path: '/internal/security/users/{username}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), body: schema.object({ diff --git a/x-pack/plugins/security/server/routes/users/delete.ts b/x-pack/plugins/security/server/routes/users/delete.ts index 429adb368574a..39f838dff7d8c 100644 --- a/x-pack/plugins/security/server/routes/users/delete.ts +++ b/x-pack/plugins/security/server/routes/users/delete.ts @@ -15,6 +15,12 @@ export function defineDeleteUserRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/internal/security/users/{username}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), }, diff --git a/x-pack/plugins/security/server/routes/users/disable.ts b/x-pack/plugins/security/server/routes/users/disable.ts index 87f61daca8c95..f2984504922b3 100644 --- a/x-pack/plugins/security/server/routes/users/disable.ts +++ b/x-pack/plugins/security/server/routes/users/disable.ts @@ -15,6 +15,12 @@ export function defineDisableUserRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}/_disable', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), }, diff --git a/x-pack/plugins/security/server/routes/users/enable.ts b/x-pack/plugins/security/server/routes/users/enable.ts index a8a9d62bee938..18ec66683bd56 100644 --- a/x-pack/plugins/security/server/routes/users/enable.ts +++ b/x-pack/plugins/security/server/routes/users/enable.ts @@ -15,6 +15,12 @@ export function defineEnableUserRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}/_enable', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), }, diff --git a/x-pack/plugins/security/server/routes/users/get.ts b/x-pack/plugins/security/server/routes/users/get.ts index ed18c8437627d..076c8c9beeef2 100644 --- a/x-pack/plugins/security/server/routes/users/get.ts +++ b/x-pack/plugins/security/server/routes/users/get.ts @@ -15,6 +15,12 @@ export function defineGetUserRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/users/{username}', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, validate: { params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), }, diff --git a/x-pack/plugins/security/server/routes/users/get_all.ts b/x-pack/plugins/security/server/routes/users/get_all.ts index eae0664189340..c8c340f2b2ceb 100644 --- a/x-pack/plugins/security/server/routes/users/get_all.ts +++ b/x-pack/plugins/security/server/routes/users/get_all.ts @@ -11,7 +11,16 @@ import { createLicensedRouteHandler } from '../licensed_route_handler'; export function defineGetAllUsersRoutes({ router }: RouteDefinitionParams) { router.get( - { path: '/internal/security/users', validate: false }, + { + path: '/internal/security/users', + security: { + authz: { + enabled: false, + reason: `This route delegates authorization to Core's scoped ES cluster client`, + }, + }, + validate: false, + }, createLicensedRouteHandler(async (context, request, response) => { try { const esClient = (await context.core).elasticsearch.client; diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts index 823fbb0286f33..ff6399f186610 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -35,7 +35,17 @@ export function defineAccessAgreementRoutes({ ); router.get( - { path: '/internal/security/access_agreement/state', validate: false }, + { + path: '/internal/security/access_agreement/state', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it requires only an active session in order to function', + }, + }, + validate: false, + }, createLicensedRouteHandler(async (context, request, response) => { if (!canHandleRequest()) { return response.forbidden({ diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 8cf8459d523b8..ed3228c244b51 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -57,7 +57,18 @@ export function defineLoginRoutes({ ); router.get( - { path: '/internal/security/login_state', validate: false, options: { authRequired: false } }, + { + path: '/internal/security/login_state', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it only provides non-sensative information about authentication provider configuration', + }, + }, + validate: false, + options: { authRequired: false }, + }, async (context, request, response) => { const { allowLogin, layout = 'form' } = license.getFeatures(); const { sortedProviders, selector } = config.authc; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 24a94b43029e0..9da144facf4f4 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -49,9 +49,21 @@ describe.skip('onPostAuthInterceptor', () => { */ function initKbnServer(router: IRouter, basePath: IBasePath) { - router.get({ path: '/api/np_test/foo', validate: false }, (context, req, h) => { - return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); - }); + router.get( + { + path: '/api/np_test/foo', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, + validate: false, + }, + (context, req, h) => { + return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); + } + ); } async function request( diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index bf3d0a57ccae2..3a5ce95ec3341 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -38,14 +38,32 @@ describe.skip('onRequestInterceptor', () => { function initKbnServer(router: IRouter, basePath: IBasePath) { router.get( - { path: '/np_foo', validate: false }, + { + path: '/np_foo', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, + validate: false, + }, (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); } ); router.get( - { path: '/some/path/s/np_foo/bar', validate: false }, + { + path: '/some/path/s/np_foo/bar', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, + validate: false, + }, (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); } @@ -54,6 +72,12 @@ describe.skip('onRequestInterceptor', () => { router.get( { path: '/i/love/np_spaces', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: { query: schema.object({ queryParam: schema.string({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.ts index 06bef75774aa0..4908f1a747b74 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -31,6 +31,13 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { params: schema.object({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts index a1610bbfed975..2703e7c36f0cd 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts @@ -18,6 +18,13 @@ export function initDisableLegacyUrlAliasesApi(deps: ExternalRouteDeps) { router.post( { path: '/api/spaces/_disable_legacy_url_aliases', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, options: { access: isServerless ? 'internal' : 'public', summary: 'Disable legacy URL aliases', diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.ts b/x-pack/plugins/spaces/server/routes/api/external/get.ts index b1ab2dc575774..3c9871e44490c 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.ts @@ -28,6 +28,13 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { params: schema.object({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts index 746735bb3736e..f7a0c4592387c 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts @@ -27,6 +27,13 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { query: schema.object({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts index f49070be66fe2..98dab60cd9c95 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts @@ -17,6 +17,13 @@ export function initGetShareableReferencesApi(deps: ExternalRouteDeps) { router.post( { path: '/api/spaces/_get_shareable_references', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, options: { access: isServerless ? 'internal' : 'public', summary: `Get shareable references`, diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.ts b/x-pack/plugins/spaces/server/routes/api/external/post.ts index de1ec53aaee44..2ecd70828d570 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.ts @@ -30,6 +30,13 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { body: getSpaceSchema(isServerless), diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.ts b/x-pack/plugins/spaces/server/routes/api/external/put.ts index 740e81bac446e..abdac1f0977d1 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.ts @@ -29,6 +29,13 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) { .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, validate: { request: { params: schema.object({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts index 9fb2a8626a841..fb9137a834349 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts @@ -40,6 +40,13 @@ export function initUpdateObjectsSpacesApi(deps: ExternalRouteDeps) { router.post( { path: '/api/spaces/_update_objects_spaces', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, options: { access: isServerless ? 'internal' : 'public', summary: `Update saved objects in spaces`, diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts index 2996e7dbc4ed1..2480b0c003fee 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts @@ -15,6 +15,13 @@ export function initGetActiveSpaceApi(deps: InternalRouteDeps) { router.get( { path: '/internal/spaces/_active_space', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service getActiveSpace API, which uses a scoped spaces client', + }, + }, validate: false, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.ts b/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.ts index 6732a8520946d..cfe14705a4e22 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.ts @@ -37,6 +37,13 @@ export function initSetSolutionSpaceApi(deps: InternalRouteDeps) { router.put( { path: '/internal/spaces/space/{id}/solution', + security: { + authz: { + enabled: false, + reason: + 'This route delegates authorization to the spaces service via a scoped spaces client', + }, + }, options: { description: `Update solution for a space`, }, From de09e3af76d4bd1ce029830bd866e20b46e3aa9e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:07:11 +1100 Subject: [PATCH 46/47] [8.x] [Security Solution][Data Quality Dashboard][Serverless] add start/end time support for latest_results (#199248) (#199385) # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution][Data Quality Dashboard][Serverless] add start/end time support for latest_results (#199248)](https://github.com/elastic/kibana/pull/199248) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Karen Grigoryan --- .../data_quality_context/index.test.tsx | 2 + .../data_quality_context/index.tsx | 8 + .../hooks/use_ilm_explain/index.test.tsx | 4 + .../pattern/hooks/use_stats/index.test.tsx | 4 + .../use_stored_pattern_results/index.test.tsx | 82 +++++++- .../use_stored_pattern_results/index.tsx | 55 ++++-- .../hooks/use_results_rollup/index.test.tsx | 29 ++- .../hooks/use_results_rollup/index.tsx | 13 +- .../use_results_rollup/utils/storage.test.ts | 22 +++ .../hooks/use_results_rollup/utils/storage.ts | 31 ++- .../impl/data_quality_panel/index.test.tsx | 2 + .../impl/data_quality_panel/index.tsx | 8 + .../mock/test_providers/test_providers.tsx | 4 + .../get_merged_data_quality_context_props.ts | 6 + .../server/helpers/get_available_indices.ts | 6 +- .../get_range_filtered_indices.test.ts | 96 ++++++++++ .../helpers/get_range_filtered_indices.ts | 61 ++++++ .../lib/fetch_available_indices.test.ts | 32 ++-- .../server/lib/fetch_available_indices.ts | 6 +- .../server/routes/get_index_stats.ts | 2 +- .../results/get_index_results_latest.test.ts | 181 +++++++++++++++++- .../results/get_index_results_latest.ts | 26 ++- .../server/schemas/result.ts | 5 + .../overview/pages/data_quality.test.tsx | 13 +- .../public/overview/pages/data_quality.tsx | 2 + 25 files changed, 642 insertions(+), 58 deletions(-) create mode 100644 x-pack/plugins/ecs_data_quality_dashboard/server/helpers/get_range_filtered_indices.test.ts create mode 100644 x-pack/plugins/ecs_data_quality_dashboard/server/helpers/get_range_filtered_indices.ts diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_context/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_context/index.test.tsx index fb8eccd4b7f8a..a82bf7d6c432b 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_context/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_context/index.test.tsx @@ -65,6 +65,8 @@ const ContextWrapper: FC> = ({ children }) => ( }, ]} setSelectedIlmPhaseOptions={jest.fn()} + defaultStartTime="now-7d" + defaultEndTime="now" > {children} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_context/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_context/index.tsx index 762efef424a10..876ff528e75ff 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_context/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_context/index.tsx @@ -41,6 +41,8 @@ export interface DataQualityProviderProps { ilmPhases: string[]; selectedIlmPhaseOptions: EuiComboBoxOptionOption[]; setSelectedIlmPhaseOptions: (options: EuiComboBoxOptionOption[]) => void; + defaultStartTime: string; + defaultEndTime: string; } const DataQualityContext = React.createContext(undefined); @@ -67,6 +69,8 @@ export const DataQualityProvider: React.FC { const value = useMemo( () => ({ @@ -90,6 +94,8 @@ export const DataQualityProvider: React.FC {children} @@ -159,6 +161,8 @@ describe('useIlmExplain', () => { }, ]} setSelectedIlmPhaseOptions={jest.fn()} + defaultStartTime={'now-7d'} + defaultEndTime={'now'} > {children} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_stats/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_stats/index.test.tsx index 061bbb5aa6824..ae4ee9a7bd2c4 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_stats/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/hooks/use_stats/index.test.tsx @@ -69,6 +69,8 @@ const ContextWrapper: FC> = ({ children }) => ( }, ]} setSelectedIlmPhaseOptions={jest.fn()} + defaultStartTime={'now-7d'} + defaultEndTime={'now'} > {children} @@ -119,6 +121,8 @@ const ContextWrapperILMNotAvailable: FC> = ({ childre }, ]} setSelectedIlmPhaseOptions={jest.fn()} + defaultStartTime={'now-7d'} + defaultEndTime={'now'} > {children} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.test.tsx index d58bf3af39d58..5f90890eea693 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.test.tsx @@ -11,6 +11,10 @@ import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { getHistoricalResultStub } from '../../../../stub/get_historical_result_stub'; import { useStoredPatternResults } from '.'; +const startTime = 'now-7d'; +const endTime = 'now'; +const isILMAvailable = true; + describe('useStoredPatternResults', () => { const httpFetch = jest.fn(); const mockToasts = notificationServiceMock.createStartContract().toasts; @@ -21,7 +25,16 @@ describe('useStoredPatternResults', () => { describe('when patterns are empty', () => { it('should return an empty array and not call getStorageResults', () => { - const { result } = renderHook(() => useStoredPatternResults([], mockToasts, httpFetch)); + const { result } = renderHook(() => + useStoredPatternResults({ + patterns: [], + toasts: mockToasts, + httpFetch, + isILMAvailable, + startTime, + endTime, + }) + ); expect(result.current).toEqual([]); expect(httpFetch).not.toHaveBeenCalled(); @@ -45,7 +58,14 @@ describe('useStoredPatternResults', () => { }); const { result, waitFor } = renderHook(() => - useStoredPatternResults(patterns, mockToasts, httpFetch) + useStoredPatternResults({ + patterns, + toasts: mockToasts, + httpFetch, + isILMAvailable, + startTime, + endTime, + }) ); await waitFor(() => result.current.length > 0); @@ -104,5 +124,63 @@ describe('useStoredPatternResults', () => { }, ]); }); + + describe('when isILMAvailable is false', () => { + it('should call getStorageResults with startDate and endDate', async () => { + const patterns = ['pattern1-*', 'pattern2-*']; + + httpFetch.mockImplementation((path: string) => { + if (path === '/internal/ecs_data_quality_dashboard/results_latest/pattern1-*') { + return Promise.resolve([getHistoricalResultStub('pattern1-index1')]); + } + + if (path === '/internal/ecs_data_quality_dashboard/results_latest/pattern2-*') { + return Promise.resolve([getHistoricalResultStub('pattern2-index1')]); + } + + return Promise.reject(new Error('Invalid path')); + }); + + const { result, waitFor } = renderHook(() => + useStoredPatternResults({ + patterns, + toasts: mockToasts, + httpFetch, + isILMAvailable: false, + startTime, + endTime, + }) + ); + + await waitFor(() => result.current.length > 0); + + expect(httpFetch).toHaveBeenCalledTimes(2); + + expect(httpFetch).toHaveBeenCalledWith( + '/internal/ecs_data_quality_dashboard/results_latest/pattern1-*', + { + method: 'GET', + signal: expect.any(AbortSignal), + version: '1', + query: { + startDate: startTime, + endDate: endTime, + }, + } + ); + expect(httpFetch).toHaveBeenCalledWith( + '/internal/ecs_data_quality_dashboard/results_latest/pattern2-*', + { + method: 'GET', + signal: expect.any(AbortSignal), + version: '1', + query: { + startDate: startTime, + endDate: endTime, + }, + } + ); + }); + }); }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.tsx index 17334c4b4a586..b92b36218c07a 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/hooks/use_stored_pattern_results/index.tsx @@ -10,13 +10,34 @@ import { HttpHandler } from '@kbn/core-http-browser'; import { isEmpty } from 'lodash/fp'; import { DataQualityCheckResult } from '../../../../types'; -import { formatResultFromStorage, getStorageResults } from '../../utils/storage'; +import { + GetStorageResultsOpts, + formatResultFromStorage, + getStorageResults, +} from '../../utils/storage'; -export const useStoredPatternResults = ( - patterns: string[], - toasts: IToasts, - httpFetch: HttpHandler -) => { +export interface UseStoredPatternResultsOpts { + patterns: string[]; + toasts: IToasts; + httpFetch: HttpHandler; + isILMAvailable: boolean; + startTime: string; + endTime: string; +} + +export type UseStoredPatternResultsReturnValue = Array<{ + pattern: string; + results: Record; +}>; + +export const useStoredPatternResults = ({ + patterns, + toasts, + httpFetch, + isILMAvailable, + startTime, + endTime, +}: UseStoredPatternResultsOpts): UseStoredPatternResultsReturnValue => { const [storedPatternResults, setStoredPatternResults] = useState< Array<{ pattern: string; results: Record }> >([]); @@ -28,8 +49,20 @@ export const useStoredPatternResults = ( const abortController = new AbortController(); const fetchStoredPatternResults = async () => { - const requests = patterns.map((pattern) => - getStorageResults({ pattern, httpFetch, abortController, toasts }).then((results = []) => ({ + const requests = patterns.map(async (pattern) => { + const getStorageResultsOpts: GetStorageResultsOpts = { + pattern, + httpFetch, + abortController, + toasts, + }; + + if (!isILMAvailable) { + getStorageResultsOpts.startTime = startTime; + getStorageResultsOpts.endTime = endTime; + } + + return getStorageResults(getStorageResultsOpts).then((results) => ({ pattern, results: Object.fromEntries( results.map((storageResult) => [ @@ -37,8 +70,8 @@ export const useStoredPatternResults = ( formatResultFromStorage({ storageResult, pattern }), ]) ), - })) - ); + })); + }); const patternResults = await Promise.all(requests); if (patternResults?.length) { @@ -47,7 +80,7 @@ export const useStoredPatternResults = ( }; fetchStoredPatternResults(); - }, [httpFetch, patterns, toasts]); + }, [endTime, httpFetch, isILMAvailable, patterns, startTime, toasts]); return storedPatternResults; }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.test.tsx index bff3c3dd54f12..7dc74731d66dd 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.test.tsx @@ -35,6 +35,8 @@ describe('useResultsRollup', () => { const patterns = ['auditbeat-*', 'packetbeat-*']; const isILMAvailable = true; + const startTime = 'now-7d'; + const endTime = 'now'; const useStoredPatternResultsMock = useStoredPatternResults as jest.Mock; @@ -52,6 +54,8 @@ describe('useResultsRollup', () => { patterns, isILMAvailable, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); @@ -94,10 +98,19 @@ describe('useResultsRollup', () => { patterns: ['auditbeat-*'], isILMAvailable, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); - expect(useStoredPatternResultsMock).toHaveBeenCalledWith(['auditbeat-*'], toasts, httpFetch); + expect(useStoredPatternResultsMock).toHaveBeenCalledWith({ + patterns: ['auditbeat-*'], + toasts, + httpFetch, + isILMAvailable, + startTime, + endTime, + }); expect(result.current.patternRollups).toEqual({ 'auditbeat-*': { @@ -119,6 +132,8 @@ describe('useResultsRollup', () => { patterns, isILMAvailable, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); @@ -144,6 +159,8 @@ describe('useResultsRollup', () => { patterns, isILMAvailable, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); @@ -180,6 +197,8 @@ describe('useResultsRollup', () => { patterns, isILMAvailable, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); @@ -369,6 +388,8 @@ describe('useResultsRollup', () => { patterns, isILMAvailable: false, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); @@ -532,6 +553,8 @@ describe('useResultsRollup', () => { patterns, isILMAvailable, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); @@ -592,6 +615,8 @@ describe('useResultsRollup', () => { patterns, isILMAvailable, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); @@ -654,6 +679,8 @@ describe('useResultsRollup', () => { patterns: ['packetbeat-*', 'auditbeat-*'], isILMAvailable, telemetryEvents: mockTelemetryEvents, + startTime, + endTime, }) ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx index d95f1d1b7f20f..bfed849e373d3 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/index.tsx @@ -40,6 +40,8 @@ interface Props { httpFetch: HttpHandler; telemetryEvents: TelemetryEvents; isILMAvailable: boolean; + startTime: string; + endTime: string; } export const useResultsRollup = ({ httpFetch, @@ -47,11 +49,20 @@ export const useResultsRollup = ({ patterns, isILMAvailable, telemetryEvents, + startTime, + endTime, }: Props): UseResultsRollupReturnValue => { const [patternIndexNames, setPatternIndexNames] = useState>({}); const [patternRollups, setPatternRollups] = useState>({}); - const storedPatternsResults = useStoredPatternResults(patterns, toasts, httpFetch); + const storedPatternsResults = useStoredPatternResults({ + httpFetch, + patterns, + toasts, + isILMAvailable, + startTime, + endTime, + }); useEffect(() => { if (!isEmpty(storedPatternsResults)) { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.test.ts index 9f315d65c01d5..b43954e73f6fd 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.test.ts @@ -200,4 +200,26 @@ describe('getStorageResults', () => { expect(toasts.addError).toHaveBeenCalledWith('test-error', { title: expect.any(String) }); expect(results).toEqual([]); }); + + it('should provide stad and end date', async () => { + await getStorageResults({ + httpFetch: fetch, + abortController: new AbortController(), + pattern: 'auditbeat-*', + toasts, + startTime: 'now-7d', + endTime: 'now', + }); + + expect(fetch).toHaveBeenCalledWith( + '/internal/ecs_data_quality_dashboard/results_latest/auditbeat-*', + expect.objectContaining({ + method: 'GET', + query: { + startDate: 'now-7d', + endDate: 'now', + }, + }) + ); + }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.ts index e4a5c43d5b4a5..7fc339c085bea 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/hooks/use_results_rollup/utils/storage.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { HttpHandler } from '@kbn/core-http-browser'; +import { HttpFetchQuery, HttpHandler } from '@kbn/core-http-browser'; import { IToasts } from '@kbn/core-notifications-browser'; import { @@ -131,23 +131,40 @@ export async function postStorageResult({ } } +export interface GetStorageResultsOpts { + pattern: string; + httpFetch: HttpHandler; + toasts: IToasts; + abortController: AbortController; + startTime?: string; + endTime?: string; +} + export async function getStorageResults({ pattern, httpFetch, toasts, abortController, -}: { - pattern: string; - httpFetch: HttpHandler; - toasts: IToasts; - abortController: AbortController; -}): Promise { + startTime, + endTime, +}: GetStorageResultsOpts): Promise { try { const route = GET_INDEX_RESULTS_LATEST.replace('{pattern}', pattern); + + const query: HttpFetchQuery = {}; + + if (startTime) { + query.startDate = startTime; + } + if (endTime) { + query.endDate = endTime; + } + const results = await httpFetch(route, { method: 'GET', signal: abortController.signal, version: INTERNAL_API_VERSION, + ...(Object.keys(query).length > 0 ? { query } : {}), }); return results; } catch (err) { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.test.tsx index 90e5dba08d4dc..f925a67ea3d32 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.test.tsx @@ -67,6 +67,8 @@ describe('DataQualityPanel', () => { setLastChecked={jest.fn()} baseTheme={DARK_THEME} toasts={toasts} + defaultStartTime={'now-7d'} + defaultEndTime={'now'} /> ); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.tsx index b6d2736d7e175..9b9cbdefb6670 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/index.tsx @@ -46,6 +46,8 @@ interface Props { setLastChecked: (lastChecked: string) => void; startDate?: string | null; theme?: PartialTheme; + defaultStartTime: string; + defaultEndTime: string; } const defaultSelectedIlmPhaseOptions: EuiComboBoxOptionOption[] = ilmPhaseOptionsStatic.filter( @@ -71,6 +73,8 @@ const DataQualityPanelComponent: React.FC = ({ setLastChecked, startDate, theme, + defaultStartTime, + defaultEndTime, }) => { const [selectedIlmPhaseOptions, setSelectedIlmPhaseOptions] = useState( defaultSelectedIlmPhaseOptions @@ -109,6 +113,8 @@ const DataQualityPanelComponent: React.FC = ({ toasts, isILMAvailable, telemetryEvents, + startTime: defaultStartTime, + endTime: defaultEndTime, }); const indicesCheckHookReturnValue = useIndicesCheck({ @@ -137,6 +143,8 @@ const DataQualityPanelComponent: React.FC = ({ ilmPhases={ilmPhases} selectedIlmPhaseOptions={selectedIlmPhaseOptions} setSelectedIlmPhaseOptions={setSelectedIlmPhaseOptions} + defaultStartTime={defaultStartTime} + defaultEndTime={defaultEndTime} > diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx index 17b73f1e6dcd0..e0220d26e8690 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx @@ -135,6 +135,8 @@ const TestDataQualityProvidersComponent: React.FC ilmPhases, selectedIlmPhaseOptions, setSelectedIlmPhaseOptions, + defaultStartTime, + defaultEndTime, } = getMergedDataQualityContextProps(dataQualityContextProps); const mergedResultsRollupContextProps = @@ -162,6 +164,8 @@ const TestDataQualityProvidersComponent: React.FC ilmPhases={ilmPhases} selectedIlmPhaseOptions={selectedIlmPhaseOptions} setSelectedIlmPhaseOptions={setSelectedIlmPhaseOptions} + defaultStartTime={defaultStartTime} + defaultEndTime={defaultEndTime} > ({ - index: indexPattern, + index: indexNameOrPattern, aggs: { index: { terms: { diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/helpers/get_range_filtered_indices.test.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/helpers/get_range_filtered_indices.test.ts new file mode 100644 index 0000000000000..87350abcf8a9c --- /dev/null +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/helpers/get_range_filtered_indices.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRangeFilteredIndices } from './get_range_filtered_indices'; +import { fetchAvailableIndices } from '../lib/fetch_available_indices'; +import type { IScopedClusterClient, Logger } from '@kbn/core/server'; + +jest.mock('../lib/fetch_available_indices'); + +const fetchAvailableIndicesMock = fetchAvailableIndices as jest.Mock; + +describe('getRangeFilteredIndices', () => { + let client: jest.Mocked; + let logger: jest.Mocked; + + beforeEach(() => { + client = { + asCurrentUser: jest.fn(), + } as unknown as jest.Mocked; + + logger = { + warn: jest.fn(), + error: jest.fn(), + } as unknown as jest.Mocked; + + jest.clearAllMocks(); + }); + + describe('when fetching available indices is successful', () => { + describe('and there are available indices', () => { + it('should return the flattened available indices', async () => { + fetchAvailableIndicesMock.mockResolvedValueOnce(['index1', 'index2']); + fetchAvailableIndicesMock.mockResolvedValueOnce(['index3']); + + const result = await getRangeFilteredIndices({ + client, + authorizedIndexNames: ['auth1', 'auth2'], + startDate: '2023-01-01', + endDate: '2023-01-31', + logger, + pattern: 'pattern*', + }); + + expect(fetchAvailableIndices).toHaveBeenCalledTimes(2); + expect(result).toEqual(['index1', 'index2', 'index3']); + expect(logger.warn).not.toHaveBeenCalled(); + }); + }); + + describe('and there are no available indices', () => { + it('should log a warning and return an empty array', async () => { + fetchAvailableIndicesMock.mockResolvedValue([]); + + const result = await getRangeFilteredIndices({ + client, + authorizedIndexNames: ['auth1', 'auth2'], + startDate: '2023-01-01', + endDate: '2023-01-31', + logger, + pattern: 'pattern*', + }); + + expect(fetchAvailableIndices).toHaveBeenCalledTimes(2); + expect(result).toEqual([]); + expect(logger.warn).toHaveBeenCalledWith( + 'No available authorized indices found under pattern: pattern*, in the given date range: 2023-01-01 - 2023-01-31' + ); + }); + }); + }); + + describe('when fetching available indices fails', () => { + it('should log an error and return an empty array', async () => { + fetchAvailableIndicesMock.mockRejectedValue(new Error('Fetch error')); + + const result = await getRangeFilteredIndices({ + client, + authorizedIndexNames: ['auth1'], + startDate: '2023-01-01', + endDate: '2023-01-31', + logger, + pattern: 'pattern*', + }); + + expect(fetchAvailableIndices).toHaveBeenCalledTimes(1); + expect(result).toEqual([]); + expect(logger.error).toHaveBeenCalledWith( + 'Error fetching available indices in the given data range: 2023-01-01 - 2023-01-31' + ); + }); + }); +}); diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/helpers/get_range_filtered_indices.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/helpers/get_range_filtered_indices.ts new file mode 100644 index 0000000000000..45a87424169e8 --- /dev/null +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/helpers/get_range_filtered_indices.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IScopedClusterClient, Logger } from '@kbn/core/server'; + +import { fetchAvailableIndices } from '../lib/fetch_available_indices'; + +export const getRangeFilteredIndices = async ({ + client, + authorizedIndexNames, + startDate, + endDate, + logger, + pattern, +}: { + client: IScopedClusterClient; + authorizedIndexNames: string[]; + startDate: string; + endDate: string; + logger: Logger; + pattern: string; +}): Promise => { + const decodedStartDate = decodeURIComponent(startDate); + const decodedEndDate = decodeURIComponent(endDate); + try { + const currentUserEsClient = client.asCurrentUser; + + const availableIndicesPromises: Array> = []; + + for (const indexName of authorizedIndexNames) { + availableIndicesPromises.push( + fetchAvailableIndices(currentUserEsClient, { + indexNameOrPattern: indexName, + startDate: decodedStartDate, + endDate: decodedEndDate, + }) + ); + } + + const availableIndices = await Promise.all(availableIndicesPromises); + + const flattenedAvailableIndices = availableIndices.flat(); + + if (flattenedAvailableIndices.length === 0) { + logger.warn( + `No available authorized indices found under pattern: ${pattern}, in the given date range: ${decodedStartDate} - ${decodedEndDate}` + ); + } + + return flattenedAvailableIndices; + } catch (err) { + logger.error( + `Error fetching available indices in the given data range: ${decodedStartDate} - ${decodedEndDate}` + ); + return []; + } +}; diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/lib/fetch_available_indices.test.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/lib/fetch_available_indices.test.ts index fa26fb68289a6..9fe8213b4eb95 100644 --- a/x-pack/plugins/ecs_data_quality_dashboard/server/lib/fetch_available_indices.test.ts +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/lib/fetch_available_indices.test.ts @@ -61,7 +61,7 @@ describe('fetchAvailableIndices', () => { const esClientMock = getEsClientMock(); await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -101,7 +101,7 @@ describe('fetchAvailableIndices', () => { const esClientMock = getEsClientMock(); await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -133,7 +133,7 @@ describe('fetchAvailableIndices', () => { ]); const result = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -164,7 +164,7 @@ describe('fetchAvailableIndices', () => { ]); const result = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -180,7 +180,7 @@ describe('fetchAvailableIndices', () => { esClientMock.cat.indices.mockResolvedValue([]); const result = await fetchAvailableIndices(esClientMock, { - indexPattern: 'nonexistent-*', + indexNameOrPattern: 'nonexistent-*', startDate: startDateString, endDate: endDateString, }); @@ -209,7 +209,7 @@ describe('fetchAvailableIndices', () => { }); const result = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -243,7 +243,7 @@ describe('fetchAvailableIndices', () => { }); const result = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -268,7 +268,7 @@ describe('fetchAvailableIndices', () => { ]); const result = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -285,7 +285,7 @@ describe('fetchAvailableIndices', () => { await expect( fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }) @@ -307,7 +307,7 @@ describe('fetchAvailableIndices', () => { }); const result = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -336,7 +336,7 @@ describe('fetchAvailableIndices', () => { }); const result = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }); @@ -371,7 +371,7 @@ describe('fetchAvailableIndices', () => { ]); const results = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: 'now-7d/d', endDate: 'now/d', }); @@ -390,7 +390,7 @@ describe('fetchAvailableIndices', () => { ]); const results = await fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: 'now-7d/d', endDate: 'now-1d/d', }); @@ -415,7 +415,7 @@ describe('fetchAvailableIndices', () => { await expect( fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: endDateString, }) @@ -429,7 +429,7 @@ describe('fetchAvailableIndices', () => { await expect( fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: 'invalid-date', endDate: endDateString, }) @@ -443,7 +443,7 @@ describe('fetchAvailableIndices', () => { await expect( fetchAvailableIndices(esClientMock, { - indexPattern: 'logs-*', + indexNameOrPattern: 'logs-*', startDate: startDateString, endDate: 'invalid-date', }) diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/lib/fetch_available_indices.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/lib/fetch_available_indices.ts index 32311f28d636a..36009f315010b 100644 --- a/x-pack/plugins/ecs_data_quality_dashboard/server/lib/fetch_available_indices.ts +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/lib/fetch_available_indices.ts @@ -32,15 +32,15 @@ const getParsedDateMs = (dateStr: string, roundUp = false) => { export const fetchAvailableIndices = async ( esClient: ElasticsearchClient, - params: { indexPattern: string; startDate: string; endDate: string } + params: { indexNameOrPattern: string; startDate: string; endDate: string } ): Promise => { - const { indexPattern, startDate, endDate } = params; + const { indexNameOrPattern, startDate, endDate } = params; const startDateMs = getParsedDateMs(startDate); const endDateMs = getParsedDateMs(endDate, true); const indicesCats = (await esClient.cat.indices({ - index: indexPattern, + index: indexNameOrPattern, format: 'json', h: 'index,creation.date', })) as FetchAvailableCatIndicesResponseRequired; diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/routes/get_index_stats.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/routes/get_index_stats.ts index d1bb25d34fc2a..fd1ec1694719d 100644 --- a/x-pack/plugins/ecs_data_quality_dashboard/server/routes/get_index_stats.ts +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/routes/get_index_stats.ts @@ -85,7 +85,7 @@ export const getIndexStatsRoute = (router: IRouter, logger: Logger) => { const meteringStatsIndices = parseMeteringStats(meteringStats.indices); const availableIndices = await fetchAvailableIndices(esClient, { - indexPattern: decodedIndexName, + indexNameOrPattern: decodedIndexName, startDate: decodedStartDate, endDate: decodedEndDate, }); diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/routes/results/get_index_results_latest.test.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/routes/results/get_index_results_latest.test.ts index bfb38864916fe..94c892e401b5a 100644 --- a/x-pack/plugins/ecs_data_quality_dashboard/server/routes/results/get_index_results_latest.test.ts +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/routes/results/get_index_results_latest.test.ts @@ -16,6 +16,24 @@ import { resultDocument } from './results.mock'; import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { ResultDocument } from '../../schemas/result'; import type { CheckIndicesPrivilegesParam } from './privileges'; +import { getRangeFilteredIndices } from '../../helpers/get_range_filtered_indices'; + +const mockCheckIndicesPrivileges = jest.fn(({ indices }: CheckIndicesPrivilegesParam) => + Promise.resolve(Object.fromEntries(indices.map((index) => [index, true]))) +); +jest.mock('./privileges', () => ({ + checkIndicesPrivileges: (params: CheckIndicesPrivilegesParam) => + mockCheckIndicesPrivileges(params), +})); + +jest.mock('../../helpers/get_range_filtered_indices', () => ({ + getRangeFilteredIndices: jest.fn(), +})); + +const mockGetRangeFilteredIndices = getRangeFilteredIndices as jest.Mock; + +const startDate = 'now-7d'; +const endDate = 'now'; const searchResponse = { aggregations: { @@ -33,14 +51,6 @@ const searchResponse = { Record >; -const mockCheckIndicesPrivileges = jest.fn(({ indices }: CheckIndicesPrivilegesParam) => - Promise.resolve(Object.fromEntries(indices.map((index) => [index, true]))) -); -jest.mock('./privileges', () => ({ - checkIndicesPrivileges: (params: CheckIndicesPrivilegesParam) => - mockCheckIndicesPrivileges(params), -})); - describe('getIndexResultsLatestRoute route', () => { describe('querying', () => { let server: ReturnType; @@ -68,7 +78,7 @@ describe('getIndexResultsLatestRoute route', () => { getIndexResultsLatestRoute(server.router, logger); }); - it('gets result', async () => { + it('gets result without startDate and endDate', async () => { const mockSearch = context.core.elasticsearch.client.asInternalUser.search; mockSearch.mockResolvedValueOnce(searchResponse); @@ -80,6 +90,159 @@ describe('getIndexResultsLatestRoute route', () => { expect(response.status).toEqual(200); expect(response.body).toEqual([resultDocument]); + + expect(mockGetRangeFilteredIndices).not.toHaveBeenCalled(); + }); + + it('gets result with startDate and endDate', async () => { + const reqWithDate = requestMock.create({ + method: 'get', + path: GET_INDEX_RESULTS_LATEST, + params: { pattern: 'logs-*' }, + query: { startDate, endDate }, + }); + + const filteredIndices = ['filtered-index-1', 'filtered-index-2']; + mockGetRangeFilteredIndices.mockResolvedValueOnce(filteredIndices); + const mockSearch = context.core.elasticsearch.client.asInternalUser.search; + mockSearch.mockResolvedValueOnce(searchResponse); + + const response = await server.inject(reqWithDate, requestContextMock.convertContext(context)); + + expect(mockGetRangeFilteredIndices).toHaveBeenCalledWith({ + client: context.core.elasticsearch.client, + authorizedIndexNames: [resultDocument.indexName], + startDate, + endDate, + logger, + pattern: 'logs-*', + }); + + expect(mockSearch).toHaveBeenCalledWith({ + index: expect.any(String), + ...getQuery(filteredIndices), + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([resultDocument]); + }); + + it('handles getRangeFilteredIndices error', async () => { + const errorMessage = 'Range Filter Error'; + + const reqWithDate = requestMock.create({ + method: 'get', + path: GET_INDEX_RESULTS_LATEST, + params: { pattern: 'logs-*' }, + query: { startDate, endDate }, + }); + + mockGetRangeFilteredIndices.mockRejectedValueOnce(new Error(errorMessage)); + + const response = await server.inject(reqWithDate, requestContextMock.convertContext(context)); + + expect(mockGetRangeFilteredIndices).toHaveBeenCalledWith({ + client: context.core.elasticsearch.client, + authorizedIndexNames: [resultDocument.indexName], + startDate, + endDate, + logger, + pattern: 'logs-*', + }); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ message: errorMessage, status_code: 500 }); + expect(logger.error).toHaveBeenCalledWith(errorMessage); + }); + + it('gets result with startDate and endDate and multiple filtered indices', async () => { + const filteredIndices = ['filtered-index-1', 'filtered-index-2', 'filtered-index-3']; + const filteredIndicesSearchResponse = { + aggregations: { + latest: { + buckets: filteredIndices.map((indexName) => ({ + key: indexName, + latest_doc: { hits: { hits: [{ _source: { indexName } }] } }, + })), + }, + }, + } as unknown as SearchResponse< + ResultDocument, + Record + >; + + const reqWithDate = requestMock.create({ + method: 'get', + path: GET_INDEX_RESULTS_LATEST, + params: { pattern: 'logs-*' }, + query: { startDate, endDate }, + }); + + mockGetRangeFilteredIndices.mockResolvedValueOnce(filteredIndices); + context.core.elasticsearch.client.asInternalUser.search.mockResolvedValueOnce( + filteredIndicesSearchResponse + ); + + const response = await server.inject(reqWithDate, requestContextMock.convertContext(context)); + + expect(mockGetRangeFilteredIndices).toHaveBeenCalledWith({ + client: context.core.elasticsearch.client, + authorizedIndexNames: [resultDocument.indexName], + startDate, + endDate, + logger, + pattern: 'logs-*', + }); + + expect(context.core.elasticsearch.client.asInternalUser.search).toHaveBeenCalledWith({ + index: expect.any(String), + ...getQuery(filteredIndices), + }); + + const expectedResults = filteredIndices.map((indexName) => ({ + indexName, + })) as ResultDocument[]; + expect(response.status).toEqual(200); + expect(response.body).toEqual(expectedResults); + }); + + it('handles partial authorization when using startDate and endDate', async () => { + const authorizationResult = { + 'filtered-index-1': true, + 'filtered-index-2': false, + }; + + mockGetRangeFilteredIndices.mockResolvedValueOnce(['filtered-index-1']); + mockCheckIndicesPrivileges.mockResolvedValueOnce(authorizationResult); + + const mockSearch = context.core.elasticsearch.client.asInternalUser.search; + mockSearch.mockResolvedValueOnce(searchResponse); + + const reqWithDate = requestMock.create({ + method: 'get', + path: GET_INDEX_RESULTS_LATEST, + params: { pattern: 'logs-*' }, + query: { startDate, endDate }, + }); + + const response = await server.inject(reqWithDate, requestContextMock.convertContext(context)); + + expect(mockGetRangeFilteredIndices).toHaveBeenCalledWith({ + client: context.core.elasticsearch.client, + authorizedIndexNames: ['filtered-index-1'], + startDate, + endDate, + logger, + pattern: 'logs-*', + }); + + expect(context.core.elasticsearch.client.asInternalUser.search).toHaveBeenCalledWith({ + index: expect.any(String), + ...getQuery(['filtered-index-1']), + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([resultDocument]); }); it('handles results data stream error', async () => { diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/routes/results/get_index_results_latest.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/routes/results/get_index_results_latest.ts index 3a294409af869..f7d1d5eed74cc 100644 --- a/x-pack/plugins/ecs_data_quality_dashboard/server/routes/results/get_index_results_latest.ts +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/routes/results/get_index_results_latest.ts @@ -4,18 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import type { IRouter, Logger } from '@kbn/core/server'; import { INTERNAL_API_VERSION, GET_INDEX_RESULTS_LATEST } from '../../../common/constants'; import { buildResponse } from '../../lib/build_response'; import { buildRouteValidation } from '../../schemas/common'; -import { GetIndexResultsLatestParams } from '../../schemas/result'; +import { GetIndexResultsLatestParams, GetIndexResultsLatestQuery } from '../../schemas/result'; import type { ResultDocument } from '../../schemas/result'; import { API_DEFAULT_ERROR_MESSAGE } from '../../translations'; import type { DataQualityDashboardRequestHandlerContext } from '../../types'; import { API_RESULTS_INDEX_NOT_AVAILABLE } from './translations'; import { getAuthorizedIndexNames } from '../../helpers/get_authorized_index_names'; +import { getRangeFilteredIndices } from '../../helpers/get_range_filtered_indices'; export const getQuery = (indexName: string[]) => ({ size: 0, @@ -53,6 +53,7 @@ export const getIndexResultsLatestRoute = ( validate: { request: { params: buildRouteValidation(GetIndexResultsLatestParams), + query: buildRouteValidation(GetIndexResultsLatestQuery), }, }, }, @@ -81,8 +82,27 @@ export const getIndexResultsLatestRoute = ( return response.ok({ body: [] }); } + const { startDate, endDate } = request.query; + + let resultingIndices: string[] = []; + + if (startDate && endDate) { + resultingIndices = resultingIndices.concat( + await getRangeFilteredIndices({ + client, + authorizedIndexNames, + startDate, + endDate, + logger, + pattern, + }) + ); + } else { + resultingIndices = authorizedIndexNames; + } + // Get the latest result for each indexName - const query = { index, ...getQuery(authorizedIndexNames) }; + const query = { index, ...getQuery(resultingIndices) }; const { aggregations } = await client.asInternalUser.search< ResultDocument, Record diff --git a/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/result.ts b/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/result.ts index 8ccb3fbc3f984..fb264fe10da8f 100644 --- a/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/result.ts +++ b/x-pack/plugins/ecs_data_quality_dashboard/server/schemas/result.ts @@ -69,6 +69,11 @@ export const PostIndexResultBody = ResultDocument; export const GetIndexResultsLatestParams = t.type({ pattern: t.string }); export type GetIndexResultsLatestParams = t.TypeOf; +export const GetIndexResultsLatestQuery = t.partial({ + startDate: t.string, + endDate: t.string, +}); + export const GetIndexResultsParams = t.type({ pattern: t.string, }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx index e39e2abd24169..8b14fff8082c5 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx @@ -8,6 +8,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import type { HttpFetchOptions } from '@kbn/core-http-browser'; import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; import { TestProviders } from '../../common/mock'; @@ -22,7 +23,17 @@ jest.mock('../../common/lib/kibana', () => { const mockKibanaServices = { get: () => ({ - http: { fetch: jest.fn() }, + http: { + fetch: jest.fn().mockImplementation((path: string, options: HttpFetchOptions) => { + if ( + path.startsWith('/internal/ecs_data_quality_dashboard/results_latest') && + options.method === 'GET' + ) { + return Promise.resolve([]); + } + return Promise.resolve(); + }), + }, }), }; diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx index 37fc927094993..67dcc3848f02a 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx @@ -171,6 +171,8 @@ const DataQualityComponent: React.FC = () => { startDate={startDate} theme={theme} toasts={toasts} + defaultStartTime={DEFAULT_START_TIME} + defaultEndTime={DEFAULT_END_TIME} /> ) : ( From afb795de1a49b19277d422eeb73b109e586ba57f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:15:15 +1100 Subject: [PATCH 47/47] [8.x] [Synthetics] Handle private locations simultaneous edits !! (#195874) (#199387) # Backport This will backport the following commits from `main` to `8.x`: - [[Synthetics] Handle private locations simultaneous edits !! (#195874)](https://github.com/elastic/kibana/pull/195874) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Shahzad --- .../current_fields.json | 1 + .../current_mappings.json | 4 + .../kbn_client/kbn_client_saved_objects.ts | 1 + .../check_registered_types.test.ts | 1 + .../group3/type_registrations.test.ts | 1 + .../common/saved_objects/private_locations.ts | 6 +- .../journeys/private_locations.journey.ts | 19 ++- .../journeys/services/add_monitor.ts | 26 +--- .../journeys/services/synthetics_services.ts | 13 +- .../synthetics/server/feature.ts | 10 +- .../routes/monitor_cruds/edit_monitor.test.ts | 10 ++ .../private_locations/add_private_location.ts | 31 ++--- .../delete_private_location.ts | 30 ++--- .../get_private_locations.ts | 12 +- .../private_locations/helpers.test.ts | 6 +- .../settings/private_locations/helpers.ts | 16 +++ .../migrate_legacy_private_locations.test.ts | 119 ++++++++++++++++++ .../migrate_legacy_private_locations.ts | 70 +++++++++++ .../get_service_locations.ts | 4 +- .../private_locations/model_version_1.test.ts | 4 +- .../server/saved_objects/private_locations.ts | 30 ++++- .../server/saved_objects/saved_objects.ts | 8 +- .../synthetics_service/get_all_locations.ts | 7 +- .../get_private_locations.ts | 50 ++++++-- .../private_location_test_service.ts | 83 ------------ .../synthetics/synthetics_rule_helper.ts | 7 +- .../add_monitor_private_location.ts | 89 +++---------- .../apis/synthetics/add_monitor_project.ts | 7 +- .../add_monitor_project_private_location.ts | 8 +- .../apis/synthetics/delete_monitor.ts | 6 +- .../apis/synthetics/delete_monitor_project.ts | 7 +- .../apis/synthetics/edit_monitor.ts | 6 +- .../synthetics/edit_monitor_public_api.ts | 2 +- .../apis/synthetics/get_monitor_project.ts | 8 +- .../api_integration/apis/synthetics/index.ts | 1 + .../apis/synthetics/inspect_monitor.ts | 2 +- .../apis/synthetics/private_location_apis.ts | 64 ++++++++++ .../services/private_location_test_service.ts | 89 +++++++++---- .../synthetics_monitor_test_service.ts | 2 +- .../apis/synthetics/sync_global_params.ts | 10 +- .../apis/synthetics/synthetics_enablement.ts | 9 ++ .../platform_security/authorization.ts | 24 ++++ 42 files changed, 576 insertions(+), 327 deletions(-) create mode 100644 x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts create mode 100644 x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts delete mode 100644 x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts create mode 100644 x-pack/test/api_integration/apis/synthetics/private_location_apis.ts diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 5dabd353a8a9a..5ca2fc677b167 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -1072,6 +1072,7 @@ "urls" ], "synthetics-param": [], + "synthetics-private-location": [], "synthetics-privates-locations": [], "tag": [ "color", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 414faf88b304f..1e9ff6ac20c79 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -3549,6 +3549,10 @@ "dynamic": false, "properties": {} }, + "synthetics-private-location": { + "dynamic": false, + "properties": {} + }, "synthetics-privates-locations": { "dynamic": false, "properties": {} diff --git a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index d5483f1fe0f9f..0b6ba0be80fab 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -109,6 +109,7 @@ const STANDARD_LIST_TYPES = [ 'synthetics-monitor', 'uptime-dynamic-settings', 'synthetics-privates-locations', + 'synthetics-private-location', 'osquery-saved-query', 'osquery-pack', diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index aedc9e3364c8f..a25e77491c0bc 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -165,6 +165,7 @@ describe('checking migration metadata changes on all registered SO types', () => "synthetics-dynamic-settings": "4b40a93eb3e222619bf4e7fe34a9b9e7ab91a0a7", "synthetics-monitor": "5ceb25b6249bd26902c9b34273c71c3dce06dbea", "synthetics-param": "3ebb744e5571de678b1312d5c418c8188002cf5e", + "synthetics-private-location": "8cecc9e4f39637d2f8244eb7985c0690ceab24be", "synthetics-privates-locations": "f53d799d5c9bc8454aaa32c6abc99a899b025d5c", "tag": "e2544392fe6563e215bb677abc8b01c2601ef2dc", "task": "3c89a7c918d5b896a5f8800f06e9114ad7e7aea3", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index e95a82e63d0ff..ba06073e454a9 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -139,6 +139,7 @@ const previouslyRegisteredTypes = [ 'synthetics-monitor', 'synthetics-param', 'synthetics-privates-locations', + 'synthetics-private-location', 'tag', 'task', 'telemetry', diff --git a/x-pack/plugins/observability_solution/synthetics/common/saved_objects/private_locations.ts b/x-pack/plugins/observability_solution/synthetics/common/saved_objects/private_locations.ts index bb3639e816059..1b5bb92dd7d88 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/saved_objects/private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/saved_objects/private_locations.ts @@ -5,5 +5,7 @@ * 2.0. */ -export const privateLocationsSavedObjectId = 'synthetics-privates-locations-singleton'; -export const privateLocationsSavedObjectName = 'synthetics-privates-locations'; +export const legacyPrivateLocationsSavedObjectId = 'synthetics-privates-locations-singleton'; +export const legacyPrivateLocationsSavedObjectName = 'synthetics-privates-locations'; + +export const privateLocationSavedObjectName = 'synthetics-private-location'; diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/private_locations.journey.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/private_locations.journey.ts index 9e6bb8352c35f..cdc5961991579 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/private_locations.journey.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/private_locations.journey.ts @@ -7,17 +7,14 @@ import { journey, step, before, after, expect } from '@elastic/synthetics'; import { waitForLoadingToFinish } from '@kbn/ux-plugin/e2e/journeys/utils'; +import { SyntheticsServices } from './services/synthetics_services'; import { byTestId } from '../../helpers/utils'; -import { - addTestMonitor, - cleanPrivateLocations, - cleanTestMonitors, - getPrivateLocations, -} from './services/add_monitor'; +import { addTestMonitor, cleanPrivateLocations, cleanTestMonitors } from './services/add_monitor'; import { syntheticsAppPageProvider } from '../page_objects/synthetics_app'; journey(`PrivateLocationsSettings`, async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params }); + const services = new SyntheticsServices(params); page.setDefaultTimeout(2 * 30000); @@ -78,16 +75,14 @@ journey(`PrivateLocationsSettings`, async ({ page, params }) => { await page.click('text=Private Locations'); await page.click('h1:has-text("Settings")'); - const privateLocations = await getPrivateLocations(params); + const privateLocations = await services.getPrivateLocations(); - const locations = privateLocations.attributes.locations; + expect(privateLocations.length).toBe(1); - expect(locations.length).toBe(1); - - locationId = locations[0].id; + locationId = privateLocations[0].id; await addTestMonitor(params.kibanaUrl, 'test-monitor', { - locations: [locations[0]], + locations: [privateLocations[0]], type: 'browser', }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor.ts index 6384179a71bb9..6a527da275eb3 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor.ts @@ -7,10 +7,7 @@ import axios from 'axios'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; -import { - privateLocationsSavedObjectId, - privateLocationsSavedObjectName, -} from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; +import { legacyPrivateLocationsSavedObjectName } from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; export const enableMonitorManagedViaApi = async (kibanaUrl: string) => { try { @@ -46,21 +43,6 @@ export const addTestMonitor = async ( } }; -export const getPrivateLocations = async (params: Record) => { - const getService = params.getService; - const server = getService('kibanaServer'); - - try { - return await server.savedObjects.get({ - id: privateLocationsSavedObjectId, - type: privateLocationsSavedObjectName, - }); - } catch (e) { - // eslint-disable-next-line no-console - console.log(e); - } -}; - export const cleanTestMonitors = async (params: Record) => { const getService = params.getService; const server = getService('kibanaServer'); @@ -79,7 +61,11 @@ export const cleanPrivateLocations = async (params: Record) => { try { await server.savedObjects.clean({ - types: [privateLocationsSavedObjectName, 'ingest-agent-policies', 'ingest-package-policies'], + types: [ + legacyPrivateLocationsSavedObjectName, + 'ingest-agent-policies', + 'ingest-package-policies', + ], }); } catch (e) { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts index 5c356492f1c24..507efe52c453f 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts @@ -10,7 +10,10 @@ import type { Client } from '@elastic/elasticsearch'; import { KbnClient } from '@kbn/test'; import pMap from 'p-map'; import { makeDownSummary, makeUpSummary } from '@kbn/observability-synthetics-test-data'; -import { SyntheticsMonitor } from '@kbn/synthetics-plugin/common/runtime_types'; +import { + SyntheticsMonitor, + SyntheticsPrivateLocations, +} from '@kbn/synthetics-plugin/common/runtime_types'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { journeyStart, journeySummary, step1, step2 } from './data/browser_docs'; @@ -251,4 +254,12 @@ export class SyntheticsServices { }); return connector.data; } + + async getPrivateLocations(): Promise { + const response = await this.requester.request({ + path: SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, + method: 'GET', + }); + return response.data as SyntheticsPrivateLocations; + } } diff --git a/x-pack/plugins/observability_solution/synthetics/server/feature.ts b/x-pack/plugins/observability_solution/synthetics/server/feature.ts index c8b4b721a9ce1..bf86ac7b0c890 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/feature.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/feature.ts @@ -14,7 +14,10 @@ import { import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects'; import { SYNTHETICS_RULE_TYPES } from '../common/constants/synthetics_alerts'; -import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations'; +import { + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, +} from '../common/saved_objects/private_locations'; import { PLUGIN } from '../common/constants/plugin'; import { syntheticsSettingsObjectType, @@ -71,7 +74,8 @@ export const syntheticsFeature = { syntheticsSettingsObjectType, syntheticsMonitorType, syntheticsApiKeyObjectType, - privateLocationsSavedObjectName, + privateLocationSavedObjectName, + legacyPrivateLocationsSavedObjectName, syntheticsParamType, // uptime settings object is also registered here since feature is shared between synthetics and uptime uptimeSettingsObjectType, @@ -102,7 +106,7 @@ export const syntheticsFeature = { syntheticsSettingsObjectType, syntheticsMonitorType, syntheticsApiKeyObjectType, - privateLocationsSavedObjectName, + legacyPrivateLocationsSavedObjectName, // uptime settings object is also registered here since feature is shared between synthetics and uptime uptimeSettingsObjectType, ], diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts index 637c0fc5c6193..cb50708c04eca 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts @@ -33,6 +33,16 @@ describe('syncEditedMonitor', () => { bulkUpdate: jest.fn(), get: jest.fn(), update: jest.fn(), + createPointInTimeFinder: jest.fn().mockImplementation(({ perPage, type: soType }) => ({ + close: jest.fn(async () => {}), + find: jest.fn().mockReturnValue({ + async *[Symbol.asyncIterator]() { + yield { + saved_objects: [], + }; + }, + }), + })), }, logger, config: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts index ac6eff7dea90d..1feb120b2ea14 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts @@ -6,14 +6,12 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { getPrivateLocationsAndAgentPolicies } from './get_private_locations'; -import { - privateLocationsSavedObjectId, - privateLocationsSavedObjectName, -} from '../../../../common/saved_objects/private_locations'; +import { privateLocationSavedObjectName } from '../../../../common/saved_objects/private_locations'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; -import type { SyntheticsPrivateLocationsAttributes } from '../../../runtime_types/private_locations'; +import { PrivateLocationAttributes } from '../../../runtime_types/private_locations'; import { toClientContract, toSavedObjectContract } from './helpers'; import { PrivateLocation } from '../../../../common/runtime_types'; @@ -40,7 +38,11 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory { + handler: async (routeContext) => { + await migrateLegacyPrivateLocations(routeContext); + + const { response, request, savedObjectsClient, syntheticsMonitorClient } = routeContext; + const location = request.body as PrivateLocationObject; const { locations, agentPolicies } = await getPrivateLocationsAndAgentPolicies( @@ -65,7 +67,6 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory loc.id !== location.agentPolicyId); const formattedLocation = toSavedObjectContract({ ...location, id: location.agentPolicyId, @@ -80,17 +81,17 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory( - privateLocationsSavedObjectName, - { locations: [...existingLocations, formattedLocation] }, + const soClient = routeContext.server.coreStart.savedObjects.createInternalRepository(); + + const result = await soClient.create( + privateLocationSavedObjectName, + formattedLocation, { - id: privateLocationsSavedObjectId, - overwrite: true, + id: location.agentPolicyId, + initialNamespaces: ['*'], } ); - const allLocations = toClientContract(result.attributes, agentPolicies); - - return allLocations.find((loc) => loc.id === location.agentPolicyId)!; + return toClientContract(result.attributes, agentPolicies); }, }); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts index 1c6ede5a2ad00..bac3907eac871 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts @@ -7,15 +7,12 @@ import { schema } from '@kbn/config-schema'; import { isEmpty } from 'lodash'; +import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { getMonitorsByLocation } from './get_location_monitors'; import { getPrivateLocationsAndAgentPolicies } from './get_private_locations'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; -import { - privateLocationsSavedObjectId, - privateLocationsSavedObjectName, -} from '../../../../common/saved_objects/private_locations'; -import type { SyntheticsPrivateLocationsAttributes } from '../../../runtime_types/private_locations'; +import { privateLocationSavedObjectName } from '../../../../common/saved_objects/private_locations'; export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'DELETE', @@ -28,12 +25,16 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory { + handler: async (routeContext) => { + await migrateLegacyPrivateLocations(routeContext); + + const { savedObjectsClient, syntheticsMonitorClient, request, response, server } = routeContext; const { locationId } = request.params as { locationId: string }; const { locations } = await getPrivateLocationsAndAgentPolicies( savedObjectsClient, - syntheticsMonitorClient + syntheticsMonitorClient, + true ); if (!locations.find((loc) => loc.id === locationId)) { @@ -55,17 +56,8 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory loc.id !== locationId); - - await savedObjectsClient.create( - privateLocationsSavedObjectName, - { locations: remainingLocations }, - { - id: privateLocationsSavedObjectId, - overwrite: true, - } - ); - - return; + await savedObjectsClient.delete(privateLocationSavedObjectName, locationId, { + force: true, + }); }, }); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts index f7adc1e7ac16e..d884bba5c2b0a 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts @@ -7,6 +7,7 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { schema } from '@kbn/config-schema'; +import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { AgentPolicyInfo } from '../../../../common/types'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../common/runtime_types'; @@ -14,7 +15,7 @@ import { SYNTHETICS_API_URLS } from '../../../../common/constants'; import { getPrivateLocations } from '../../../synthetics_service/get_private_locations'; import type { SyntheticsPrivateLocationsAttributes } from '../../../runtime_types/private_locations'; import { SyntheticsMonitorClient } from '../../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; -import { toClientContract } from './helpers'; +import { allLocationsToClientContract } from './helpers'; export const getPrivateLocationsRoute: SyntheticsRestApiRouteFactory< SyntheticsPrivateLocations | PrivateLocation @@ -29,14 +30,17 @@ export const getPrivateLocationsRoute: SyntheticsRestApiRouteFactory< }), }, }, - handler: async ({ savedObjectsClient, syntheticsMonitorClient, request, response }) => { + handler: async (routeContext) => { + await migrateLegacyPrivateLocations(routeContext); + + const { savedObjectsClient, syntheticsMonitorClient, request, response } = routeContext; const { id } = request.params as { id?: string }; const { locations, agentPolicies } = await getPrivateLocationsAndAgentPolicies( savedObjectsClient, syntheticsMonitorClient ); - const list = toClientContract({ locations }, agentPolicies); + const list = allLocationsToClientContract({ locations }, agentPolicies); if (!id) return list; const location = list.find((loc) => loc.id === id || loc.label === id); if (!location) { @@ -53,7 +57,7 @@ export const getPrivateLocationsRoute: SyntheticsRestApiRouteFactory< export const getPrivateLocationsAndAgentPolicies = async ( savedObjectsClient: SavedObjectsClientContract, syntheticsMonitorClient: SyntheticsMonitorClient, - excludeAgentPolicies: boolean = false + excludeAgentPolicies = false ): Promise => { try { const [privateLocations, agentPolicies] = await Promise.all([ diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.test.ts index 6055b217f8794..84c531cb9ce70 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { toClientContract } from './helpers'; +import { allLocationsToClientContract } from './helpers'; const testLocations = { locations: [ @@ -56,7 +56,7 @@ const testLocations2 = { describe('toClientContract', () => { it('formats SO attributes to client contract with falsy geo location', () => { // @ts-ignore fixtures are purposely wrong types for testing - expect(toClientContract(testLocations)).toEqual([ + expect(allLocationsToClientContract(testLocations)).toEqual([ { agentPolicyId: 'e3134290-0f73-11ee-ba15-159f4f728deb', geo: { @@ -86,7 +86,7 @@ describe('toClientContract', () => { it('formats SO attributes to client contract with truthy geo location', () => { // @ts-ignore fixtures are purposely wrong types for testing - expect(toClientContract(testLocations2)).toEqual([ + expect(allLocationsToClientContract(testLocations2)).toEqual([ { agentPolicyId: 'e3134290-0f73-11ee-ba15-159f4f728deb', geo: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.ts index 1c6c03067a817..8df065ad3e48d 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/helpers.ts @@ -13,6 +13,22 @@ import type { import { PrivateLocation } from '../../../../common/runtime_types'; export const toClientContract = ( + location: PrivateLocationAttributes, + agentPolicies?: AgentPolicyInfo[] +): PrivateLocation => { + const agPolicy = agentPolicies?.find((policy) => policy.id === location.agentPolicyId); + return { + label: location.label, + id: location.id, + agentPolicyId: location.agentPolicyId, + isServiceManaged: false, + isInvalid: !Boolean(agPolicy), + tags: location.tags, + geo: location.geo, + }; +}; + +export const allLocationsToClientContract = ( attributes: SyntheticsPrivateLocationsAttributes, agentPolicies?: AgentPolicyInfo[] ): SyntheticsPrivateLocations => { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts new file mode 100644 index 0000000000000..2305853aab3f1 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; +import { SyntheticsServerSetup } from '../../../types'; +import { coreMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { + type ISavedObjectsRepository, + SavedObjectsClientContract, +} from '@kbn/core-saved-objects-api-server'; + +describe('migrateLegacyPrivateLocations', () => { + let serverMock: SyntheticsServerSetup; + let savedObjectsClient: jest.Mocked; + let repositoryMock: ISavedObjectsRepository; + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + serverMock = { + coreStart: coreStartMock, + logger: loggerMock.create(), + } as any; + savedObjectsClient = savedObjectsClientMock.create(); + repositoryMock = coreMock.createStart().savedObjects.createInternalRepository(); + + coreStartMock.savedObjects.createInternalRepository.mockReturnValue(repositoryMock); + }); + + it('should get the legacy private locations', async () => { + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { locations: [{ id: '1', label: 'Location 1' }] }, + } as any); + savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient, + } as any); + + expect(savedObjectsClient.get).toHaveBeenCalledWith( + 'synthetics-privates-locations', + 'synthetics-privates-locations-singleton' + ); + }); + + it('should log and return if an error occurs while getting legacy private locations', async () => { + const error = new Error('Get error'); + savedObjectsClient.get.mockRejectedValueOnce(error); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient, + } as any); + + expect(serverMock.logger.error).toHaveBeenCalledWith( + `Error getting legacy private locations: ${error}` + ); + expect(repositoryMock.bulkCreate).not.toHaveBeenCalled(); + }); + + it('should return if there are no legacy locations', async () => { + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { locations: [] }, + } as any); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient: savedObjectsClientMock, + } as any); + + expect(repositoryMock.bulkCreate).not.toHaveBeenCalled(); + }); + + it('should bulk create new private locations if there are legacy locations', async () => { + const legacyLocations = [{ id: '1', label: 'Location 1' }]; + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { locations: legacyLocations }, + } as any); + savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient, + } as any); + + expect(repositoryMock.bulkCreate).toHaveBeenCalledWith( + legacyLocations.map((location) => ({ + id: location.id, + attributes: location, + type: 'synthetics-private-location', + initialNamespaces: ['*'], + })), + { overwrite: true } + ); + }); + + it('should delete legacy private locations if bulk create count matches', async () => { + const legacyLocations = [{ id: '1', label: 'Location 1' }]; + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { locations: legacyLocations }, + } as any); + savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + + await migrateLegacyPrivateLocations({ + server: serverMock, + savedObjectsClient, + } as any); + + expect(savedObjectsClient.delete).toHaveBeenCalledWith( + 'synthetics-privates-locations', + 'synthetics-privates-locations-singleton', + {} + ); + }); +}); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts new file mode 100644 index 0000000000000..cd73e27b950e3 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject } from '@kbn/core-saved-objects-server'; +import { + type PrivateLocationAttributes, + SyntheticsPrivateLocationsAttributes, +} from '../../../runtime_types/private_locations'; +import { + legacyPrivateLocationsSavedObjectId, + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, +} from '../../../../common/saved_objects/private_locations'; +import { RouteContext } from '../../types'; + +export const migrateLegacyPrivateLocations = async ({ + server, + savedObjectsClient, +}: RouteContext) => { + try { + let obj: SavedObject | undefined; + try { + obj = await savedObjectsClient.get( + legacyPrivateLocationsSavedObjectName, + legacyPrivateLocationsSavedObjectId + ); + } catch (e) { + server.logger.error(`Error getting legacy private locations: ${e}`); + return; + } + const legacyLocations = obj?.attributes.locations ?? []; + if (legacyLocations.length === 0) { + return; + } + + const soClient = server.coreStart.savedObjects.createInternalRepository(); + + await soClient.bulkCreate( + legacyLocations.map((location) => ({ + id: location.id, + attributes: location, + type: privateLocationSavedObjectName, + initialNamespaces: ['*'], + })), + { + overwrite: true, + } + ); + + const { total } = await savedObjectsClient.find({ + type: privateLocationSavedObjectName, + fields: [], + perPage: 0, + }); + + if (total === legacyLocations.length) { + await savedObjectsClient.delete( + legacyPrivateLocationsSavedObjectName, + legacyPrivateLocationsSavedObjectId, + {} + ); + } + } catch (e) { + server.logger.error(`Error migrating legacy private locations: ${e}`); + } +}; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/get_service_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/get_service_locations.ts index a9142170c9e26..ca704cdff1b28 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/get_service_locations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { toClientContract } from '../settings/private_locations/helpers'; +import { allLocationsToClientContract } from '../settings/private_locations/helpers'; import { getPrivateLocationsAndAgentPolicies } from '../settings/private_locations/get_private_locations'; import { SyntheticsRestApiRouteFactory } from '../types'; import { getAllLocations } from '../../synthetics_service/get_all_locations'; @@ -45,7 +45,7 @@ export const getServiceLocationsRoute: SyntheticsRestApiRouteFactory = () => ({ const { locations: privateLocations, agentPolicies } = await getPrivateLocationsAndAgentPolicies(savedObjectsClient, syntheticsMonitorClient); - const result = toClientContract({ locations: privateLocations }, agentPolicies); + const result = allLocationsToClientContract({ locations: privateLocations }, agentPolicies); return { locations: result, }; diff --git a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/migrations/private_locations/model_version_1.test.ts b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/migrations/private_locations/model_version_1.test.ts index 63a9f940143a4..dbcdea546a9f8 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/migrations/private_locations/model_version_1.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/migrations/private_locations/model_version_1.test.ts @@ -5,7 +5,7 @@ * 2.0. */ import { transformGeoProperty } from './model_version_1'; -import { privateLocationsSavedObjectName } from '../../../../common/saved_objects/private_locations'; +import { legacyPrivateLocationsSavedObjectName } from '../../../../common/saved_objects/private_locations'; describe('model version 1 migration', () => { const testLocation = { @@ -19,7 +19,7 @@ describe('model version 1 migration', () => { concurrentMonitors: 1, }; const testObject = { - type: privateLocationsSavedObjectName, + type: legacyPrivateLocationsSavedObjectName, id: 'synthetics-privates-locations-singleton', attributes: { locations: [testLocation], diff --git a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/private_locations.ts index ee7426ead23af..370c8d203dff6 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/private_locations.ts @@ -7,11 +7,33 @@ import { SavedObjectsType } from '@kbn/core/server'; import { modelVersion1 } from './migrations/private_locations/model_version_1'; -import { privateLocationsSavedObjectName } from '../../common/saved_objects/private_locations'; -export const privateLocationsSavedObjectId = 'synthetics-privates-locations-singleton'; +import { + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, +} from '../../common/saved_objects/private_locations'; -export const PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE: SavedObjectsType = { - name: privateLocationsSavedObjectName, +export const PRIVATE_LOCATION_SAVED_OBJECT_TYPE: SavedObjectsType = { + name: privateLocationSavedObjectName, + hidden: false, + namespaceType: 'multiple', + mappings: { + dynamic: false, + properties: { + /* Leaving these commented to make it clear that these fields exist, even though we don't want them indexed. + When adding new fields please add them here. If they need to be searchable put them in the uncommented + part of properties. + */ + }, + }, + management: { + importableAndExportable: true, + }, +}; + +export const legacyPrivateLocationsSavedObjectId = 'synthetics-privates-locations-singleton'; + +export const LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE: SavedObjectsType = { + name: legacyPrivateLocationsSavedObjectName, hidden: false, namespaceType: 'agnostic', mappings: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts index 9b4a365941a7d..d59ecb507166b 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/saved_objects/saved_objects.ts @@ -24,7 +24,10 @@ import { SYNTHETICS_SECRET_ENCRYPTED_TYPE, syntheticsParamSavedObjectType, } from './synthetics_param'; -import { PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE } from './private_locations'; +import { + LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE, + PRIVATE_LOCATION_SAVED_OBJECT_TYPE, +} from './private_locations'; import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../constants/settings'; import { DynamicSettingsAttributes } from '../runtime_types/settings'; import { @@ -37,7 +40,8 @@ export const registerSyntheticsSavedObjects = ( savedObjectsService: SavedObjectsServiceSetup, encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ) => { - savedObjectsService.registerType(PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE); + savedObjectsService.registerType(LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE); + savedObjectsService.registerType(PRIVATE_LOCATION_SAVED_OBJECT_TYPE); savedObjectsService.registerType(getSyntheticsMonitorSavedObjectType(encryptedSavedObjects)); savedObjectsService.registerType(syntheticsServiceApiKey); diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_all_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_all_locations.ts index c24b28c00ca99..0d8355cebc1f6 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_all_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_all_locations.ts @@ -5,7 +5,7 @@ * 2.0. */ import { SavedObjectsClientContract } from '@kbn/core/server'; -import { toClientContract } from '../routes/settings/private_locations/helpers'; +import { allLocationsToClientContract } from '../routes/settings/private_locations/helpers'; import { getPrivateLocationsAndAgentPolicies } from '../routes/settings/private_locations/get_private_locations'; import { SyntheticsServerSetup } from '../types'; import { getServiceLocations } from './get_service_locations'; @@ -34,7 +34,10 @@ export async function getAllLocations({ ), getServicePublicLocations(server, syntheticsMonitorClient), ]); - const pvtLocations = toClientContract({ locations: privateLocations }, agentPolicies); + const pvtLocations = allLocationsToClientContract( + { locations: privateLocations }, + agentPolicies + ); return { publicLocations, privateLocations: pvtLocations, diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_private_locations.ts index a850cbf081e68..a476df9dfe038 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_private_locations.ts @@ -5,22 +5,42 @@ * 2.0. */ -import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server'; import { - privateLocationsSavedObjectId, - privateLocationsSavedObjectName, + SavedObject, + SavedObjectsClientContract, + SavedObjectsErrorHelpers, +} from '@kbn/core/server'; +import { uniqBy } from 'lodash'; +import { + legacyPrivateLocationsSavedObjectId, + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, } from '../../common/saved_objects/private_locations'; -import type { SyntheticsPrivateLocationsAttributes } from '../runtime_types/private_locations'; +import { + PrivateLocationAttributes, + SyntheticsPrivateLocationsAttributes, +} from '../runtime_types/private_locations'; export const getPrivateLocations = async ( client: SavedObjectsClientContract ): Promise => { try { - const obj = await client.get( - privateLocationsSavedObjectName, - privateLocationsSavedObjectId - ); - return obj?.attributes.locations ?? []; + const finder = client.createPointInTimeFinder({ + type: privateLocationSavedObjectName, + perPage: 1000, + }); + + const results: Array> = []; + + for await (const response of finder.find()) { + results.push(...response.saved_objects); + } + + finder.close().catch((e) => {}); + + const legacyLocations = await getLegacyPrivateLocations(client); + + return uniqBy([...results.map((r) => r.attributes), ...legacyLocations], 'id'); } catch (getErr) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { return []; @@ -28,3 +48,15 @@ export const getPrivateLocations = async ( throw getErr; } }; + +const getLegacyPrivateLocations = async (client: SavedObjectsClientContract) => { + try { + const obj = await client.get( + legacyPrivateLocationsSavedObjectName, + legacyPrivateLocationsSavedObjectId + ); + return obj?.attributes.locations ?? []; + } catch (getErr) { + return []; + } +}; diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts b/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts deleted file mode 100644 index e9ac7237dca52..0000000000000 --- a/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { v4 as uuidv4 } from 'uuid'; -import { privateLocationsSavedObjectName } from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; -import { privateLocationsSavedObjectId } from '@kbn/synthetics-plugin/server/saved_objects/private_locations'; -import { SyntheticsPrivateLocations } from '@kbn/synthetics-plugin/common/runtime_types'; -import { Agent as SuperTestAgent } from 'supertest'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export const INSTALLED_VERSION = '1.1.1'; - -export class PrivateLocationTestService { - private supertest: SuperTestAgent; - private readonly getService: FtrProviderContext['getService']; - - constructor(getService: FtrProviderContext['getService']) { - this.supertest = getService('supertest'); - this.getService = getService; - } - - async installSyntheticsPackage() { - await this.supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); - const response = await this.supertest - .get(`/api/fleet/epm/packages/synthetics/${INSTALLED_VERSION}`) - .set('kbn-xsrf', 'true') - .expect(200); - if (response.body.item.status !== 'installed') { - await this.supertest - .post(`/api/fleet/epm/packages/synthetics/${INSTALLED_VERSION}`) - .set('kbn-xsrf', 'true') - .send({ force: true }) - .expect(200); - } - } - - async addTestPrivateLocation() { - const apiResponse = await this.addFleetPolicy(uuidv4()); - const testPolicyId = apiResponse.body.item.id; - return (await this.setTestLocations([testPolicyId]))[0]; - } - - async addFleetPolicy(name: string) { - return this.supertest - .post('/api/fleet/agent_policies?sys_monitoring=true') - .set('kbn-xsrf', 'true') - .send({ - name, - description: '', - namespace: 'default', - monitoring_enabled: [], - }) - .expect(200); - } - - async setTestLocations(testFleetPolicyIds: string[]) { - const server = this.getService('kibanaServer'); - - const locations: SyntheticsPrivateLocations = testFleetPolicyIds.map((id, index) => ({ - label: 'Test private location ' + index, - agentPolicyId: id, - id, - geo: { - lat: 0, - lon: 0, - }, - isServiceManaged: false, - })); - - await server.savedObjects.create({ - type: privateLocationsSavedObjectName, - id: privateLocationsSavedObjectId, - attributes: { - locations, - }, - overwrite: true, - }); - return locations; - } -} diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts index 2915cb5ee5d3b..a2da1c849945f 100644 --- a/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts +++ b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts @@ -16,9 +16,9 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { Agent as SuperTestAgent } from 'supertest'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import expect from '@kbn/expect'; +import { PrivateLocationTestService } from '../../../api_integration/apis/synthetics/services/private_location_test_service'; import { waitForAlertInIndex } from '../helpers/alerting_wait_for_helpers'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { PrivateLocationTestService } from './private_location_test_service'; import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; export const SYNTHETICS_ALERT_ACTION_INDEX = 'alert-action-synthetics'; @@ -172,11 +172,6 @@ export class SyntheticsRuleHelper { return result.body as EncryptedSyntheticsSavedMonitor; } - async addPrivateLocation() { - await this.locService.installSyntheticsPackage(); - return this.locService.addTestPrivateLocation(); - } - async waitForStatusAlert({ ruleId, filters, diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts index 051ae14396687..5b0c967601638 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); let testFleetPolicyID: string; + let pvtLoc: PrivateLocation; const testPolicyName = 'Fleet test server policy' + Date.now(); let _httpMonitorJson: HTTPFields; @@ -68,29 +69,15 @@ export default function ({ getService }: FtrProviderContext) { httpMonitorJson = _httpMonitorJson; }); - it('adds a test fleet policy', async () => { - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testFleetPolicyID = apiResponse.body.item.id; - }); - it('add a test private location', async () => { - await testPrivateLocations.setTestLocations([testFleetPolicyID]); + pvtLoc = await testPrivateLocations.addPrivateLocation(); + testFleetPolicyID = pvtLoc.id; const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS); const testResponse: Array = [ ...getDevLocation('mockDevUrl'), - { - id: testFleetPolicyID, - isServiceManaged: false, - isInvalid: false, - label: 'Test private location 0', - geo: { - lat: 0, - lon: 0, - }, - agentPolicyId: testFleetPolicyID, - }, + { ...pvtLoc, isInvalid: false }, ]; expect(apiResponse.body.locations).eql(testResponse); @@ -137,16 +124,7 @@ export default function ({ getService }: FtrProviderContext) { it('adds a monitor in private location', async () => { const newMonitor = httpMonitorJson; - newMonitor.locations.push({ - id: testFleetPolicyID, - agentPolicyId: testFleetPolicyID, - label: 'Test private location 0', - isServiceManaged: false, - geo: { - lat: 0, - lon: 0, - }, - }); + newMonitor.locations.push(pvtLoc); const { body, rawBody } = await addMonitorAPI(newMonitor); @@ -182,19 +160,13 @@ export default function ({ getService }: FtrProviderContext) { const resPolicy = await testPrivateLocations.addFleetPolicy(testPolicyName + 1); testFleetPolicyID2 = resPolicy.body.item.id; - await testPrivateLocations.setTestLocations([testFleetPolicyID, testFleetPolicyID2]); - - httpMonitorJson.locations.push({ - id: testFleetPolicyID2, - agentPolicyId: testFleetPolicyID2, - label: 'Test private location ' + 1, - isServiceManaged: false, - geo: { - lat: 0, - lon: 0, - }, + const pvtLoc2 = await testPrivateLocations.addPrivateLocation({ + policyId: testFleetPolicyID2, + label: 'Test private location 1', }); + httpMonitorJson.locations.push(pvtLoc2); + const apiResponse = await supertestAPI .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId) .set('kbn-xsrf', 'true') @@ -308,54 +280,23 @@ export default function ({ getService }: FtrProviderContext) { }); it('handles spaces', async () => { - const username = 'admin'; - const password = `${username}-password`; - const roleName = 'uptime-role'; - const SPACE_ID = `test-space-${uuidv4()}`; - const SPACE_NAME = `test-space-name ${uuidv4()}`; + const { username, password, SPACE_ID, roleName } = await monitorTestService.addsNewSpace(); + let monitorId = ''; const monitor = { ...httpMonitorJson, name: `Test monitor ${uuidv4()}`, [ConfigKey.NAMESPACE]: 'default', - locations: [ - { - id: testFleetPolicyID, - agentPolicyId: testFleetPolicyID, - label: 'Test private location 0', - isServiceManaged: false, - geo: { - lat: 0, - lon: 0, - }, - }, - ], + locations: [pvtLoc], }; try { - await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); - await security.role.create(roleName, { - kibana: [ - { - feature: { - uptime: ['all'], - actions: ['all'], - }, - spaces: ['*'], - }, - ], - }); - await security.user.create(username, { - password, - roles: [roleName], - full_name: 'a kibana user', - }); const apiResponse = await supertestWithoutAuth .post(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .auth(username, password) .set('kbn-xsrf', 'true') - .send(monitor) - .expect(200); + .send(monitor); + expect(apiResponse.status).eql(200, JSON.stringify(apiResponse.body)); const { created_at: createdAt, updated_at: updatedAt } = apiResponse.body; expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index ea8821901c9e9..5518988ff78c7 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -46,8 +46,6 @@ export default function ({ getService }: FtrProviderContext) { let icmpProjectMonitors: ProjectMonitorsRequest; let testPolicyId = ''; - const testPolicyName = 'Fleet test server policy' + Date.now(); - const setUniqueIds = (request: ProjectMonitorsRequest) => { return { ...request, @@ -87,9 +85,8 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); await testPrivateLocations.installSyntheticsPackage(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + const loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.id; await supertest .post(SYNTHETICS_API_URLS.PARAMS) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts index 8ab44a6615890..c9c6c293d6130 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project_private_location.ts @@ -24,9 +24,6 @@ export default function ({ getService }: FtrProviderContext) { let projectMonitors: ProjectMonitorsRequest; const monitorTestService = new SyntheticsMonitorTestService(getService); - - let testPolicyId = ''; - const testPolicyName = 'Fleet test server policy' + Date.now(); const testPrivateLocations = new PrivateLocationTestService(getService); const setUniqueIds = (request: ProjectMonitorsRequest) => { @@ -42,10 +39,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); await testPrivateLocations.installSyntheticsPackage(); - - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + await testPrivateLocations.addPrivateLocation(); }); after(async () => { diff --git a/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts b/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts index f8781295e8005..4192529654a28 100644 --- a/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts @@ -59,10 +59,8 @@ export default function ({ getService }: FtrProviderContext) { await testPrivateLocations.installSyntheticsPackage(); - const testPolicyName = 'Fleet test server policy' + Date.now(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + const loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.id; }); beforeEach(() => { diff --git a/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts index ecd824a1052e7..0cb982ec90cc6 100644 --- a/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts @@ -37,11 +37,10 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); await testPrivateLocations.installSyntheticsPackage(); - const testPolicyName = 'Fleet test server policy' + Date.now(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + const loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.id; }); beforeEach(() => { diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts index 7c23c4981c9cf..9767d1e447927 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts @@ -79,10 +79,8 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); - const testPolicyName = 'Fleet test server policy' + Date.now(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + const loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.id; }); after(async () => { diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts index e8112627397c4..aeb0eaa0299b3 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts @@ -240,7 +240,7 @@ export default function ({ getService }: FtrProviderContext) { it('can add private location to existing monitor', async () => { await testPrivateLocations.installSyntheticsPackage(); - pvtLoc = await testPrivateLocations.addTestPrivateLocation(); + pvtLoc = await testPrivateLocations.addPrivateLocation(); expect(pvtLoc).not.empty(); diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts index d996d0181df6b..53089d2bec2d3 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor_project.ts @@ -22,13 +22,13 @@ export default function ({ getService }: FtrProviderContext) { this.tags('skipCloud'); const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); let projectMonitors: LegacyProjectMonitorsRequest; let httpProjectMonitors: LegacyProjectMonitorsRequest; let tcpProjectMonitors: LegacyProjectMonitorsRequest; let icmpProjectMonitors: LegacyProjectMonitorsRequest; - let testPolicyId = ''; const testPrivateLocations = new PrivateLocationTestService(getService); const setUniqueIds = (request: LegacyProjectMonitorsRequest) => { @@ -39,12 +39,10 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); await testPrivateLocations.installSyntheticsPackage(); - const testPolicyName = 'Fleet test server policy' + Date.now(); - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testPolicyId = apiResponse.body.item.id; - await testPrivateLocations.setTestLocations([testPolicyId]); + await testPrivateLocations.addPrivateLocation(); }); beforeEach(() => { diff --git a/x-pack/test/api_integration/apis/synthetics/index.ts b/x-pack/test/api_integration/apis/synthetics/index.ts index a8b39893570c2..15e76126e9555 100644 --- a/x-pack/test/api_integration/apis/synthetics/index.ts +++ b/x-pack/test/api_integration/apis/synthetics/index.ts @@ -35,5 +35,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./inspect_monitor')); loadTestFile(require.resolve('./test_now_monitor')); loadTestFile(require.resolve('./suggestions')); + loadTestFile(require.resolve('./private_location_apis')); }); } diff --git a/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts b/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts index 71132965ee520..7889f4ad37dfc 100644 --- a/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts @@ -171,7 +171,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('inspect http monitor in private location', async () => { - const location = await testPrivateLocations.addTestPrivateLocation(); + const location = await testPrivateLocations.addPrivateLocation(); const apiResponse = await monitorTestService.inspectMonitor({ ..._monitors[0], locations: [ diff --git a/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts b/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts new file mode 100644 index 0000000000000..415c91af28347 --- /dev/null +++ b/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { + legacyPrivateLocationsSavedObjectId, + legacyPrivateLocationsSavedObjectName, + privateLocationSavedObjectName, +} from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { PrivateLocationTestService } from './services/private_location_test_service'; + +export default function ({ getService }: FtrProviderContext) { + describe('PrivateLocationAPI', function () { + this.tags('skipCloud'); + + const kServer = getService('kibanaServer'); + + const testPrivateLocations = new PrivateLocationTestService(getService); + + before(async () => { + await testPrivateLocations.installSyntheticsPackage(); + + await kServer.savedObjects.clean({ + types: [legacyPrivateLocationsSavedObjectName, privateLocationSavedObjectName], + }); + }); + + it('adds a test legacy private location', async () => { + const locs = await testPrivateLocations.addLegacyPrivateLocations(); + expect(locs.length).to.be(2); + }); + + it('adds a test private location', async () => { + await testPrivateLocations.addPrivateLocation(); + }); + + it('list all locations', async () => { + const locs = await testPrivateLocations.fetchAll(); + expect(locs.body.length).to.be(3); + }); + + it('migrates to new saved objet type', async () => { + const newData = await kServer.savedObjects.find({ + type: privateLocationSavedObjectName, + }); + + expect(newData.saved_objects.length).to.be(3); + + try { + await kServer.savedObjects.get({ + type: legacyPrivateLocationsSavedObjectName, + id: legacyPrivateLocationsSavedObjectId, + }); + } catch (e) { + expect(e.response.status).to.be(404); + } + }); + }); +} diff --git a/x-pack/test/api_integration/apis/synthetics/services/private_location_test_service.ts b/x-pack/test/api_integration/apis/synthetics/services/private_location_test_service.ts index f08a708358a2f..f923a5dd887c1 100644 --- a/x-pack/test/api_integration/apis/synthetics/services/private_location_test_service.ts +++ b/x-pack/test/api_integration/apis/synthetics/services/private_location_test_service.ts @@ -4,11 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; -import { privateLocationsSavedObjectName } from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; -import { privateLocationsSavedObjectId } from '@kbn/synthetics-plugin/server/saved_objects/private_locations'; -import { SyntheticsPrivateLocations } from '@kbn/synthetics-plugin/common/runtime_types'; +import expect from '@kbn/expect'; +import { PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types'; import { KibanaSupertestProvider } from '@kbn/ftr-common-functional-services'; +import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { + legacyPrivateLocationsSavedObjectId, + legacyPrivateLocationsSavedObjectName, +} from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; import { FtrProviderContext } from '../../../ftr_provider_context'; export const INSTALLED_VERSION = '1.1.1'; @@ -37,18 +40,12 @@ export class PrivateLocationTestService { } } - async addTestPrivateLocation() { - const apiResponse = await this.addFleetPolicy(uuidv4()); - const testPolicyId = apiResponse.body.item.id; - return (await this.setTestLocations([testPolicyId]))[0]; - } - - async addFleetPolicy(name: string) { + async addFleetPolicy(name?: string) { return this.supertest .post('/api/fleet/agent_policies?sys_monitoring=true') .set('kbn-xsrf', 'true') .send({ - name, + name: name ?? 'Fleet test server policy' + Date.now(), description: '', namespace: 'default', monitoring_enabled: [], @@ -56,28 +53,72 @@ export class PrivateLocationTestService { .expect(200); } - async setTestLocations(testFleetPolicyIds: string[]) { - const server = this.getService('kibanaServer'); + async addPrivateLocation({ policyId, label }: { policyId?: string; label?: string } = {}) { + let agentPolicyId = policyId; - const locations: SyntheticsPrivateLocations = testFleetPolicyIds.map((id, index) => ({ - label: 'Test private location ' + index, - agentPolicyId: id, - id, + if (!agentPolicyId) { + const apiResponse = await this.addFleetPolicy(); + agentPolicyId = apiResponse.body.item.id; + } + + const location: Omit = { + label: label ?? 'Test private location 0', + agentPolicyId: agentPolicyId!, geo: { lat: 0, lon: 0, }, - isServiceManaged: false, - })); + }; + + const response = await this.supertest + .post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .set('kbn-xsrf', 'true') + .send(location); + + expect(response.status).to.be(200); + + const { isInvalid, ...loc } = response.body; + + return loc; + } + + async addLegacyPrivateLocations() { + const server = this.getService('kibanaServer'); + const fleetPolicy = await this.addFleetPolicy(); + const fleetPolicy2 = await this.addFleetPolicy(); + + const locs = [ + { + id: fleetPolicy.body.item.id, + agentPolicyId: fleetPolicy.body.item.id, + name: 'Test private location 1', + lat: 0, + lon: 0, + }, + { + id: fleetPolicy2.body.item.id, + agentPolicyId: fleetPolicy2.body.item.id, + name: 'Test private location 2', + lat: 0, + lon: 0, + }, + ]; await server.savedObjects.create({ - type: privateLocationsSavedObjectName, - id: privateLocationsSavedObjectId, + type: legacyPrivateLocationsSavedObjectName, + id: legacyPrivateLocationsSavedObjectId, attributes: { - locations, + locations: locs, }, overwrite: true, }); - return locations; + return locs; + } + + async fetchAll() { + return this.supertest + .get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .set('kbn-xsrf', 'true') + .expect(200); } } diff --git a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts index c0c15024b5401..498da8c6b1800 100644 --- a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts +++ b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts @@ -202,7 +202,7 @@ export class SyntheticsMonitorTestService { full_name: 'a kibana user', }); - return { username, password, SPACE_ID }; + return { username, password, SPACE_ID, roleName }; } async deleteMonitor(monitorId?: string | string[], statusCode = 200, spaceId?: string) { diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts index e0a79a8905ee8..44cd5b19d6697 100644 --- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts @@ -60,15 +60,9 @@ export default function ({ getService }: FtrProviderContext) { httpMonitorJson = _httpMonitorJson; }); - const testPolicyName = 'Fleet test server policy' + Date.now(); - - it('adds a test fleet policy', async () => { - const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); - testFleetPolicyID = apiResponse.body.item.id; - }); - it('add a test private location', async () => { - await testPrivateLocations.setTestLocations([testFleetPolicyID]); + const loc = await testPrivateLocations.addPrivateLocation(); + testFleetPolicyID = loc.id; const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS); diff --git a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts index 0dcb52e348f8d..f6a98bf77e4fe 100644 --- a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts +++ b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts @@ -48,6 +48,15 @@ export default function ({ getService }: FtrProviderContext) { ); }; + before(async () => { + // clean up all api keys + const { body } = await esSupertest.get(`/_security/api_key`).query({ with_limited_by: true }); + const apiKeys = body.api_keys || []; + for (const apiKey of apiKeys) { + await esSupertest.delete(`/_security/api_key`).send({ ids: [apiKey.id] }); + } + }); + describe('[PUT] /internal/uptime/service/enablement', () => { beforeEach(async () => { const apiKeys = await getApiKeys(); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts index d4cca44448676..1e3ca0eecfafe 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts @@ -8728,6 +8728,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:uptime-synthetics-api-key/delete", "saved_object:uptime-synthetics-api-key/bulk_delete", "saved_object:uptime-synthetics-api-key/share_to_space", + "saved_object:synthetics-private-location/bulk_get", + "saved_object:synthetics-private-location/get", + "saved_object:synthetics-private-location/find", + "saved_object:synthetics-private-location/open_point_in_time", + "saved_object:synthetics-private-location/close_point_in_time", + "saved_object:synthetics-private-location/create", + "saved_object:synthetics-private-location/bulk_create", + "saved_object:synthetics-private-location/update", + "saved_object:synthetics-private-location/bulk_update", + "saved_object:synthetics-private-location/delete", + "saved_object:synthetics-private-location/bulk_delete", + "saved_object:synthetics-private-location/share_to_space", "saved_object:synthetics-privates-locations/bulk_get", "saved_object:synthetics-privates-locations/get", "saved_object:synthetics-privates-locations/find", @@ -9417,6 +9429,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:uptime-synthetics-api-key/delete", "saved_object:uptime-synthetics-api-key/bulk_delete", "saved_object:uptime-synthetics-api-key/share_to_space", + "saved_object:synthetics-private-location/bulk_get", + "saved_object:synthetics-private-location/get", + "saved_object:synthetics-private-location/find", + "saved_object:synthetics-private-location/open_point_in_time", + "saved_object:synthetics-private-location/close_point_in_time", + "saved_object:synthetics-private-location/create", + "saved_object:synthetics-private-location/bulk_create", + "saved_object:synthetics-private-location/update", + "saved_object:synthetics-private-location/bulk_update", + "saved_object:synthetics-private-location/delete", + "saved_object:synthetics-private-location/bulk_delete", + "saved_object:synthetics-private-location/share_to_space", "saved_object:synthetics-privates-locations/bulk_get", "saved_object:synthetics-privates-locations/get", "saved_object:synthetics-privates-locations/find",