diff --git a/.buildkite/pipelines/bazel_cache.yml b/.buildkite/pipelines/bazel_cache.yml index daf56eb712a8d1..9aa961bcddbd29 100644 --- a/.buildkite/pipelines/bazel_cache.yml +++ b/.buildkite/pipelines/bazel_cache.yml @@ -1,5 +1,7 @@ steps: - label: ':pipeline: Create pipeline with priority' + agents: + queue: kibana-default concurrency_group: bazel_macos concurrency: 1 concurrency_method: eager diff --git a/.buildkite/pipelines/es_snapshots/promote.yml b/.buildkite/pipelines/es_snapshots/promote.yml index 5a003321246a18..f2f7b423c94c2e 100644 --- a/.buildkite/pipelines/es_snapshots/promote.yml +++ b/.buildkite/pipelines/es_snapshots/promote.yml @@ -10,3 +10,5 @@ steps: required: true - label: Promote Snapshot command: .buildkite/scripts/steps/es_snapshots/promote.sh + agents: + queue: kibana-default diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index f98626ef25c012..58908d1578bb5f 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -14,6 +14,8 @@ steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build timeout_in_minutes: 10 + agents: + queue: kibana-default - wait @@ -85,6 +87,8 @@ steps: - command: .buildkite/scripts/steps/es_snapshots/trigger_promote.sh label: Trigger promotion timeout_in_minutes: 10 + agents: + queue: kibana-default depends_on: - default-cigroup - default-cigroup-docker @@ -98,3 +102,5 @@ steps: - command: .buildkite/scripts/lifecycle/post_build.sh label: Post-Build timeout_in_minutes: 10 + agents: + queue: kibana-default diff --git a/.buildkite/pipelines/flaky_tests/pipeline.js b/.buildkite/pipelines/flaky_tests/pipeline.js index cb5c37bf58348a..b7f93412edb37c 100644 --- a/.buildkite/pipelines/flaky_tests/pipeline.js +++ b/.buildkite/pipelines/flaky_tests/pipeline.js @@ -51,6 +51,9 @@ const pipeline = { { command: '.buildkite/pipelines/flaky_tests/runner.sh', label: 'Create pipeline', + agents: { + queue: 'kibana-default', + }, }, ], }; diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 6953c146050ebc..e5bc841774fde5 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -2,6 +2,8 @@ steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build timeout_in_minutes: 10 + agents: + queue: kibana-default - wait @@ -54,24 +56,44 @@ steps: - command: .buildkite/scripts/steps/functional/oss_accessibility.sh label: 'OSS Accessibility Tests' agents: - queue: ci-group-4d + queue: n2-4-spot depends_on: build timeout_in_minutes: 120 retry: automatic: - - exit_status: '*' + - exit_status: '1' limit: 1 + - exit_status: '-1' + limit: 3 + - exit_status: '130' + limit: 3 + - exit_status: '137' + limit: 3 + - exit_status: '143' + limit: 3 + - exit_status: '255' + limit: 3 - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: n2-4 + queue: n2-4-spot depends_on: build timeout_in_minutes: 120 retry: automatic: - - exit_status: '*' + - exit_status: '1' limit: 1 + - exit_status: '-1' + limit: 3 + - exit_status: '130' + limit: 3 + - exit_status: '137' + limit: 3 + - exit_status: '143' + limit: 3 + - exit_status: '255' + limit: 3 - command: .buildkite/scripts/steps/functional/oss_firefox.sh label: 'OSS Firefox Tests' @@ -87,13 +109,23 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: n2-4 + queue: n2-4-spot depends_on: build timeout_in_minutes: 120 retry: automatic: - - exit_status: '*' + - exit_status: '1' limit: 1 + - exit_status: '-1' + limit: 3 + - exit_status: '130' + limit: 3 + - exit_status: '137' + limit: 3 + - exit_status: '143' + limit: 3 + - exit_status: '255' + limit: 3 - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' @@ -109,13 +141,23 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: n2-4 + queue: n2-4-spot depends_on: build timeout_in_minutes: 120 retry: automatic: - - exit_status: '*' + - exit_status: '1' limit: 1 + - exit_status: '-1' + limit: 3 + - exit_status: '130' + limit: 3 + - exit_status: '137' + limit: 3 + - exit_status: '143' + limit: 3 + - exit_status: '255' + limit: 3 - command: .buildkite/scripts/steps/test/jest.sh label: 'Jest Tests' @@ -136,9 +178,23 @@ steps: - command: .buildkite/scripts/steps/test/api_integration.sh label: 'API Integration Tests' agents: - queue: n2-2 + queue: n2-4-spot timeout_in_minutes: 120 key: api-integration + retry: + automatic: + - exit_status: '1' + limit: 1 + - exit_status: '-1' + limit: 3 + - exit_status: '130' + limit: 3 + - exit_status: '137' + limit: 3 + - exit_status: '143' + limit: 3 + - exit_status: '255' + limit: 3 - command: .buildkite/scripts/steps/lint.sh label: 'Linting' @@ -174,3 +230,5 @@ steps: - command: .buildkite/scripts/lifecycle/post_build.sh label: Post-Build timeout_in_minutes: 10 + agents: + queue: kibana-default diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index e5f6dcc2d1d5fd..c6acb48b3e212e 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -5,6 +5,8 @@ steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build timeout_in_minutes: 10 + agents: + queue: kibana-default - wait @@ -34,3 +36,5 @@ steps: - command: .buildkite/scripts/lifecycle/post_build.sh label: Post-Build timeout_in_minutes: 10 + agents: + queue: kibana-default diff --git a/.buildkite/pipelines/performance/daily.yml b/.buildkite/pipelines/performance/daily.yml index 208456f9c67a52..564bfb5e501b3e 100644 --- a/.buildkite/pipelines/performance/daily.yml +++ b/.buildkite/pipelines/performance/daily.yml @@ -1,25 +1,27 @@ steps: - - block: ":gear: Performance Tests Configuration" - prompt: "Fill out the details for performance test" + - block: ':gear: Performance Tests Configuration' + prompt: 'Fill out the details for performance test' fields: - - text: ":arrows_counterclockwise: Iterations" - key: "performance-test-iteration-count" - hint: "How many times you want to run tests? " + - text: ':arrows_counterclockwise: Iterations' + key: 'performance-test-iteration-count' + hint: 'How many times you want to run tests? ' required: true if: build.env('PERF_TEST_COUNT') == null - - label: ":male-mechanic::skin-tone-2: Pre-Build" + - label: ':male-mechanic::skin-tone-2: Pre-Build' command: .buildkite/scripts/lifecycle/pre_build.sh + agents: + queue: kibana-default - wait - - label: ":factory_worker: Build Kibana Distribution and Plugins" + - label: ':factory_worker: Build Kibana Distribution and Plugins' command: .buildkite/scripts/steps/build_kibana.sh agents: queue: c2-16 key: build - - label: ":muscle: Performance Tests with Playwright config" + - label: ':muscle: Performance Tests with Playwright config' command: .buildkite/scripts/steps/functional/performance_playwright.sh agents: queue: c2-16 @@ -28,6 +30,7 @@ steps: - wait: ~ continue_on_failure: true - - label: ":male_superhero::skin-tone-2: Post-Build" + - label: ':male_superhero::skin-tone-2: Post-Build' command: .buildkite/scripts/lifecycle/post_build.sh - + agents: + queue: kibana-default diff --git a/.buildkite/pipelines/pull_request.yml b/.buildkite/pipelines/pull_request.yml deleted file mode 100644 index 41c13bb403e1a9..00000000000000 --- a/.buildkite/pipelines/pull_request.yml +++ /dev/null @@ -1,17 +0,0 @@ -env: - GITHUB_COMMIT_STATUS_ENABLED: 'true' - GITHUB_COMMIT_STATUS_CONTEXT: 'buildkite/kibana-pull-request' -steps: - - command: .buildkite/scripts/lifecycle/pre_build.sh - label: Pre-Build - - - wait - - - command: echo 'Hello World' - label: Test - - - wait: ~ - continue_on_failure: true - - - command: .buildkite/scripts/lifecycle/post_build.sh - label: Post-Build diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index d832717906bb1b..3117ba98078d92 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -2,6 +2,8 @@ steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build timeout_in_minutes: 10 + agents: + queue: kibana-default - wait diff --git a/.buildkite/pipelines/pull_request/post_build.yml b/.buildkite/pipelines/pull_request/post_build.yml index 4f252bf8abc111..63f7169334584b 100644 --- a/.buildkite/pipelines/pull_request/post_build.yml +++ b/.buildkite/pipelines/pull_request/post_build.yml @@ -4,3 +4,5 @@ steps: - command: .buildkite/scripts/lifecycle/post_build.sh label: Post-Build + agents: + queue: kibana-default diff --git a/.buildkite/pipelines/purge_cloud_deployments.yml b/.buildkite/pipelines/purge_cloud_deployments.yml index 8287abf2ca5a25..9567f67a047f8e 100644 --- a/.buildkite/pipelines/purge_cloud_deployments.yml +++ b/.buildkite/pipelines/purge_cloud_deployments.yml @@ -2,3 +2,5 @@ steps: - command: .buildkite/scripts/steps/cloud/purge.sh label: Purge old cloud deployments timeout_in_minutes: 10 + agents: + queue: kibana-default diff --git a/.buildkite/pipelines/update_demo_env.yml b/.buildkite/pipelines/update_demo_env.yml index e2dfdd782fd413..12c4f296f5dfd0 100644 --- a/.buildkite/pipelines/update_demo_env.yml +++ b/.buildkite/pipelines/update_demo_env.yml @@ -2,6 +2,8 @@ steps: - command: .buildkite/scripts/steps/demo_env/es_and_init.sh label: Initialize Environment and Deploy ES timeout_in_minutes: 10 + agents: + queue: kibana-default - command: .buildkite/scripts/steps/demo_env/kibana.sh label: Build and Deploy Kibana diff --git a/.github/workflows/backport-next.yml b/.github/workflows/backport-next.yml deleted file mode 100644 index 6779bb42472418..00000000000000 --- a/.github/workflows/backport-next.yml +++ /dev/null @@ -1,27 +0,0 @@ -on: - pull_request_target: - branches: - - main - types: - - labeled - - closed - -jobs: - backport: - name: Backport PR - runs-on: ubuntu-latest - if: | - github.event.pull_request.merged == true - && contains(github.event.pull_request.labels.*.name, 'auto-backport-next') - && ( - (github.event.action == 'labeled' && github.event.label.name == 'auto-backport-next') - || (github.event.action == 'closed') - ) - steps: - - name: Backport Action - uses: sqren/backport-github-action@v7.3.1 - with: - github_token: ${{secrets.KIBANAMACHINE_TOKEN}} - - - name: Backport log - run: cat /home/runner/.backport/backport.log diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index d126ea6ec9b388..375854b9c54b71 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -9,6 +9,7 @@ on: jobs: backport: name: Backport PR + runs-on: ubuntu-latest if: | github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-backport') @@ -16,26 +17,11 @@ jobs: (github.event.action == 'labeled' && github.event.label.name == 'auto-backport') || (github.event.action == 'closed') ) - runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v2 - with: - repository: 'elastic/kibana-github-actions' - ref: main - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Fix Version Label Gaps - uses: ./actions/fix-version-gaps + - name: Backport Action + uses: sqren/backport-github-action@v7.4.0 with: github_token: ${{secrets.KIBANAMACHINE_TOKEN}} - - name: Run Backport - uses: ./actions/backport - with: - github_token: ${{secrets.KIBANAMACHINE_TOKEN}} - commit_user: kibanamachine - commit_email: 42973632+kibanamachine@users.noreply.github.com + - name: Backport log + run: cat ~/.backport/backport.log diff --git a/.gitignore b/.gitignore index 818d3a472d52c3..7e451584582380 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,3 @@ fleet-server-* elastic-agent.yml fleet-server.yml -/x-pack/plugins/fleet/server/bundled_packages diff --git a/.i18nrc.json b/.i18nrc.json index 5c362908a18760..7ec704aab3a7ae 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -66,6 +66,7 @@ "uiActions": "src/plugins/ui_actions", "uiActionsExamples": "examples/ui_action_examples", "usageCollection": "src/plugins/usage_collection", + "utils": "packages/kbn-securitysolution-utils/src", "visDefaultEditor": "src/plugins/vis_default_editor", "visTypeHeatmap": "src/plugins/vis_types/heatmap", "visTypeMarkdown": "src/plugins/vis_type_markdown", diff --git a/dev_docs/contributing/best_practices.mdx b/dev_docs/contributing/best_practices.mdx index d7aa42946eac39..0daf068fcb438a 100644 --- a/dev_docs/contributing/best_practices.mdx +++ b/dev_docs/contributing/best_practices.mdx @@ -12,137 +12,9 @@ tags: ['kibana', 'onboarding', 'dev', 'architecture'] First things first, be sure to review our and check out all the available platform that can simplify plugin development. -## Developer documentation +## Documentation -### High-level documentation - -#### Structure - -Refer to [divio documentation](https://documentation.divio.com/) for guidance on where and how to structure our high-level documentation. - - and - sections are both _explanation_ oriented, - covers both _tutorials_ and _How to_, and the section covers _reference_ material. - -#### Location - -If the information spans multiple plugins, consider adding it to the [dev_docs](https://github.com/elastic/kibana/tree/main/dev_docs) folder. If it is plugin specific, consider adding it inside the plugin folder. Write it in an mdx file if you would like it to show up in our new (beta) documentation system. - - - -To add docs into the new docs system, create an `.mdx` file that -contains . Read about the syntax . An extra step is needed to add a menu item. will walk you through how to set the docs system -up locally and edit the nav menu. - - - -#### Keep content fresh - -A fresh pair of eyes are invaluable. Recruit new hires to read, review and update documentation. Leads should also periodically review documentation to ensure it stays up to date. File issues any time you notice documentation is outdated. - -#### Consider your target audience - -Documentation in the Kibana Developer Guide is targeted towards developers building Kibana plugins. Keep implementation details about internal plugin code out of these docs. - -#### High to low level - -When a developer first lands in our docs, think about their journey. Introduce basic concepts before diving into details. The left navigation should be set up so documents on top are higher level than documents near the bottom. - -#### Think outside-in - -It's easy to forget what it felt like to first write code in Kibana, but do your best to frame these docs "outside-in". Don't use esoteric, internal language unless a definition is documented and linked. The fresh eyes of a new hire can be a great asset. - -### API documentation - -We automatically generate . The following guidelines will help ensure your are useful. - -#### Code comments - -Every publicly exposed function, class, interface, type, parameter and property should have a comment using JSDoc style comments. - -- Use `@param` tags for every function parameter. -- Use `@returns` tags for return types. -- Use `@throws` when appropriate. -- Use `@beta` or `@deprecated` when appropriate. -- Use `@removeBy {version}` on `@deprecated` APIs. The version should be the last version the API will work in. For example, `@removeBy 7.15` means the API will be removed in 7.16. This lets us avoid mid-release cycle coordination. The API can be removed as soon as the 7.15 branch is cut. -- Use `@internal` to indicate this API item is intended for internal use only, which will also remove it from the docs. - -#### Interfaces vs inlined types - -Prefer types and interfaces over complex inline objects. For example, prefer: - -```ts -/** -* The SearchSpec interface contains settings for creating a new SearchService, like -* username and password. -*/ -export interface SearchSpec { - /** - * Stores the username. Duh, - */ - username: string; - /** - * Stores the password. I hope it's encrypted! - */ - password: string; -} - - /** - * Retrieve search services - * @param searchSpec Configuration information for initializing the search service. - * @returns the id of the search service - */ -export getSearchService: (searchSpec: SearchSpec) => string; -``` - -over: - -```ts -/** - * Retrieve search services - * @param searchSpec Configuration information for initializing the search service. - * @returns the id of the search service - */ -export getSearchService: (searchSpec: { username: string; password: string }) => string; -``` - -In the former, there will be a link to the `SearchSpec` interface with documentation for the `username` and `password` properties. In the latter the object will render inline, without comments: - -![prefer interfaces documentation](../assets/dev_docs_nested_object.png) - -#### Export every type used in a public API - -When a publicly exported API items references a private type, this results in a broken link in our docs system. The private type is, by proxy, part of your public API, and as such, should be exported. - -Do: - -```ts -export interface AnInterface { bar: string }; -export type foo: string | AnInterface; -``` - -Don't: - -```ts -interface AnInterface { bar: string }; -export type foo: string | AnInterface; -``` - -#### Avoid “Pick” - -`Pick` not only ends up being unhelpful in our documentation system, but it's also of limited help in your IDE. For that reason, avoid `Pick` and other similarly complex types on your public API items. Using these semantics internally is fine. - -![pick api documentation](../assets/api_doc_pick.png) - -### Example plugins - -Running Kibana with `yarn start --run-examples` will include all [example plugins](https://github.com/elastic/kibana/tree/main/examples). These are tested examples of platform services in use. We strongly encourage anyone providing a platform level service or to include a tutorial that links to a tested example plugin. This is better than relying on copied code snippets, which can quickly get out of date. - -You can also visit these [examples plugins hosted online](https://demo.kibana.dev/8.0/app/home). Note that because anonymous access is enabled, some -of the demos are currently not working. +Documentation best practices can be found . ## Performance diff --git a/dev_docs/contributing/documentation.mdx b/dev_docs/contributing/documentation.mdx new file mode 100644 index 00000000000000..ad9286dd07ab83 --- /dev/null +++ b/dev_docs/contributing/documentation.mdx @@ -0,0 +1,195 @@ +--- +id: kibDocumentation +slug: /kibana-dev-docs/contributing/documentation +title: Documentation +summary: Writing documentation during development +date: 2022-03-01 +tags: ['kibana', 'onboarding', 'dev'] +--- + +Docs should be written during development and accompany PRs when relevant. There are multiple types of documentation, and different places to add each. + +## End-user documentation + +User-facing features should be documented in [asciidoc](http://asciidoc.org/) at [https://github.com/elastic/kibana/tree/main/docs](https://github.com/elastic/kibana/tree/main/docs) + +To build the docs, you must clone the [elastic/docs](https://github.com/elastic/docs) repo as a sibling of your Kibana repo. Follow the instructions in that project’s [README](https://github.com/elastic/docs#readme) for getting the docs tooling set up. + +To build the docs: + +```bash +node scripts/docs.js --open +``` + +## REST APIs +REST APIs should be documented using the following formats: + +- [API doc template](https://raw.githubusercontent.com/elastic/docs/main/shared/api-ref-ex.asciidoc) +- [API object definition template](https://raw.githubusercontent.com/elastic/docs/main/shared/api-definitions-ex.asciidoc) + +## Developer documentation + +Developer documentation can be segmented into two types: internal plugin details, and information on extending Kibana. Our [Kibana Developer Guide](https://docs.elastic.dev/kibana-dev-docs/getting-started/welcome) is meant to serve the latter. The guide can only be accessed internally at the moment, though the raw content is public in our [public repository]((https://github.com/elastic/kibana/tree/main/dev_docs)). + +Internal plugin details can be kept alongside the code it describes. Information about extending Kibana may go in the root of your plugin folder, or inside the top-level [dev_docs](https://github.com/elastic/kibana/tree/main/dev_docs) folder. + + + +Only `mdx` files with the appropriate are rendered inside the Developer Guide. Read about the syntax . Edit [kibana/nav-kibana-dev.docnav.json](https://github.com/elastic/kibana/blob/main/nav-kibana-dev.docnav.json) to have a link to your document appear in the navigation menu. Read for more details on how to add new content and test locally. + + + +### Structure + +The high-level developer documentation located in the [dev_docs](https://github.com/elastic/kibana/tree/main/dev_docs) folder attempts to follow [divio documentation](https://documentation.divio.com/) guidance. and sections are _explanation_ oriented, while + falls under both _tutorials_ and _how to_. The section is _reference_ material. + +Developers may choose to keep information that is specific to a particular plugin along side the code. + +### Best practices + +#### Keep content fresh + +A fresh pair of eyes are invaluable. Recruit new hires to read, review and update documentation. Leads should also periodically review documentation to ensure it stays up to date. File issues any time you notice documentation is outdated. + +#### Consider your target audience + +Documentation in the Kibana Developer Guide is targeted towards developers building Kibana plugins. Keep implementation details about internal plugin code out of these docs. + +#### High to low level + +When a developer first lands in our docs, think about their journey. Introduce basic concepts before diving into details. The left navigation should be set up so documents on top are higher level than documents near the bottom. + +#### Think outside-in + +It's easy to forget what it felt like to first write code in Kibana, but do your best to frame these docs "outside-in". Don't use esoteric, internal language unless a definition is documented and linked. The fresh eyes of a new hire can be a great asset. + + +## API documentation + +We automatically generate . The following guidelines will help ensure your are useful. + +If you encounter an error of the form: + + + +You can increase [max memory](https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-megabytes) for node as follows: + +```bash +# As a runtime argument +node --max-old-space-size=8192 foo/bar + +# As an env variable, in order to apply it systematically +export NODE_OPTIONS=--max-old-space-size=8192 +``` + +### Code comments + +Every function, class, interface, type, parameter and property that is exposed to other plugins should have a [TSDoc](https://tsdoc.org/)-style comment. + +- Use `@param` tags for every function parameter. +- Use `@returns` tags for return types. +- Use `@throws` when appropriate. +- Use `@beta` or `@deprecated` when appropriate. +- Use `@removeBy {version}` on `@deprecated` APIs. The version should be the last version the API will work in. For example, `@removeBy 7.15` means the API will be removed in 7.16. This lets us avoid mid-release cycle coordination. The API can be removed as soon as the 7.15 branch is cut. +- Use `@internal` to indicate this API item is intended for internal use only, which will also remove it from the docs. + +### Interfaces vs inlined types + +Prefer types and interfaces over complex inline objects. For example, prefer: + +```ts +/** +* The SearchSpec interface contains settings for creating a new SearchService, like +* username and password. +*/ +export interface SearchSpec { + /** + * Stores the username. Duh, + */ + username: string; + /** + * Stores the password. I hope it's encrypted! + */ + password: string; +} + + /** + * Retrieve search services + * @param searchSpec Configuration information for initializing the search service. + * @returns the id of the search service + */ +export getSearchService: (searchSpec: SearchSpec) => string; +``` + +over: + +```ts +/** + * Retrieve search services + * @param searchSpec Configuration information for initializing the search service. + * @returns the id of the search service + */ +export getSearchService: (searchSpec: { username: string; password: string }) => string; +``` + +In the former, there will be a link to the `SearchSpec` interface with documentation for the `username` and `password` properties. In the latter the object will render inline, without comments: + +![prefer interfaces documentation](../assets/dev_docs_nested_object.png) + +### Export every type used in a public API + +When a publicly exported API item references a private type, this results in a broken link in our docs system. The private type is, by proxy, part of your public API, and as such, should be exported. + +Do: + +```ts +export interface AnInterface { bar: string }; +export type foo: string | AnInterface; +``` + +Don't: + +```ts +interface AnInterface { bar: string }; +export type foo: string | AnInterface; +``` + +### Avoid “Pick” + +`Pick` not only ends up being unhelpful in our documentation system, but it's also of limited help in your IDE. For that reason, avoid `Pick` and other similarly complex types on your public API items. Using these semantics internally is fine. + +![pick api documentation](../assets/api_doc_pick.png) + + +### Debugging tips + +There are three great ways to debug issues with the API infrastructure. + +1. Write a test + +[api_doc_suite.test.ts](https://github.com/elastic/kibana/blob/main/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts) is a pretty comprehensive test suite that builds the test docs inside the [**fixtures** folder](https://github.com/elastic/kibana/tree/main/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src). + +Edit the code inside `__fixtures__` to replicate the bug, write a test to track what should happen, then run `yarn jest api_doc_suite`. + +Once you've verified the bug is reproducible, use debug messages to narrow down the problem. This is much faster than running the entire suite to debug. + +2. Use [ts-ast-viewer.com](https://ts-ast-viewer.com/#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgMQgjgG8AoOSudJAfgC44AKdIxgZximQHMBKOAF4AfHE7ckPANxkAvkA) + +This nifty website will let you add some types and see how the system parses it. For example, the link above shows there is a `QuestionToken` as a sibling to the `FunctionType` which is why [this bug](https://github.com/elastic/kibana/issues/107145) reported children being lost. The API infra system didn't categorize the node as a function type node. + +3. Play around with `ts-morph` in a Code Sandbox. + +You can fork [this Code Sandbox example](https://codesandbox.io/s/typescript-compiler-issue-0lkwx?file=/src/use_ts_compiler.ts) that was used to explore how to generate the node signature in different ways (e.g. `node.getType.getText()` shows different results than `node.getType.getText(node)`). Here is [another messy example](https://codesandbox.io/s/admiring-field-5btxs). + +The code sandbox approach can be a lot faster to iterate compared to running it in Kibana. + +## Example plugins + +Running Kibana with `yarn start --run-examples` will include all [example plugins](https://github.com/elastic/kibana/tree/main/examples). These are tested examples of platform services in use. We strongly encourage anyone providing a platform level service or to include a tutorial that links to a tested example plugin. This is better than relying on copied code snippets, which can quickly get out of date. + +You can also visit these [examples plugins hosted online](https://demo.kibana.dev/8.2/app/home). Note that because anonymous access is enabled, some +of the demos are currently not working. diff --git a/docs/api/saved-objects/resolve_import_errors.asciidoc b/docs/api/saved-objects/resolve_import_errors.asciidoc index 7a57e03875e359..162e9589e4f9e0 100644 --- a/docs/api/saved-objects/resolve_import_errors.asciidoc +++ b/docs/api/saved-objects/resolve_import_errors.asciidoc @@ -25,7 +25,7 @@ To resolve errors, you can: ==== Path parameters `space_id`:: - (Optional, string) An identifier for the <>. When `space_id` is unspecfied in the URL, the default space is used. + (Optional, string) An identifier for the <>. When `space_id` is unspecified in the URL, the default space is used. [[saved-objects-api-resolve-import-errors-query-params]] ==== Query parameters diff --git a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc index d79df2c085b193..9d26f9656d3f60 100644 --- a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc +++ b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc @@ -68,7 +68,7 @@ Execute the <>, w `id`:::: (Required, string) The saved object ID. `overwrite`:::: - (Required, boolean) When set to `true`, the saved object from the source space (desigated by the <>) overwrites the conflicting object in the destination space. When set to `false`, this does nothing. + (Required, boolean) When set to `true`, the saved object from the source space (designated by the <>) overwrites the conflicting object in the destination space. When set to `false`, this does nothing. `destinationId`:::: (Optional, string) Specifies the destination ID that the copied object should have, if different from the current ID. `ignoreMissingReferences`::: diff --git a/docs/api/upgrade-assistant/default-field.asciidoc b/docs/api/upgrade-assistant/default-field.asciidoc index 8bdcd359d56681..bbe44d894963b9 100644 --- a/docs/api/upgrade-assistant/default-field.asciidoc +++ b/docs/api/upgrade-assistant/default-field.asciidoc @@ -26,7 +26,7 @@ GET /api/upgrade_assistant/add_query_default_field/myIndex // KIBANA <1> A required array of {es} field types that generate the list of fields. -<2> An optional array of additional field names, dot-deliminated. +<2> An optional array of additional field names, dot-delimited. To add the `index.query.default_field` index setting to the specified index, {kib} generates an array of all fields from the index mapping. The fields contain the types specified in `fieldTypes`. {kib} appends any other fields specified in `otherFields` to the array of default fields. diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index f76b9976dd1d2e..8a2beef22b6bd6 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -84,7 +84,7 @@ image:apm/images/red-service.png[APM red service]:: Max anomaly score **≥75**. [role="screenshot"] image::apm/images/apm-service-map-anomaly.png[Example view of anomaly scores on service maps in the APM app] -If an anomaly has been detected, click *view anomalies* to view the anomaly detection metric viewier in the Machine learning app. +If an anomaly has been detected, click *view anomalies* to view the anomaly detection metric viewer in the Machine learning app. This time series analysis will display additional details on the severity and time of the detected anomalies. To learn how to create a machine learning job, see <>. diff --git a/docs/developer/advanced/development-es-snapshots.asciidoc b/docs/developer/advanced/development-es-snapshots.asciidoc index 38146e65b6326f..ad9eb17ec309cf 100644 --- a/docs/developer/advanced/development-es-snapshots.asciidoc +++ b/docs/developer/advanced/development-es-snapshots.asciidoc @@ -13,6 +13,7 @@ https://ci.kibana.dev/es-snapshots[A dashboard] is available that shows the curr 2. Each snapshot is uploaded to a public Google Cloud Storage bucket, `kibana-ci-es-snapshots-daily`. ** At this point, the snapshot is not automatically used in CI or local development. It needs to be tested/verified first. 3. Each snapshot is tested with the latest commit of the corresponding {kib} branch, using the full CI suite. +3a. If a test fails during snapshot verification the Kibana Operations team will skip it and create an issue for the team to fix the test, or work with the Elasticsearch team to get a fix implemented there. Once the fix is ready a Kibana PR can be opened to unskip the test. 4. After CI ** If the snapshot passes, it is promoted and automatically used in CI and local development. ** If the snapshot fails, the issue must be investigated and resolved. A new incompatibility may exist between {es} and {kib}. diff --git a/docs/developer/architecture/kibana-platform-plugin-api.asciidoc b/docs/developer/architecture/kibana-platform-plugin-api.asciidoc index 2005a90bb87bb8..9cf60cda76f759 100644 --- a/docs/developer/architecture/kibana-platform-plugin-api.asciidoc +++ b/docs/developer/architecture/kibana-platform-plugin-api.asciidoc @@ -221,7 +221,7 @@ These are the contracts exposed by the core services for each lifecycle: [cols=",,",options="header",] |=== |lifecycle |server contract|browser contract -|_contructor_ +|_constructor_ |{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md[PluginInitializerContext] |{kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.plugininitializercontext.md[PluginInitializerContext] diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index 2631ee717c3d5f..92b6818a09865b 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -51,7 +51,7 @@ Additionally, in order to migrate into project refs, you also need to make sure ], "references": [ { "path": "../../core/tsconfig.json" }, - // add references to other TypeScript projects your plugin dependes on + // add references to other TypeScript projects your plugin depends on ] } ---- diff --git a/docs/developer/contributing/development-ci-metrics.asciidoc b/docs/developer/contributing/development-ci-metrics.asciidoc index 3a133e64ea5286..2905bd72a501fe 100644 --- a/docs/developer/contributing/development-ci-metrics.asciidoc +++ b/docs/developer/contributing/development-ci-metrics.asciidoc @@ -137,4 +137,4 @@ If you only want to run the build once you can run: node scripts/build_kibana_platform_plugins --validate-limits --focus {pluginId} ----------- -This command needs to apply production optimizations to get the right sizes, which means that the optimizer will take significantly longer to run and on most developmer machines will consume all of your machines resources for 20 minutes or more. If you'd like to multi-task while this is running you might need to limit the number of workers using the `--max-workers` flag. \ No newline at end of file +This command needs to apply production optimizations to get the right sizes, which means that the optimizer will take significantly longer to run and on most developer machines will consume all of your machines resources for 20 minutes or more. If you'd like to multi-task while this is running you might need to limit the number of workers using the `--max-workers` flag. \ No newline at end of file diff --git a/docs/developer/contributing/development-documentation.asciidoc b/docs/developer/contributing/development-documentation.asciidoc index 7137d5bad051cb..9f221c0f01309d 100644 --- a/docs/developer/contributing/development-documentation.asciidoc +++ b/docs/developer/contributing/development-documentation.asciidoc @@ -3,14 +3,6 @@ Docs should be written during development and accompany PRs when relevant. There are multiple types of documentation, and different places to add each. -[discrete] -=== Developer services documentation - -Documentation about specific services a plugin offers should be encapsulated in: - -* README.asciidoc at the base of the plugin folder. -* Typescript comments for all public services. - [discrete] === End user documentation @@ -31,7 +23,7 @@ node scripts/docs.js --open REST APIs should be documented using the following recommended formats: -* https://raw.githubusercontent.com/elastic/docs/master/shared/api-ref-ex.asciidoc[API doc templaate] +* https://raw.githubusercontent.com/elastic/docs/master/shared/api-ref-ex.asciidoc[API doc template] * https://raw.githubusercontent.com/elastic/docs/master/shared/api-definitions-ex.asciidoc[API object definition template] [discrete] diff --git a/docs/developer/contributing/interpreting-ci-failures.asciidoc b/docs/developer/contributing/interpreting-ci-failures.asciidoc index ffbe448d79a444..eead720f03c609 100644 --- a/docs/developer/contributing/interpreting-ci-failures.asciidoc +++ b/docs/developer/contributing/interpreting-ci-failures.asciidoc @@ -22,7 +22,7 @@ image::images/job_view.png[Jenkins job view showing a test failure] 1. *Git Changes:* the list of commits that were in this build which weren't in the previous build. For Pull Requests this list is calculated by comparing against the most recent Pull Request which was tested, it is not limited to build for this specific Pull Request, so it's not very useful. 2. *Test Results:* A link to the test results screen, and shortcuts to the failed tests. Functional tests capture and store the log output from each specific test, and make it visible at these links. For other test runners only the error message is visible and log output must be tracked down in the *Pipeline Steps*. 3. *Google Cloud Storage (GCS) Upload Report:* Link to the screen which lists out the artifacts uploaded to GCS during this job execution. -4. *Pipeline Steps:*: A breakdown of the pipline that was executed, along with individual log output for each step in the pipeline. +4. *Pipeline Steps:*: A breakdown of the pipeline that was executed, along with individual log output for each step in the pipeline. [discrete] === Viewing ciGroup/test Logs diff --git a/docs/developer/index.asciidoc b/docs/developer/index.asciidoc index 86d1d32e75e363..7d9116a72d069d 100644 --- a/docs/developer/index.asciidoc +++ b/docs/developer/index.asciidoc @@ -2,6 +2,9 @@ = Developer guide -- + +NOTE: This is our legacy developer guide, and while we strive to keep it accurate, new content is added inside the {kib-repo}blob/{branch}/dev_docs[Kibana repo]. The rendered https://docs.elastic.dev/kibana-dev-docs/getting-started/welcome[guide] can only be accessed internally at the moment, though the raw content is public in our {kib-repo}blob/{branch}/dev_docs[public repository]. + Contributing to {kib} can be daunting at first, but it doesn't have to be. The following sections should get you up and running in no time. If you have any problems, file an issue in the https://github.com/elastic/kibana/issues[Kibana repo]. diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.executioncontext.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.executioncontext.md new file mode 100644 index 00000000000000..be5689ad7b080f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.executioncontext.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreSetup](./kibana-plugin-core-public.coresetup.md) > [executionContext](./kibana-plugin-core-public.coresetup.executioncontext.md) + +## CoreSetup.executionContext property + +[ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) + +Signature: + +```typescript +executionContext: ExecutionContextSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.md index 9488b8a26b8679..31793ec6f7a581 100644 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.md @@ -17,6 +17,7 @@ export interface CoreSetup + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreStart](./kibana-plugin-core-public.corestart.md) > [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) + +## CoreStart.executionContext property + +[ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md) + +Signature: + +```typescript +executionContext: ExecutionContextStart; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md index ae67696e12501b..edd80e1adb9f98 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -20,6 +20,7 @@ export interface CoreStart | [chrome](./kibana-plugin-core-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-core-public.chromestart.md) | | [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) | DeprecationsServiceStart | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | | [docLinks](./kibana-plugin-core-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | +| [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) | ExecutionContextStart | [ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md) | | [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | | [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-core-public.httpstart.md) | | [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.clear.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.clear.md new file mode 100644 index 00000000000000..94936b94d0710b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.clear.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [clear](./kibana-plugin-core-public.executioncontextsetup.clear.md) + +## ExecutionContextSetup.clear() method + +clears the context + +Signature: + +```typescript +clear(): void; +``` +Returns: + +void + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.context_.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.context_.md new file mode 100644 index 00000000000000..d6c74db6d603e9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.context_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [context$](./kibana-plugin-core-public.executioncontextsetup.context_.md) + +## ExecutionContextSetup.context$ property + +The current context observable + +Signature: + +```typescript +context$: Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.get.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.get.md new file mode 100644 index 00000000000000..65e9b1218649d2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.get.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [get](./kibana-plugin-core-public.executioncontextsetup.get.md) + +## ExecutionContextSetup.get() method + +Get the current top level context + +Signature: + +```typescript +get(): KibanaExecutionContext; +``` +Returns: + +KibanaExecutionContext + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.getaslabels.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.getaslabels.md new file mode 100644 index 00000000000000..0f0bda4e2913e9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.getaslabels.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [getAsLabels](./kibana-plugin-core-public.executioncontextsetup.getaslabels.md) + +## ExecutionContextSetup.getAsLabels() method + +returns apm labels + +Signature: + +```typescript +getAsLabels(): Labels; +``` +Returns: + +Labels + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.md new file mode 100644 index 00000000000000..01581d2e80a5c9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) + +## ExecutionContextSetup interface + +Kibana execution context. Used to provide execution context to Elasticsearch, reporting, performance monitoring, etc. + +Signature: + +```typescript +export interface ExecutionContextSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [context$](./kibana-plugin-core-public.executioncontextsetup.context_.md) | Observable<KibanaExecutionContext> | The current context observable | + +## Methods + +| Method | Description | +| --- | --- | +| [clear()](./kibana-plugin-core-public.executioncontextsetup.clear.md) | clears the context | +| [get()](./kibana-plugin-core-public.executioncontextsetup.get.md) | Get the current top level context | +| [getAsLabels()](./kibana-plugin-core-public.executioncontextsetup.getaslabels.md) | returns apm labels | +| [set(c$)](./kibana-plugin-core-public.executioncontextsetup.set.md) | Set the current top level context | +| [withGlobalContext(context)](./kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md) | merges the current top level context with the specific event context | + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.set.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.set.md new file mode 100644 index 00000000000000..e3dcea78c827ab --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.set.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [set](./kibana-plugin-core-public.executioncontextsetup.set.md) + +## ExecutionContextSetup.set() method + +Set the current top level context + +Signature: + +```typescript +set(c$: KibanaExecutionContext): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| c$ | KibanaExecutionContext | | + +Returns: + +void + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md new file mode 100644 index 00000000000000..574d0fd989750f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [withGlobalContext](./kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md) + +## ExecutionContextSetup.withGlobalContext() method + +merges the current top level context with the specific event context + +Signature: + +```typescript +withGlobalContext(context?: KibanaExecutionContext): KibanaExecutionContext; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| context | KibanaExecutionContext | | + +Returns: + +KibanaExecutionContext + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextstart.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextstart.md new file mode 100644 index 00000000000000..0d210ba5bb1c4d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextstart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md) + +## ExecutionContextStart type + +See [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md). + +Signature: + +```typescript +export declare type ExecutionContextStart = ExecutionContextSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md index 6266639b63976d..d8f8a77d84b2f4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md +++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md @@ -10,9 +10,10 @@ Represents a meta-information about a Kibana entity initiating a search request. ```typescript export declare type KibanaExecutionContext = { - readonly type: string; - readonly name: string; - readonly id: string; + readonly type?: string; + readonly name?: string; + readonly page?: string; + readonly id?: string; readonly description?: string; readonly url?: string; child?: KibanaExecutionContext; diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b51f5ed833fd37..fca697144a8723 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -62,6 +62,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | DeprecationsService provides methods to fetch domain deprecation details from the Kibana server. | | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | | [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) error APIs. | +| [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) | Kibana execution context. Used to provide execution context to Elasticsearch, reporting, performance monitoring, etc. | | [FatalErrorInfo](./kibana-plugin-core-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HttpFetchOptions](./kibana-plugin-core-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-core-public.httphandler.md). | @@ -160,6 +161,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeBreadcrumb](./kibana-plugin-core-public.chromebreadcrumb.md) | | | [ChromeHelpExtensionLinkBase](./kibana-plugin-core-public.chromehelpextensionlinkbase.md) | | | [ChromeHelpExtensionMenuLink](./kibana-plugin-core-public.chromehelpextensionmenulink.md) | | +| [ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md) | See [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md). | | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HttpStart](./kibana-plugin-core-public.httpstart.md) | See [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | | [IToasts](./kibana-plugin-core-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-core-public.toastsapi.md). | diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.getaslabels.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.getaslabels.md new file mode 100644 index 00000000000000..c8816a3deee4da --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.getaslabels.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [getAsLabels](./kibana-plugin-core-server.executioncontextsetup.getaslabels.md) + +## ExecutionContextSetup.getAsLabels() method + +Signature: + +```typescript +getAsLabels(): apm.Labels; +``` +Returns: + +apm.Labels + diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md index 24591648ad953b..7fdc4d1ec1d57c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md @@ -15,5 +15,6 @@ export interface ExecutionContextSetup | Method | Description | | --- | --- | +| [getAsLabels()](./kibana-plugin-core-server.executioncontextsetup.getaslabels.md) | | | [withContext(context, fn)](./kibana-plugin-core-server.executioncontextsetup.withcontext.md) | Keeps track of execution context while the passed function is executed. Data are carried over all async operations spawned by the passed function. The nested calls stack the registered context on top of each other. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md index 0d65a3662da6f3..792af8f693869f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md @@ -10,9 +10,10 @@ Represents a meta-information about a Kibana entity initiating a search request. ```typescript export declare type KibanaExecutionContext = { - readonly type: string; - readonly name: string; - readonly id: string; + readonly type?: string; + readonly name?: string; + readonly page?: string; + readonly id?: string; readonly description?: string; readonly url?: string; child?: KibanaExecutionContext; diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc index 3231d2162f2e18..e745835c879994 100644 --- a/docs/osquery/osquery.asciidoc +++ b/docs/osquery/osquery.asciidoc @@ -273,12 +273,18 @@ for an agent policy through Fleet. This integration supports x64 architecture on Windows, MacOS, and Linux platforms, and ARM64 architecture on Linux. -NOTE: The original {filebeat-ref}/filebeat-module-osquery.html[Filebeat Osquery module] +[NOTE] +========================= + +* The original {filebeat-ref}/filebeat-module-osquery.html[Filebeat Osquery module] and the https://docs.elastic.co/en/integrations/osquery[Osquery] integration collect logs from self-managed Osquery deployments. The *Osquery Manager* integration manages Osquery deployments and supports running and scheduling queries from {kib}. +* *Osquery Manager* cannot be integrated with an Elastic Agent in standalone mode. +========================= + [float] === Customize Osquery sub-feature privileges diff --git a/docs/settings/enterprise-search-settings.asciidoc b/docs/settings/enterprise-search-settings.asciidoc new file mode 100644 index 00000000000000..736a7614b31edb --- /dev/null +++ b/docs/settings/enterprise-search-settings.asciidoc @@ -0,0 +1,26 @@ +[role="xpack"] +[[enterprise-search-settings-kb]] +=== Enterprise Search settings in {kib} +++++ +Enterprise Search settings +++++ + +On Elastic Cloud, you do not need to configure any settings to use Enterprise Search in {kib}. It is enabled by default. On self-managed installations, you must configure `enterpriseSearch.host`. + +`enterpriseSearch.host`:: +The http(s) URL of your Enterprise Search instance. For example, in a local self-managed setup, +set this to `http://localhost:3002`. Authentication between {kib} and the Enterprise Search host URL, +such as via OAuth, is not supported. You can also +{enterprise-search-ref}/configure-ssl-tls.html#configure-ssl-tls-in-kibana[configure {kib} to trust +your Enterprise Search TLS certificate authority]. + + +`enterpriseSearch.accessCheckTimeout`:: +When launching the Enterprise Search UI, the maximum number of milliseconds for {kib} to wait +for a response from Enterprise Search +before considering the attempt failed and logging a warning. +Default: 5000. + +`enterpriseSearch.accessCheckTimeoutWarning`:: +When launching the Enterprise Search UI, the maximum number of milliseconds for {kib} to wait for a response from +Enterprise Search before logging a warning. Default: 300. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 1d698e90879373..9e1ee62f093fe3 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -55,7 +55,7 @@ https://www.elastic.co/guide/en/elasticsearch/client/index.html[{es} Client docu If you are running {kib} on our hosted {es} Service, click *View deployment details* on the *Integrations* view -to verify your {es} endpoint and Cloud ID, and create API keys for integestion. +to verify your {es} endpoint and Cloud ID, and create API keys for integration. [float] === Add sample data diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index e55a94a516d68d..3a1e0f1a7f4ffa 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -282,9 +282,6 @@ on the {kib} index at startup. {kib} users still need to authenticate with that the {kib} server uses to perform maintenance on the {kib} index at startup. This setting is an alternative to `elasticsearch.username` and `elasticsearch.password`. -| `enterpriseSearch.host` - | The http(s) URL of your Enterprise Search instance. For example, in a local self-managed setup, set this to `http://localhost:3002`. Authentication between Kibana and the Enterprise Search host URL, such as via OAuth, is not supported. You can also {enterprise-search-ref}/configure-ssl-tls.html#configure-ssl-tls-in-kibana[configure Kibana to trust your Enterprise Search TLS certificate authority]. - | `interpreter.enableInVisualize` | Enables use of interpreter in Visualize. *Default: `true`* @@ -719,6 +716,7 @@ Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[] include::{kib-repo-dir}/settings/apm-settings.asciidoc[] include::{kib-repo-dir}/settings/banners-settings.asciidoc[] +include::{kib-repo-dir}/settings/enterprise-search-settings.asciidoc[] include::{kib-repo-dir}/settings/fleet-settings.asciidoc[] include::{kib-repo-dir}/settings/i18n-settings.asciidoc[] include::{kib-repo-dir}/settings/logging-settings.asciidoc[] @@ -726,8 +724,8 @@ include::{kib-repo-dir}/settings/logs-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/monitoring-settings.asciidoc[] include::{kib-repo-dir}/settings/reporting-settings.asciidoc[] -include::secure-settings.asciidoc[] include::{kib-repo-dir}/settings/search-sessions-settings.asciidoc[] +include::secure-settings.asciidoc[] include::{kib-repo-dir}/settings/security-settings.asciidoc[] include::{kib-repo-dir}/settings/spaces-settings.asciidoc[] include::{kib-repo-dir}/settings/task-manager-settings.asciidoc[] diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index a097e34b20911f..56e3606c18b72f 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -276,20 +276,4 @@ For other types of month over month calculations, use <> o Calculating the duration between the start and end of an event is unsupported in *TSVB* because *TSVB* requires correlation between different time periods. *TSVB* requires that the duration is pre-calculated. -==== - -[discrete] -[group-on-multiple-fields] -.*How do I group on multiple fields?* -[%collapsible] -==== - -To group with multiple fields, create runtime fields in the {data-source} you are visualizing. - -. Create a runtime field. Refer to <> for more information. -+ -[role="screenshot"] -image::images/tsvb_group_by_multiple_fields.png[Group by multiple fields] - -. Create a *TSVB* visualization and group by this field. ==== \ No newline at end of file diff --git a/docs/user/production-considerations/task-manager-production-considerations.asciidoc b/docs/user/production-considerations/task-manager-production-considerations.asciidoc index 672c310f138e98..28c5f6e4f14c86 100644 --- a/docs/user/production-considerations/task-manager-production-considerations.asciidoc +++ b/docs/user/production-considerations/task-manager-production-considerations.asciidoc @@ -101,7 +101,7 @@ Scaling {kib} instances horizontally requires a higher degree of coordination, w A recommended strategy is to follow these steps: 1. Produce a <> as a guide to provisioning as many {kib} instances as needed. Include any growth in tasks that you predict experiencing in the near future, and a buffer to better address ad-hoc tasks. -2. After provisioning a deployment, assess whether the provisioned {kib} instances achieve the required throughput by evaluating the <> as described in <>. +2. After provisioning a deployment, assess whether the provisioned {kib} instances achieve the required throughput by evaluating the <> as described in <>. 3. If the throughput is insufficient, and {kib} instances exhibit low resource usage, incrementally scale vertically while <> the impact of these changes. 4. If the throughput is insufficient, and {kib} instances are exhibiting high resource usage, incrementally scale horizontally by provisioning new {kib} instances and reassess. diff --git a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc index a22d46902f54c3..606dd3c8a24eec 100644 --- a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc +++ b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc @@ -412,7 +412,7 @@ This assessment is based on the following: * Comparing the `last_successful_poll` to the `timestamp` (value of `2021-02-16T11:38:10.077Z`) at the root, where you can see the last polling cycle took place 1 second before the monitoring stats were exposed by the health monitoring API. * Comparing the `last_polling_delay` to the `timestamp` (value of `2021-02-16T11:38:10.077Z`) at the root, where you can see the last polling cycle delay took place 2 days ago, suggesting {kib} instances are not conflicting often. -* The `p50` of the `duration` shows that at least 50% of polling cycles take, at most, 13 millisconds to complete. +* The `p50` of the `duration` shows that at least 50% of polling cycles take, at most, 13 milliseconds to complete. * Evaluating the `result_frequency_percent_as_number`: ** 80% of the polling cycles completed without claiming any tasks (suggesting that there aren't any overdue tasks). ** 20% completed with Task Manager claiming tasks that were then executed. @@ -508,7 +508,7 @@ For details on achieving higher throughput by adjusting your scaling strategy, s Tasks run for too long, overrunning their schedule *Diagnosis*: -The <> theory analyzed a hypothetical scenario where both drift and load were unusually high. +The <> theory analyzed a hypothetical scenario where both drift and load were unusually high. Suppose an alternate scenario, where `drift` is high, but `load` is not, such as the following: @@ -688,7 +688,7 @@ Keep in mind that these stats give you a glimpse at a moment in time, and even t [[task-manager-health-evaluate-the-workload]] ===== Evaluate the Workload -Predicting the required throughput a deplyment might need to support Task Manager is difficult, as features can schedule an unpredictable number of tasks at a variety of scheduled cadences. +Predicting the required throughput a deployment might need to support Task Manager is difficult, as features can schedule an unpredictable number of tasks at a variety of scheduled cadences. <> provides statistics that make it easier to monitor the adequacy of the existing throughput. By evaluating the workload, the required throughput can be estimated, which is used when following the Task Manager <>. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 2f2b2793897996..446de62326f8ed 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -5,7 +5,7 @@ Authentication ++++ :keywords: administrator, concept, security, authentication -:description: A list of the supported authentication mechanisms in {kib}. +:description: A list of the supported authentication mechanisms in {kib}. {kib} supports the following authentication mechanisms: @@ -483,4 +483,4 @@ To make this iframe leverage anonymous access automatically, you will need to mo NOTE: `auth_provider_hint` query string parameter goes *before* the hash URL fragment. -For more information on how to embed, refer to <>. +For more information, refer to <>. diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 933e257ca235e8..43ca1ed4bf8137 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -15,6 +15,19 @@ { "id": "kibTroubleshooting" } ] }, + { + "label": "Contributing", + "items": [ + { "id": "kibDevPrinciples" }, + { "id": "kibRepoStructure" }, + { "id": "kibStandards" }, + { "id": "kibBestPractices" }, + { "id": "kibDocumentation" }, + { "id": "kibStyleGuide" }, + { "id": "ktRFCProcess" }, + { "id": "kibGitHub" } + ] + }, { "label": "Key concepts", "items": [ @@ -52,18 +65,6 @@ { "id": "kibDevSharePluginReadme"} ] }, - { - "label": "Contributing", - "items": [ - { "id": "kibRepoStructure" }, - { "id": "kibDevPrinciples" }, - { "id": "kibStandards" }, - { "id": "ktRFCProcess" }, - { "id": "kibBestPractices" }, - { "id": "kibStyleGuide" }, - { "id": "kibGitHub" } - ] - }, { "label": "Contributors Newsletters", "items": [ diff --git a/package.json b/package.json index fdb358c25fb83c..a2022e05c86cb8 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", "@elastic/charts": "43.1.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.1.0-canary.3", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.1", "@elastic/ems-client": "8.0.0", "@elastic/eui": "48.1.1", "@elastic/filesaver": "1.1.2", @@ -737,7 +737,7 @@ "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^98.0.0", + "chromedriver": "^98.0.1", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compression-webpack-plugin": "^4.0.0", diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log.ts index 84e9159dfcd415..e9b5ab04a73905 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log.ts +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log.ts @@ -62,7 +62,10 @@ export class ToolingLog { * @param delta the number of spaces to increase/decrease the indentation * @param block a function to run and reset any indentation changes after */ - public indent(delta = 0, block?: () => Promise) { + public indent(delta: number): undefined; + public indent(delta: number, block: () => Promise): Promise; + public indent(delta: number, block: () => T): T; + public indent(delta = 0, block?: () => T | Promise) { const originalWidth = this.indentWidth$.getValue(); this.indentWidth$.next(Math.max(originalWidth + delta, 0)); if (!block) { diff --git a/packages/kbn-es/src/cli_commands/build_snapshots.js b/packages/kbn-es/src/cli_commands/build_snapshots.js index d6ea76faf2cf96..070f11b8b5f84d 100644 --- a/packages/kbn-es/src/cli_commands/build_snapshots.js +++ b/packages/kbn-es/src/cli_commands/build_snapshots.js @@ -42,32 +42,31 @@ exports.run = async (defaults = {}) => { for (const license of ['oss', 'trial']) { for (const platform of ['darwin', 'win32', 'linux']) { log.info('Building', platform, license === 'trial' ? 'default' : 'oss', 'snapshot'); - log.indent(4); + await log.indent(4, async () => { + const snapshotPath = await buildSnapshot({ + license, + sourcePath: options.sourcePath, + log, + platform, + }); - const snapshotPath = await buildSnapshot({ - license, - sourcePath: options.sourcePath, - log, - platform, - }); - - const filename = basename(snapshotPath); - const outputPath = resolve(outputDir, filename); - const hash = createHash('sha512'); - await pipelineAsync( - Fs.createReadStream(snapshotPath), - new Transform({ - transform(chunk, _, cb) { - hash.update(chunk); - cb(undefined, chunk); - }, - }), - Fs.createWriteStream(outputPath) - ); + const filename = basename(snapshotPath); + const outputPath = resolve(outputDir, filename); + const hash = createHash('sha512'); + await pipelineAsync( + Fs.createReadStream(snapshotPath), + new Transform({ + transform(chunk, _, cb) { + hash.update(chunk); + cb(undefined, chunk); + }, + }), + Fs.createWriteStream(outputPath) + ); - Fs.writeFileSync(`${outputPath}.sha512`, `${hash.digest('hex')} ${filename}`); - log.success('snapshot and shasum written to', outputPath); - log.indent(-4); + Fs.writeFileSync(`${outputPath}.sha512`, `${hash.digest('hex')} ${filename}`); + log.success('snapshot and shasum written to', outputPath); + }); } } }; diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 22ff9ae3c0cde3..3dd6d79fcb14ec 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -59,13 +59,10 @@ exports.Cluster = class Cluster { */ async installSource(options = {}) { this._log.info(chalk.bold('Installing from source')); - this._log.indent(4); - - const { installPath } = await installSource({ log: this._log, ...options }); - - this._log.indent(-4); - - return { installPath }; + return await this._log.indent(4, async () => { + const { installPath } = await installSource({ log: this._log, ...options }); + return { installPath }; + }); } /** @@ -78,16 +75,14 @@ exports.Cluster = class Cluster { */ async downloadSnapshot(options = {}) { this._log.info(chalk.bold('Downloading snapshot')); - this._log.indent(4); + return await this._log.indent(4, async () => { + const { installPath } = await downloadSnapshot({ + log: this._log, + ...options, + }); - const { installPath } = await downloadSnapshot({ - log: this._log, - ...options, + return { installPath }; }); - - this._log.indent(-4); - - return { installPath }; } /** @@ -100,16 +95,14 @@ exports.Cluster = class Cluster { */ async installSnapshot(options = {}) { this._log.info(chalk.bold('Installing from snapshot')); - this._log.indent(4); + return await this._log.indent(4, async () => { + const { installPath } = await installSnapshot({ + log: this._log, + ...options, + }); - const { installPath } = await installSnapshot({ - log: this._log, - ...options, + return { installPath }; }); - - this._log.indent(-4); - - return { installPath }; } /** @@ -122,16 +115,14 @@ exports.Cluster = class Cluster { */ async installArchive(path, options = {}) { this._log.info(chalk.bold('Installing from an archive')); - this._log.indent(4); + return await this._log.indent(4, async () => { + const { installPath } = await installArchive(path, { + log: this._log, + ...options, + }); - const { installPath } = await installArchive(path, { - log: this._log, - ...options, + return { installPath }; }); - - this._log.indent(-4); - - return { installPath }; } /** @@ -144,21 +135,19 @@ exports.Cluster = class Cluster { */ async extractDataDirectory(installPath, archivePath, extractDirName = 'data') { this._log.info(chalk.bold(`Extracting data directory`)); - this._log.indent(4); - - // stripComponents=1 excludes the root directory as that is how our archives are - // structured. This works in our favor as we can explicitly extract into the data dir - const extractPath = path.resolve(installPath, extractDirName); - this._log.info(`Data archive: ${archivePath}`); - this._log.info(`Extract path: ${extractPath}`); - - await extract({ - archivePath, - targetDir: extractPath, - stripComponents: 1, + await this._log.indent(4, async () => { + // stripComponents=1 excludes the root directory as that is how our archives are + // structured. This works in our favor as we can explicitly extract into the data dir + const extractPath = path.resolve(installPath, extractDirName); + this._log.info(`Data archive: ${archivePath}`); + this._log.info(`Extract path: ${extractPath}`); + + await extract({ + archivePath, + targetDir: extractPath, + stripComponents: 1, + }); }); - - this._log.indent(-4); } /** @@ -169,24 +158,27 @@ exports.Cluster = class Cluster { * @returns {Promise} */ async start(installPath, options = {}) { - this._exec(installPath, options); - - await Promise.race([ - // wait for native realm to be setup and es to be started - Promise.all([ - first(this._process.stdout, (data) => { - if (/started/.test(data)) { - return true; - } - }), - this._setupPromise, - ]), + // _exec indents and we wait for our own end condition, so reset the indent level to it's current state after we're done waiting + await this._log.indent(0, async () => { + this._exec(installPath, options); + + await Promise.race([ + // wait for native realm to be setup and es to be started + Promise.all([ + first(this._process.stdout, (data) => { + if (/started/.test(data)) { + return true; + } + }), + this._setupPromise, + ]), - // await the outcome of the process in case it exits before starting - this._outcome.then(() => { - throw createCliError('ES exited without starting'); - }), - ]); + // await the outcome of the process in case it exits before starting + this._outcome.then(() => { + throw createCliError('ES exited without starting'); + }), + ]); + }); } /** @@ -197,16 +189,19 @@ exports.Cluster = class Cluster { * @returns {Promise} */ async run(installPath, options = {}) { - this._exec(installPath, options); + // _exec indents and we wait for our own end condition, so reset the indent level to it's current state after we're done waiting + await this._log.indent(0, async () => { + this._exec(installPath, options); + + // log native realm setup errors so they aren't uncaught + this._setupPromise.catch((error) => { + this._log.error(error); + this.stop(); + }); - // log native realm setup errors so they aren't uncaught - this._setupPromise.catch((error) => { - this._log.error(error); - this.stop(); + // await the final outcome of the process + await this._outcome; }); - - // await the final outcome of the process - await this._outcome; } /** diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts index 517e3bbfa51331..060f05a445eb59 100644 --- a/packages/kbn-optimizer/src/log_optimizer_state.ts +++ b/packages/kbn-optimizer/src/log_optimizer_state.ts @@ -95,16 +95,16 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { if (state.phase === 'issue') { log.error(`webpack compile errors`); - log.indent(4); - for (const b of state.compilerStates) { - if (b.type === 'compiler issue') { - log.error(`[${b.bundleId}] build`); - log.indent(4); - log.error(b.failure); - log.indent(-4); + log.indent(4, () => { + for (const b of state.compilerStates) { + if (b.type === 'compiler issue') { + log.error(`[${b.bundleId}] build`); + log.indent(4, () => { + log.error(b.failure); + }); + } } - } - log.indent(-4); + }); return; } diff --git a/packages/kbn-plugin-helpers/src/tasks/optimize.ts b/packages/kbn-plugin-helpers/src/tasks/optimize.ts index c0f984eb03fcfb..ee05fa3d3354c2 100644 --- a/packages/kbn-plugin-helpers/src/tasks/optimize.ts +++ b/packages/kbn-plugin-helpers/src/tasks/optimize.ts @@ -23,26 +23,25 @@ export async function optimize({ log, plugin, sourceDir, buildDir }: BuildContex } log.info('running @kbn/optimizer'); - log.indent(2); - - // build bundles into target - const config = OptimizerConfig.create({ - repoRoot: REPO_ROOT, - pluginPaths: [sourceDir], - cache: false, - dist: true, - filter: [plugin.manifest.id], + await log.indent(2, async () => { + // build bundles into target + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + pluginPaths: [sourceDir], + cache: false, + dist: true, + filter: [plugin.manifest.id], + }); + + const target = Path.resolve(sourceDir, 'target'); + + await runOptimizer(config).pipe(logOptimizerState(log, config)).toPromise(); + + // clean up unnecessary files + Fs.unlinkSync(Path.resolve(target, 'public/metrics.json')); + Fs.unlinkSync(Path.resolve(target, 'public/.kbn-optimizer-cache')); + + // move target into buildDir + await asyncRename(target, Path.resolve(buildDir, 'target')); }); - - const target = Path.resolve(sourceDir, 'target'); - - await runOptimizer(config).pipe(logOptimizerState(log, config)).toPromise(); - - // clean up unnecessary files - Fs.unlinkSync(Path.resolve(target, 'public/metrics.json')); - Fs.unlinkSync(Path.resolve(target, 'public/.kbn-optimizer-cache')); - - // move target into buildDir - await asyncRename(target, Path.resolve(buildDir, 'target')); - log.indent(-2); } diff --git a/packages/kbn-securitysolution-autocomplete/README.md b/packages/kbn-securitysolution-autocomplete/README.md index 41bfd9baf628d8..83b2d6a1882cea 100644 --- a/packages/kbn-securitysolution-autocomplete/README.md +++ b/packages/kbn-securitysolution-autocomplete/README.md @@ -1,6 +1,6 @@ # Autocomplete Fields -Need an input that shows available index fields? Or an input that autocompletes based on a selected indexPattern field? Bingo! That's what these components are for. They are generalized enough so that they can be reused throughout and repurposed based on your needs. +Need an input that shows available index fields? Or an input that auto-completes based on a selected indexPattern field? Bingo! That's what these components are for. They are generalized enough so that they can be reused throughout and repurposed based on your needs. All three of the available components rely on Eui's combo box. @@ -119,4 +119,24 @@ The `onChange` handler is passed selected `string[]`. indexPattern={indexPattern} onChange={handleFieldMatchAnyValueChange} /> +``` + +## AutocompleteFieldWildcardComponent + +This component can be used to allow users to select a single value. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own value. + +The `onChange` handler is passed selected `string[]`. + +```js + ``` \ No newline at end of file diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.test.tsx new file mode 100644 index 00000000000000..34769a76563c16 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.test.tsx @@ -0,0 +1,279 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { ReactWrapper, mount } from 'enzyme'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { act } from '@testing-library/react'; +import { AutocompleteFieldWildcardComponent } from '.'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { fields, getField } from '../fields/index.mock'; +import { autocompleteStartMock } from '../autocomplete/index.mock'; + +jest.mock('../hooks/use_field_value_autocomplete'); + +describe('AutocompleteFieldWildcardComponent', () => { + let wrapper: ReactWrapper; + + const getValueSuggestionsMock = jest + .fn() + .mockResolvedValue([false, true, ['value 3', 'value 4'], jest.fn()]); + + beforeEach(() => { + (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ + false, + true, + ['value 1', 'value 2'], + getValueSuggestionsMock, + ]); + }); + + afterEach(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('it renders row label if one passed in', () => { + wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="valuesAutocompleteWildcardLabel"] label').at(0).text() + ).toEqual('Row Label'); + }); + + test('it renders disabled if "isDisabled" is true', () => { + wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="valuesAutocompleteWildcard"] input').prop('disabled') + ).toBeTruthy(); + }); + + test('it renders loading if "isLoading" is true', () => { + wrapper = mount( + + ); + wrapper.find('[data-test-subj="valuesAutocompleteWildcard"] button').at(0).simulate('click'); + expect( + wrapper + .find('EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteWildcard-optionsList"]') + .prop('isLoading') + ).toBeTruthy(); + }); + + test('it allows user to clear values if "isClearable" is true', () => { + wrapper = mount( + + ); + + expect( + wrapper + .find('[data-test-subj="comboBoxInput"]') + .hasClass('euiComboBox__inputWrap-isClearable') + ).toBeTruthy(); + }); + + test('it correctly displays selected value', () => { + wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="valuesAutocompleteWildcard"] EuiComboBoxPill').at(0).text() + ).toEqual('/opt/*/app.dmg'); + }); + + test('it invokes "onChange" when new value created', async () => { + const mockOnChange = jest.fn(); + wrapper = mount( + + ); + + ( + wrapper.find(EuiComboBox).props() as unknown as { + onCreateOption: (a: string) => void; + } + ).onCreateOption('/opt/*/app.dmg'); + + expect(mockOnChange).toHaveBeenCalledWith('/opt/*/app.dmg'); + }); + + test('it invokes "onChange" when new value selected', async () => { + const mockOnChange = jest.fn(); + wrapper = mount( + + ); + + ( + wrapper.find(EuiComboBox).props() as unknown as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + } + ).onChange([{ label: 'value 1' }]); + + expect(mockOnChange).toHaveBeenCalledWith('value 1'); + }); + + test('it refreshes autocomplete with search query when new value searched', () => { + wrapper = mount( + + ); + act(() => { + ( + wrapper.find(EuiComboBox).props() as unknown as { + onSearchChange: (a: string) => void; + } + ).onSearchChange('A:\\Some Folder\\inc*.exe'); + }); + + expect(useFieldValueAutocomplete).toHaveBeenCalledWith({ + autocompleteService: autocompleteStartMock, + fieldValue: '', + indexPattern: { + fields, + id: '1234', + title: 'logs-endpoint.events.*', + }, + operatorType: 'wildcard', + query: 'A:\\Some Folder\\inc*.exe', + selectedField: getField('file.path.text'), + }); + }); +}); diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.tsx new file mode 100644 index 00000000000000..159267c3386de7 --- /dev/null +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.tsx @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useMemo, useState, useEffect, memo } from 'react'; +import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; +import { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; + +import { uniq } from 'lodash'; + +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +type AutocompleteStart = any; + +import * as i18n from '../translations'; +import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete'; +import { + getGenericComboBoxProps, + GetGenericComboBoxPropsReturn, +} from '../get_generic_combo_box_props'; +import { paramIsValid } from '../param_is_valid'; + +const SINGLE_SELECTION = { asPlainText: true }; + +interface AutocompleteFieldWildcardProps { + placeholder: string; + selectedField: DataViewFieldBase | undefined; + selectedValue: string | undefined; + indexPattern: DataViewBase | undefined; + isLoading: boolean; + isDisabled?: boolean; + isClearable?: boolean; + isRequired?: boolean; + fieldInputWidth?: number; + rowLabel?: string; + autocompleteService: AutocompleteStart; + onChange: (arg: string) => void; + onError: (arg: boolean) => void; + onWarning: (arg: boolean) => void; + warning?: string; +} + +export const AutocompleteFieldWildcardComponent: React.FC = memo( + ({ + autocompleteService, + placeholder, + rowLabel, + selectedField, + selectedValue, + indexPattern, + isLoading, + isDisabled = false, + isClearable = false, + isRequired = false, + fieldInputWidth, + onChange, + onError, + onWarning, + warning, + }): JSX.Element => { + const [searchQuery, setSearchQuery] = useState(''); + const [touched, setIsTouched] = useState(false); + const [error, setError] = useState(undefined); + const [isLoadingSuggestions, , suggestions] = useFieldValueAutocomplete({ + autocompleteService, + fieldValue: selectedValue, + indexPattern, + operatorType: OperatorTypeEnum.WILDCARD, + query: searchQuery, + selectedField, + }); + const getLabel = useCallback((option: string): string => option, []); + const optionsMemo = useMemo((): string[] => { + const valueAsStr = String(selectedValue); + return selectedValue != null && selectedValue.trim() !== '' + ? uniq([valueAsStr, ...suggestions]) + : suggestions; + }, [suggestions, selectedValue]); + const selectedOptionsMemo = useMemo((): string[] => { + const valueAsStr = String(selectedValue); + return selectedValue ? [valueAsStr] : []; + }, [selectedValue]); + + const handleError = useCallback( + (err: string | undefined): void => { + setError((existingErr): string | undefined => { + const oldErr = existingErr != null; + const newErr = err != null; + if (oldErr !== newErr && onError != null) { + onError(newErr); + } + + return err; + }); + }, + [setError, onError] + ); + + const handleWarning = useCallback( + (warn: string | undefined): void => { + onWarning(warn !== undefined); + }, + [onWarning] + ); + + const { comboOptions, labels, selectedComboOptions } = useMemo( + (): GetGenericComboBoxPropsReturn => + getGenericComboBoxProps({ + getLabel, + options: optionsMemo, + selectedOptions: selectedOptionsMemo, + }), + [optionsMemo, selectedOptionsMemo, getLabel] + ); + + const handleValuesChange = useCallback( + (newOptions: EuiComboBoxOptionOption[]): void => { + const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]); + handleError(undefined); + handleWarning(undefined); + onChange(newValue ?? ''); + }, + [handleError, handleWarning, labels, onChange, optionsMemo] + ); + + const handleSearchChange = useCallback( + (searchVal: string): void => { + if (searchVal.trim() !== '' && selectedField != null) { + const err = paramIsValid(searchVal, selectedField, isRequired, touched); + handleError(err); + handleWarning(warning); + setSearchQuery(searchVal); + } + }, + [handleError, isRequired, selectedField, touched, warning, handleWarning] + ); + + const handleCreateOption = useCallback( + (option: string): boolean | undefined => { + const err = paramIsValid(option, selectedField, isRequired, touched); + handleError(err); + handleWarning(warning); + + if (err != null) { + // Explicitly reject the user's input + return false; + } else { + onChange(option); + return undefined; + } + }, + [isRequired, onChange, selectedField, touched, handleError, handleWarning, warning] + ); + + const setIsTouchedValue = useCallback((): void => { + setIsTouched(true); + + const err = paramIsValid(selectedValue, selectedField, isRequired, true); + handleError(err); + handleWarning(warning); + }, [ + setIsTouched, + handleError, + selectedValue, + selectedField, + isRequired, + handleWarning, + warning, + ]); + + const inputPlaceholder = useMemo((): string => { + if (isLoading || isLoadingSuggestions) { + return i18n.LOADING; + } else if (selectedField == null) { + return i18n.SELECT_FIELD_FIRST; + } else { + return placeholder; + } + }, [isLoading, selectedField, isLoadingSuggestions, placeholder]); + + const isLoadingState = useMemo( + (): boolean => isLoading || isLoadingSuggestions, + [isLoading, isLoadingSuggestions] + ); + + useEffect((): void => { + setError(undefined); + if (onError != null) { + onError(false); + } + if (onWarning != null) { + onWarning(false); + } + }, [selectedField, onError, onWarning]); + + const defaultInput = useMemo((): JSX.Element => { + return ( + + + + ); + }, [ + comboOptions, + error, + fieldInputWidth, + handleCreateOption, + handleSearchChange, + handleValuesChange, + inputPlaceholder, + isClearable, + isDisabled, + isLoadingState, + rowLabel, + selectedComboOptions, + selectedField, + setIsTouchedValue, + warning, + ]); + + return defaultInput; + } +); + +AutocompleteFieldWildcardComponent.displayName = 'AutocompleteFieldWildcard'; diff --git a/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts b/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts index d25dc5d45c9ec2..b3def81c43360a 100644 --- a/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts +++ b/packages/kbn-securitysolution-autocomplete/src/fields/index.mock.ts @@ -309,6 +309,14 @@ export const fields: DataViewFieldBase[] = [ readFromDocValues: false, subType: { nested: { path: 'nestedField.nestedChild' } }, }, + { + name: 'file.path.text', + type: 'string', + esTypes: ['text'], + searchable: true, + aggregatable: false, + subType: { multi: { parent: 'file.path' } }, + }, ] as unknown as DataViewFieldBase[]; export const getField = (name: string) => fields.find((field) => field.name === name); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts index 9ed9c6358c3971..24e4d759989ebc 100644 --- a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts +++ b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.test.ts @@ -8,6 +8,7 @@ import { doesNotExistOperator, + EVENT_FILTERS_OPERATORS, EXCEPTION_OPERATORS, existsOperator, isNotOperator, @@ -40,6 +41,15 @@ describe('#getOperators', () => { expect(operator).toEqual([isOperator]); }); + test('it includes a "matches" operator when field is "file.path.text"', () => { + const operator = getOperators({ + name: 'file.path.text', + type: 'simple', + }); + + expect(operator).toEqual(EVENT_FILTERS_OPERATORS); + }); + test('it returns all operator types when field type is not null, boolean, or nested', () => { const operator = getOperators(getField('machine.os.raw')); diff --git a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts index e84dc33e676e63..643c330b15241b 100644 --- a/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts +++ b/packages/kbn-securitysolution-autocomplete/src/get_operators/index.ts @@ -10,6 +10,7 @@ import { DataViewFieldBase } from '@kbn/es-query'; import { EXCEPTION_OPERATORS, + EVENT_FILTERS_OPERATORS, OperatorOption, doesNotExistOperator, existsOperator, @@ -30,6 +31,8 @@ export const getOperators = (field: DataViewFieldBase | undefined): OperatorOpti return [isOperator, isNotOperator, existsOperator, doesNotExistOperator]; } else if (field.type === 'nested') { return [isOperator]; + } else if (field.name === 'file.path.text') { + return EVENT_FILTERS_OPERATORS; } else { return EXCEPTION_OPERATORS; } diff --git a/packages/kbn-securitysolution-autocomplete/src/index.ts b/packages/kbn-securitysolution-autocomplete/src/index.ts index 5fcb3f954189ad..fcb1ea6b2cde64 100644 --- a/packages/kbn-securitysolution-autocomplete/src/index.ts +++ b/packages/kbn-securitysolution-autocomplete/src/index.ts @@ -11,6 +11,7 @@ export * from './field_value_exists'; export * from './field_value_lists'; export * from './field_value_match'; export * from './field_value_match_any'; +export * from './field_value_wildcard'; export * from './filter_field_to_list'; export * from './get_generic_combo_box_props'; export * from './get_operators'; diff --git a/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx index 6d1622f0fa95f7..48f5cbf25b91ce 100644 --- a/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/operator/index.test.tsx @@ -129,6 +129,9 @@ describe('operator', () => { { label: 'is not in list', }, + { + label: 'matches', + }, ]); }); @@ -196,6 +199,30 @@ describe('operator', () => { ]); }); + test('it only displays subset of operators if field name is "file.path.text"', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"]`).at(0).prop('options') + ).toEqual([ + { label: 'is' }, + { label: 'is not' }, + { label: 'is one of' }, + { label: 'is not one of' }, + { label: 'matches' }, + ]); + }); + test('it invokes "onChange" when option selected', () => { const mockOnChange = jest.fn(); const wrapper = mount( diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.mock.ts index 176a6357b30e72..64f7e1aceeb2af 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.mock.ts @@ -10,11 +10,11 @@ import { EndpointEntriesArray } from '.'; import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; -import { getEndpointEntryMatchWildcard } from '../entry_match_wildcard/index.mock'; +import { getEndpointEntryMatchWildcardMock } from '../entry_match_wildcard/index.mock'; export const getEndpointEntriesArrayMock = (): EndpointEntriesArray => [ getEndpointEntryMatchMock(), getEndpointEntryMatchAnyMock(), getEndpointEntryNestedMock(), - getEndpointEntryMatchWildcard(), + getEndpointEntryMatchWildcardMock(), ]; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.test.ts index ca852e15c5c2a7..08235d35e921f6 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entries/index.test.ts @@ -20,7 +20,7 @@ import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; import { getEndpointEntriesArrayMock } from './index.mock'; import { getEntryListMock } from '../../entries_list/index.mock'; import { getEntryExistsMock } from '../../entries_exist/index.mock'; -import { getEndpointEntryMatchWildcard } from '../entry_match_wildcard/index.mock'; +import { getEndpointEntryMatchWildcardMock } from '../entry_match_wildcard/index.mock'; describe('Endpoint', () => { describe('entriesArray', () => { @@ -101,7 +101,7 @@ describe('Endpoint', () => { }); test('it should validate an array with wildcard entry', () => { - const payload = [getEndpointEntryMatchWildcard()]; + const payload = [getEndpointEntryMatchWildcardMock()]; const decoded = endpointEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.mock.ts index e001552277e0ca..842e046ea67eeb 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.mock.ts @@ -9,7 +9,7 @@ import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../../constants/index.mock'; import { EndpointEntryMatchWildcard } from './index'; -export const getEndpointEntryMatchWildcard = (): EndpointEntryMatchWildcard => ({ +export const getEndpointEntryMatchWildcardMock = (): EndpointEntryMatchWildcard => ({ field: FIELD, operator: OPERATOR, type: WILDCARD, diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.test.ts new file mode 100644 index 00000000000000..9671e721f20c61 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/endpoint/entry_match_wildcard/index.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointEntryMatchWildcardMock } from './index.mock'; +import { EndpointEntryMatchWildcard, endpointEntryMatchWildcard } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEntryMatchWildcardMock } from '../../entry_match_wildcard/index.mock'; + +describe('endpointEntryMatchWildcard', () => { + test('it should validate an entry', () => { + const payload = getEndpointEntryMatchWildcardMock(); + const decoded = endpointEntryMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate when "operator" is "excluded"', () => { + const payload = getEntryMatchWildcardMock(); + payload.operator = 'excluded'; + const decoded = endpointEntryMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "excluded" supplied to "operator"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEndpointEntryMatchWildcardMock(), + field: '', + }; + const decoded = endpointEntryMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is not string', () => { + const payload: Omit & { value: string[] } = { + ...getEndpointEntryMatchWildcardMock(), + value: ['some value'], + }; + const decoded = endpointEntryMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is empty string', () => { + const payload: Omit & { value: string } = { + ...getEndpointEntryMatchWildcardMock(), + value: '', + }; + const decoded = endpointEntryMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "wildcard"', () => { + const payload: Omit & { type: string } = { + ...getEndpointEntryMatchWildcardMock(), + type: 'match', + }; + const decoded = endpointEntryMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EndpointEntryMatchWildcard & { + extraKey?: string; + } = getEndpointEntryMatchWildcardMock(); + payload.extraKey = 'some value'; + const decoded = endpointEntryMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchWildcardMock()); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/autocomplete_operators/index.ts b/packages/kbn-securitysolution-list-utils/src/autocomplete_operators/index.ts index 101076bdfcfffc..ac3236528b6718 100644 --- a/packages/kbn-securitysolution-list-utils/src/autocomplete_operators/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/autocomplete_operators/index.ts @@ -85,11 +85,21 @@ export const isNotInListOperator: OperatorOption = { value: 'is_not_in_list', }; +export const matchesOperator: OperatorOption = { + message: i18n.translate('lists.exceptions.matchesOperatorLabel', { + defaultMessage: 'matches', + }), + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.WILDCARD, + value: 'matches', +}; + export const EVENT_FILTERS_OPERATORS: OperatorOption[] = [ isOperator, isNotOperator, isOneOfOperator, isNotOneOfOperator, + matchesOperator, ]; export const EXCEPTION_OPERATORS: OperatorOption[] = [ @@ -101,6 +111,7 @@ export const EXCEPTION_OPERATORS: OperatorOption[] = [ doesNotExistOperator, isInListOperator, isNotInListOperator, + matchesOperator, ]; export const EXCEPTION_OPERATORS_SANS_LISTS: OperatorOption[] = [ diff --git a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts index 394d4f02b8772b..eabf8dfa33f98b 100644 --- a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts @@ -172,6 +172,8 @@ export const getOperatorType = (item: BuilderEntry): OperatorTypeEnum => { return OperatorTypeEnum.MATCH; case 'match_any': return OperatorTypeEnum.MATCH_ANY; + case 'wildcard': + return OperatorTypeEnum.WILDCARD; case 'list': return OperatorTypeEnum.LIST; default: @@ -207,6 +209,7 @@ export const getEntryValue = (item: BuilderEntry): string | string[] | undefined switch (item.type) { case OperatorTypeEnum.MATCH: case OperatorTypeEnum.MATCH_ANY: + case OperatorTypeEnum.WILDCARD: return item.value; case OperatorTypeEnum.EXISTS: return undefined; @@ -523,6 +526,54 @@ export const getEntryOnMatchChange = ( } }; +/** + * Determines proper entry update when user updates value + * when operator is of type "wildcard" + * + * @param item - current exception item entry values + * @param newField - newly entered value + * + */ +export const getEntryOnWildcardChange = ( + item: FormattedBuilderEntry, + newField: string +): { index: number; updatedEntry: BuilderEntry } => { + const { nested, parent, entryIndex, field, operator } = item; + + if (nested != null && parent != null) { + const fieldName = field != null ? field.name.split('.').slice(-1)[0] : ''; + + return { + index: parent.parentIndex, + updatedEntry: { + ...parent.parent, + entries: [ + ...parent.parent.entries.slice(0, entryIndex), + { + field: fieldName, + id: item.id, + operator: operator.operator, + type: OperatorTypeEnum.WILDCARD, + value: newField, + }, + ...parent.parent.entries.slice(entryIndex + 1), + ], + }, + }; + } else { + return { + index: entryIndex, + updatedEntry: { + field: field != null ? field.name : '', + id: item.id, + operator: operator.operator, + type: OperatorTypeEnum.WILDCARD, + value: newField, + }, + }; + } +}; + /** * On operator change, determines whether value needs to be cleared or not * @@ -563,6 +614,15 @@ export const getEntryFromOperator = ( operator: selectedOperator.operator, type: OperatorTypeEnum.LIST, }; + case 'wildcard': + return { + field: fieldValue, + id: currentEntry.id, + operator: selectedOperator.operator, + type: OperatorTypeEnum.WILDCARD, + value: + isSameOperatorType && typeof currentEntry.value === 'string' ? currentEntry.value : '', + }; default: return { field: fieldValue, diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel index cfb6b722ea2e63..70ecc2712d4af7 100644 --- a/packages/kbn-securitysolution-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -28,11 +28,13 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ + "//packages/kbn-i18n", "@npm//tslib", - "@npm//uuid", + "@npm//uuid" ] TYPES_DEPS = [ + "//packages/kbn-i18n:npm_module_types", "@npm//tslib", "@npm//@types/jest", "@npm//@types/node", diff --git a/packages/kbn-securitysolution-utils/src/index.ts b/packages/kbn-securitysolution-utils/src/index.ts index 755bbd2203dffd..e3442a3ec7dc81 100644 --- a/packages/kbn-securitysolution-utils/src/index.ts +++ b/packages/kbn-securitysolution-utils/src/index.ts @@ -8,3 +8,4 @@ export * from './add_remove_id_to_item'; export * from './transform_data_to_ndjson'; +export * from './path_validations'; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts similarity index 84% rename from x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts rename to packages/kbn-securitysolution-utils/src/path_validations/index.test.ts index 952a2fa234ace5..ee2d8764a30afb 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts @@ -1,12 +1,84 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import { isPathValid, hasSimpleExecutableName } from './validations'; -import { OperatingSystem, ConditionEntryField } from '../../types'; +import { + isPathValid, + hasSimpleExecutableName, + OperatingSystem, + ConditionEntryField, + validateFilePathInput, + FILENAME_WILDCARD_WARNING, + FILEPATH_WARNING, +} from '.'; + +describe('validateFilePathInput', () => { + describe('windows', () => { + const os = OperatingSystem.WINDOWS; + + it('warns on wildcard in file name at the end of the path', () => { + expect(validateFilePathInput({ os, value: 'c:\\path*.exe' })).toEqual( + FILENAME_WILDCARD_WARNING + ); + }); + + it('warns on unix paths or non-windows paths', () => { + expect(validateFilePathInput({ os, value: '/opt/bin' })).toEqual(FILEPATH_WARNING); + }); + + it('warns on malformed paths', () => { + expect(validateFilePathInput({ os, value: 'c:\\path/opt' })).toEqual(FILEPATH_WARNING); + expect(validateFilePathInput({ os, value: '1242' })).toEqual(FILEPATH_WARNING); + expect(validateFilePathInput({ os, value: 'w12efdfa' })).toEqual(FILEPATH_WARNING); + }); + }); + describe('unix paths', () => { + const os = + parseInt((Math.random() * 2).toString(), 10) === 1 + ? OperatingSystem.MAC + : OperatingSystem.LINUX; + + it('warns on wildcard in file name at the end of the path', () => { + expect(validateFilePathInput({ os, value: '/opt/bin*' })).toEqual(FILENAME_WILDCARD_WARNING); + }); + + it('warns on windows paths', () => { + expect(validateFilePathInput({ os, value: 'd:\\path\\file.exe' })).toEqual(FILEPATH_WARNING); + }); + + it('warns on malformed paths', () => { + expect(validateFilePathInput({ os, value: 'opt/bin\\file.exe' })).toEqual(FILEPATH_WARNING); + expect(validateFilePathInput({ os, value: '1242' })).toEqual(FILEPATH_WARNING); + expect(validateFilePathInput({ os, value: 'w12efdfa' })).toEqual(FILEPATH_WARNING); + }); + }); +}); + +describe('No Warnings', () => { + it('should not show warnings on non path entries ', () => { + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.HASH, + type: 'match', + value: '5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e', + }) + ).toEqual(true); + + expect( + isPathValid({ + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.SIGNER, + type: 'match', + value: '', + }) + ).toEqual(true); + }); +}); describe('Unacceptable Windows wildcard paths', () => { it('should not accept paths that do not have a folder name with a wildcard ', () => { diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.ts new file mode 100644 index 00000000000000..82d2cc3151b90e --- /dev/null +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.ts @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const FILENAME_WILDCARD_WARNING = i18n.translate('utils.filename.wildcardWarning', { + defaultMessage: `A wildcard in the filename will affect the endpoint's performance`, +}); + +export const FILEPATH_WARNING = i18n.translate('utils.filename.pathWarning', { + defaultMessage: `Path may be formed incorrectly; verify value`, +}); + +export const enum ConditionEntryField { + HASH = 'process.hash.*', + PATH = 'process.executable.caseless', + SIGNER = 'process.Ext.code_signature', +} + +export const enum OperatingSystem { + LINUX = 'linux', + MAC = 'macos', + WINDOWS = 'windows', +} + +export type TrustedAppEntryTypes = 'match' | 'wildcard'; +/* + * regex to match executable names + * starts matching from the eol of the path + * file names with a single or multiple spaces (for spaced names) + * and hyphens and combinations of these that produce complex names + * such as: + * c:\home\lib\dmp.dmp + * c:\home\lib\my-binary-app-+/ some/ x/ dmp.dmp + * /home/lib/dmp.dmp + * /home/lib/my-binary-app+-\ some\ x\ dmp.dmp + */ +export const WIN_EXEC_PATH = /(\\[-\w]+|\\[-\w]+[\.]+[\w]+)$/i; +export const UNIX_EXEC_PATH = /(\/[-\w]+|\/[-\w]+[\.]+[\w]+)$/i; + +export const validateFilePathInput = ({ + os, + value = '', +}: { + os: OperatingSystem; + value?: string; +}): string | undefined => { + const textInput = value.trim(); + const isValidFilePath = isPathValid({ + os, + field: 'file.path.text', + type: 'wildcard', + value: textInput, + }); + const hasSimpleFileName = hasSimpleExecutableName({ + os, + type: 'wildcard', + value: textInput, + }); + + if (!textInput.length) { + return FILEPATH_WARNING; + } + + if (isValidFilePath) { + if (!hasSimpleFileName) { + return FILENAME_WILDCARD_WARNING; + } + } else { + return FILEPATH_WARNING; + } +}; + +export const hasSimpleExecutableName = ({ + os, + type, + value, +}: { + os: OperatingSystem; + type: TrustedAppEntryTypes; + value: string; +}): boolean => { + if (type === 'wildcard') { + return os === OperatingSystem.WINDOWS ? WIN_EXEC_PATH.test(value) : UNIX_EXEC_PATH.test(value); + } + return true; +}; + +export const isPathValid = ({ + os, + field, + type, + value, +}: { + os: OperatingSystem; + field: ConditionEntryField | 'file.path.text'; + type: TrustedAppEntryTypes; + value: string; +}): boolean => { + if (field === ConditionEntryField.PATH || field === 'file.path.text') { + if (type === 'wildcard') { + return os === OperatingSystem.WINDOWS + ? isWindowsWildcardPathValid(value) + : isLinuxMacWildcardPathValid(value); + } + return doesPathMatchRegex({ value, os }); + } + return true; +}; + +const doesPathMatchRegex = ({ os, value }: { os: OperatingSystem; value: string }): boolean => { + if (os === OperatingSystem.WINDOWS) { + const filePathRegex = + /^[a-z]:(?:|\\\\[^<>:"'/\\|?*]+\\[^<>:"'/\\|?*]+|%\w+%|)[\\](?:[^<>:"'/\\|?*]+[\\/])*([^<>:"'/\\|?*])+$/i; + return filePathRegex.test(value); + } + return /^(\/|(\/[\w\-]+)+|\/[\w\-]+\.[\w]+|(\/[\w-]+)+\/[\w\-]+\.[\w]+)$/i.test(value); +}; + +const isWindowsWildcardPathValid = (path: string): boolean => { + const firstCharacter = path[0]; + const lastCharacter = path.slice(-1); + const trimmedValue = path.trim(); + const hasSlash = /\//.test(trimmedValue); + if (path.length === 0) { + return false; + } else if ( + hasSlash || + trimmedValue.length !== path.length || + firstCharacter === '^' || + lastCharacter === '\\' || + !hasWildcard({ path, isWindowsPath: true }) + ) { + return false; + } else { + return true; + } +}; + +const isLinuxMacWildcardPathValid = (path: string): boolean => { + const firstCharacter = path[0]; + const lastCharacter = path.slice(-1); + const trimmedValue = path.trim(); + if (path.length === 0) { + return false; + } else if ( + trimmedValue.length !== path.length || + firstCharacter !== '/' || + lastCharacter === '/' || + path.length > 1024 === true || + path.includes('//') === true || + !hasWildcard({ path, isWindowsPath: false }) + ) { + return false; + } else { + return true; + } +}; + +const hasWildcard = ({ + path, + isWindowsPath, +}: { + path: string; + isWindowsPath: boolean; +}): boolean => { + for (const pathComponent of path.split(isWindowsPath ? '\\' : '/')) { + if (/[\*|\?]+/.test(pathComponent) === true) { + return true; + } + } + return false; +}; diff --git a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts index dec381fb04b565..96ebcd79c4e436 100644 --- a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts +++ b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts @@ -31,6 +31,7 @@ export interface Test { file?: string; parent?: Suite; isPassed: () => boolean; + pending?: boolean; } export interface Runner extends EventEmitter { diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index 53ec36dfbe55c8..9f21d8bd595b5d 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import Path from 'path'; +import { writeFileSync, mkdirSync } from 'fs'; +import Path, { dirname } from 'path'; import { ToolingLog } from '@kbn/dev-utils'; import { REPO_ROOT } from '@kbn/utils'; @@ -98,6 +99,15 @@ export class FunctionalTestRunner { reporterOptions ); + // there's a bug in mocha's dry run, see https://github.com/mochajs/mocha/issues/4838 + // until we can update to a mocha version where this is fixed, we won't actually + // execute the mocha dry run but simulate it by reading the suites and tests of + // the mocha object and writing a report file with similar structure to the json report + // (just leave out some execution details like timing, retry and erros) + if (config.get('mochaOpts.dryRun')) { + return this.simulateMochaDryRun(mocha); + } + await this.lifecycle.beforeTests.trigger(mocha.suite); this.log.info('Starting tests'); @@ -244,4 +254,62 @@ export class FunctionalTestRunner { this.closed = true; await this.lifecycle.cleanup.trigger(); } + + simulateMochaDryRun(mocha: any) { + interface TestEntry { + file: string; + title: string; + fullTitle: string; + } + + const getFullTitle = (node: Test | Suite): string => { + const parentTitle = node.parent && getFullTitle(node.parent); + return parentTitle ? `${parentTitle} ${node.title}` : node.title; + }; + + let suiteCount = 0; + const passes: TestEntry[] = []; + const pending: TestEntry[] = []; + + const collectTests = (suite: Suite) => { + for (const subSuite of suite.suites) { + suiteCount++; + for (const test of subSuite.tests) { + const testEntry = { + title: test.title, + fullTitle: getFullTitle(test), + file: test.file || '', + }; + if (test.pending) { + pending.push(testEntry); + } else { + passes.push(testEntry); + } + } + collectTests(subSuite); + } + }; + + collectTests(mocha.suite); + + const reportData = { + stats: { + suites: suiteCount, + tests: passes.length + pending.length, + passes: passes.length, + pending: pending.length, + failures: 0, + }, + tests: [...passes, ...pending], + passes, + pending, + failures: [], + }; + + const reportPath = mocha.options.reporterOptions.output; + mkdirSync(dirname(reportPath), { recursive: true }); + writeFileSync(reportPath, JSON.stringify(reportData, null, 2), 'utf8'); + + return 0; + } } diff --git a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js index 6e25e4c073ab0b..417fc8e10aeca2 100644 --- a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js +++ b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js @@ -37,5 +37,10 @@ export default function () { captureLogOutput: false, sendToCiStats: false, }, + servers: { + elasticsearch: { + port: 1234, + }, + }, }; } diff --git a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js index 4c87b53b5753b2..067528c4ae120b 100644 --- a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js +++ b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js @@ -13,4 +13,9 @@ export default () => ({ mochaReporter: { sendToCiStats: false, }, + servers: { + elasticsearch: { + port: 1234, + }, + }, }); diff --git a/packages/kbn-test/src/functional_test_runner/integration_tests/failure_hooks.test.js b/packages/kbn-test/src/functional_test_runner/integration_tests/failure_hooks.test.js index 0d986a1602e124..47ae51ca62f131 100644 --- a/packages/kbn-test/src/functional_test_runner/integration_tests/failure_hooks.test.js +++ b/packages/kbn-test/src/functional_test_runner/integration_tests/failure_hooks.test.js @@ -61,7 +61,7 @@ describe('failure hooks', function () { expect(tests).toHaveLength(0); } catch (error) { - console.error('full log output', linesCopy.join('\n')); + error.message += `\n\nfull log output:${linesCopy.join('\n')}`; throw error; } }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.1.js b/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.1.js index 123bc8b9bc201b..afcad01c4ab924 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.1.js +++ b/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.1.js @@ -9,5 +9,10 @@ export default function () { return { testFiles: ['config.1'], + servers: { + elasticsearch: { + port: 1234, + }, + }, }; } diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.2.js b/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.2.js index 2dd4c96186fcda..692a3de786723d 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.2.js +++ b/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.2.js @@ -11,5 +11,10 @@ export default async function ({ readConfigFile }) { return { testFiles: [...config1.get('testFiles'), 'config.2'], + servers: { + elasticsearch: { + port: 1234, + }, + }, }; } diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/config.test.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.test.ts index 88c1fd99f0014e..d551e7a884b416 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/config.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/config.test.ts @@ -15,6 +15,11 @@ describe('Config', () => { services: { foo: () => 42, }, + servers: { + elasticsearch: { + port: 1234, + }, + }, }, primary: true, path: process.cwd(), diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index f65cb3c41f4218..42a77b85ddc6c3 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -17,19 +17,33 @@ const ID_PATTERN = /^[a-zA-Z0-9_]+$/; // it will search both --inspect and --inspect-brk const INSPECTING = !!process.execArgv.find((arg) => arg.includes('--inspect')); -const urlPartsSchema = () => +const maybeRequireKeys = (keys: string[], schemas: Record) => { + if (!keys.length) { + return schemas; + } + + const withRequires: Record = {}; + for (const [key, schema] of Object.entries(schemas)) { + withRequires[key] = keys.includes(key) ? schema.required() : schema; + } + return withRequires; +}; + +const urlPartsSchema = ({ requiredKeys }: { requiredKeys?: string[] } = {}) => Joi.object() - .keys({ - protocol: Joi.string().valid('http', 'https').default('http'), - hostname: Joi.string().hostname().default('localhost'), - port: Joi.number(), - auth: Joi.string().regex(/^[^:]+:.+$/, 'username and password separated by a colon'), - username: Joi.string(), - password: Joi.string(), - pathname: Joi.string().regex(/^\//, 'start with a /'), - hash: Joi.string().regex(/^\//, 'start with a /'), - certificateAuthorities: Joi.array().items(Joi.binary()).optional(), - }) + .keys( + maybeRequireKeys(requiredKeys ?? [], { + protocol: Joi.string().valid('http', 'https').default('http'), + hostname: Joi.string().hostname().default('localhost'), + port: Joi.number(), + auth: Joi.string().regex(/^[^:]+:.+$/, 'username and password separated by a colon'), + username: Joi.string(), + password: Joi.string(), + pathname: Joi.string().regex(/^\//, 'start with a /'), + hash: Joi.string().regex(/^\//, 'start with a /'), + certificateAuthorities: Joi.array().items(Joi.binary()).optional(), + }) + ) .default(); const appUrlPartsSchema = () => @@ -170,7 +184,9 @@ export const schema = Joi.object() servers: Joi.object() .keys({ kibana: urlPartsSchema(), - elasticsearch: urlPartsSchema(), + elasticsearch: urlPartsSchema({ + requiredKeys: ['port'], + }), }) .default(), diff --git a/renovate.json b/renovate.json index 9b673a5a9ccf65..0b6ca59edefe2e 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base", ":disableDependencyDashboard"], + "extends": ["config:base"], "ignorePaths": ["**/__fixtures__/**", "**/fixtures/**"], "enabledManagers": ["npm"], "baseBranches": ["main", "7.16", "7.15"], diff --git a/src/core/public/apm_system.test.ts b/src/core/public/apm_system.test.ts index 842d5de7e5afcb..0a3a1dee63e57e 100644 --- a/src/core/public/apm_system.test.ts +++ b/src/core/public/apm_system.test.ts @@ -13,6 +13,7 @@ import type { Transaction } from '@elastic/apm-rum'; import { ApmSystem } from './apm_system'; import { Subject } from 'rxjs'; import { InternalApplicationStart } from './application/types'; +import { executionContextServiceMock } from './execution_context/execution_context_service.mock'; const initMock = init as jest.Mocked; const apmMock = apm as DeeplyMockedKeys; @@ -96,6 +97,7 @@ describe('ApmSystem', () => { application: { currentAppId$, } as any as InternalApplicationStart, + executionContext: executionContextServiceMock.createInternalStartContract(), }); expect(mark).toHaveBeenCalledWith('apm-start'); @@ -118,6 +120,7 @@ describe('ApmSystem', () => { application: { currentAppId$, } as any as InternalApplicationStart, + executionContext: executionContextServiceMock.createInternalStartContract(), }); currentAppId$.next('myapp'); @@ -145,6 +148,7 @@ describe('ApmSystem', () => { application: { currentAppId$, } as any as InternalApplicationStart, + executionContext: executionContextServiceMock.createInternalStartContract(), }); currentAppId$.next('myapp'); diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 2231f394381f04..4e116c0a0182dd 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -10,6 +10,7 @@ import type { ApmBase, AgentConfigOptions, Transaction } from '@elastic/apm-rum' import { modifyUrl } from '@kbn/std'; import { CachedResourceObserver } from './apm_resource_counter'; import type { InternalApplicationStart } from './application'; +import { ExecutionContextStart } from './execution_context'; /** "GET protocol://hostname:port/pathname" */ const HTTP_REQUEST_TRANSACTION_NAME_REGEX = @@ -27,6 +28,7 @@ interface ApmConfig extends AgentConfigOptions { interface StartDeps { application: InternalApplicationStart; + executionContext: ExecutionContextStart; } export class ApmSystem { @@ -34,6 +36,7 @@ export class ApmSystem { private pageLoadTransaction?: Transaction; private resourceObserver: CachedResourceObserver; private apm?: ApmBase; + /** * `apmConfig` would be populated with relevant APM RUM agent * configuration if server is started with elastic.apm.* config. @@ -64,6 +67,15 @@ export class ApmSystem { this.markPageLoadStart(); + start.executionContext.context$.subscribe((c) => { + // We're using labels because we want the context to be indexed + // https://www.elastic.co/guide/en/apm/get-started/current/metadata.html + const apmContext = start.executionContext.getAsLabels(); + this.apm?.addLabels(apmContext); + }); + + // TODO: Start a new transaction every page change instead of every app change. + /** * Register listeners for navigation changes and capture them as * route-change transactions after Kibana app is bootstrapped diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 3d3331d54792bb..1aa01c13dd3751 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -31,6 +31,7 @@ import { DeprecationsService } from './deprecations'; import { ThemeService } from './theme'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; +import { ExecutionContextService } from './execution_context'; interface Params { rootDomElement: HTMLElement; @@ -87,6 +88,7 @@ export class CoreSystem { private readonly theme: ThemeService; private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; + private readonly executionContext: ExecutionContextService; private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { @@ -121,6 +123,7 @@ export class CoreSystem { this.application = new ApplicationService(); this.integrations = new IntegrationsService(); this.deprecations = new DeprecationsService(); + this.executionContext = new ExecutionContextService(); this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); @@ -137,7 +140,13 @@ export class CoreSystem { }); await this.integrations.setup(); this.docLinks.setup(); - const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); + + const executionContext = this.executionContext.setup(); + const http = this.http.setup({ + injectedMetadata, + fatalErrors: this.fatalErrorsSetup, + executionContext, + }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); const theme = this.theme.setup({ injectedMetadata }); @@ -153,6 +162,7 @@ export class CoreSystem { notifications, theme, uiSettings, + executionContext, }; // Services that do not expose contracts at setup @@ -201,6 +211,11 @@ export class CoreSystem { targetDomElement: notificationsTargetDomElement, }); const application = await this.application.start({ http, theme, overlays }); + + const executionContext = this.executionContext.start({ + curApp$: application.currentAppId$, + }); + const chrome = await this.chrome.start({ application, docLinks, @@ -216,6 +231,7 @@ export class CoreSystem { application, chrome, docLinks, + executionContext, http, theme, savedObjects, @@ -248,6 +264,7 @@ export class CoreSystem { return { application, + executionContext, }; } catch (error) { if (this.fatalErrorsSetup) { diff --git a/src/core/public/execution_context/execution_context_service.mock.ts b/src/core/public/execution_context/execution_context_service.mock.ts new file mode 100644 index 00000000000000..3941aa333cfa2a --- /dev/null +++ b/src/core/public/execution_context/execution_context_service.mock.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PublicMethodsOf } from '@kbn/utility-types'; +import { BehaviorSubject } from 'rxjs'; + +import { ExecutionContextService, ExecutionContextSetup } from './execution_context_service'; + +const createContractMock = (): jest.Mocked => ({ + context$: new BehaviorSubject({}), + clear: jest.fn(), + set: jest.fn(), + get: jest.fn(), + getAsLabels: jest.fn(), + withGlobalContext: jest.fn(), +}); + +const createMock = (): jest.Mocked> => ({ + setup: jest.fn().mockReturnValue(createContractMock()), + start: jest.fn().mockReturnValue(createContractMock()), + stop: jest.fn(), +}); + +export const executionContextServiceMock = { + create: createMock, + createSetupContract: createContractMock, + createStartContract: createContractMock, + createInternalSetupContract: createContractMock, + createInternalStartContract: createContractMock, +}; diff --git a/src/core/public/execution_context/execution_context_service.ts b/src/core/public/execution_context/execution_context_service.ts new file mode 100644 index 00000000000000..bf13a7351f9b5f --- /dev/null +++ b/src/core/public/execution_context/execution_context_service.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isEqual, isUndefined, omitBy } from 'lodash'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { CoreService, KibanaExecutionContext } from '../../types'; + +// Should be exported from elastic/apm-rum +export type LabelValue = string | number | boolean; + +export interface Labels { + [key: string]: LabelValue; +} + +/** + * Kibana execution context. + * Used to provide execution context to Elasticsearch, reporting, performance monitoring, etc. + * @public + **/ +export interface ExecutionContextSetup { + /** + * The current context observable + **/ + context$: Observable; + /** + * Set the current top level context + **/ + set(c$: KibanaExecutionContext): void; + /** + * Get the current top level context + **/ + get(): KibanaExecutionContext; + /** + * clears the context + **/ + clear(): void; + /** + * returns apm labels + **/ + getAsLabels(): Labels; + /** + * merges the current top level context with the specific event context + **/ + withGlobalContext(context?: KibanaExecutionContext): KibanaExecutionContext; +} + +/** + * See {@link ExecutionContextSetup}. + * @public + */ +export type ExecutionContextStart = ExecutionContextSetup; + +export interface StartDeps { + curApp$: Observable; +} + +/** @internal */ +export class ExecutionContextService + implements CoreService +{ + private context$: BehaviorSubject = new BehaviorSubject({}); + private appId?: string; + private subscription: Subscription = new Subscription(); + private contract?: ExecutionContextSetup; + + public setup() { + this.contract = { + context$: this.context$.asObservable(), + clear: () => { + this.context$.next({}); + }, + set: (c: KibanaExecutionContext) => { + const newVal = { + ...this.context$.value, + ...c, + }; + if (!isEqual(newVal, this.context$.value)) { + this.context$.next(newVal); + } + }, + get: () => { + return this.mergeContext(); + }, + getAsLabels: () => { + return this.removeUndefined({ + name: this.appId, + id: this.context$.value?.id, + page: this.context$.value?.page, + }) as Labels; + }, + withGlobalContext: (context: KibanaExecutionContext) => { + return this.mergeContext(context); + }, + }; + + return this.contract; + } + + public start({ curApp$ }: StartDeps) { + const start = this.contract!; + + // Track app id changes and clear context on app change + this.subscription.add( + curApp$.subscribe((appId) => { + this.appId = appId; + start.clear(); + }) + ); + + return start; + } + + public stop() { + this.subscription.unsubscribe(); + } + + private removeUndefined(context: KibanaExecutionContext = {}) { + return omitBy(context, isUndefined); + } + + private mergeContext(context: KibanaExecutionContext = {}): KibanaExecutionContext { + return { + name: this.appId, + url: window.location.pathname, + ...this.context$.value, + ...context, + }; + } +} diff --git a/src/core/public/execution_context/index.ts b/src/core/public/execution_context/index.ts index b15a967ac714aa..f160b0ecea67b5 100644 --- a/src/core/public/execution_context/index.ts +++ b/src/core/public/execution_context/index.ts @@ -8,3 +8,5 @@ export type { KibanaExecutionContext } from '../../types'; export { ExecutionContextContainer } from './execution_context_container'; +export { ExecutionContextService } from './execution_context_service'; +export type { ExecutionContextSetup, ExecutionContextStart } from './execution_context_service'; diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index e897d69057e029..0691e2443c17f5 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -15,6 +15,7 @@ import { first } from 'rxjs/operators'; import { Fetch } from './fetch'; import { BasePath } from './base_path'; import { HttpResponse, HttpFetchOptionsWithPath } from './types'; +import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; function delay(duration: number) { return new Promise((r) => setTimeout(r, duration)); @@ -23,9 +24,11 @@ function delay(duration: number) { const BASE_PATH = 'http://localhost/myBase'; describe('Fetch', () => { + const executionContextMock = executionContextServiceMock.createSetupContract(); const fetchInstance = new Fetch({ basePath: new BasePath(BASE_PATH), kibanaVersion: 'VERSION', + executionContext: executionContextMock, }); afterEach(() => { fetchMock.restore(); @@ -230,13 +233,15 @@ describe('Fetch', () => { it('should inject context headers if provided', async () => { fetchMock.get('*', {}); + const context = { + type: 'test-type', + name: 'test-name', + description: 'test-description', + id: '42', + }; + executionContextMock.withGlobalContext.mockReturnValue(context); await fetchInstance.fetch('/my/path', { - context: { - type: 'test-type', - name: 'test-name', - description: 'test-description', - id: '42', - }, + context, }); expect(fetchMock.lastOptions()!.headers).toMatchObject({ @@ -245,6 +250,29 @@ describe('Fetch', () => { }); }); + it('should include top level context context headers if provided', async () => { + fetchMock.get('*', {}); + + const context = { + type: 'test-type', + name: 'test-name', + description: 'test-description', + id: '42', + }; + executionContextMock.withGlobalContext.mockReturnValue({ + ...context, + name: 'banana', + }); + await fetchInstance.fetch('/my/path', { + context, + }); + + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'x-kbn-context': + '%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22banana%22%2C%22description%22%3A%22test-description%22%2C%22id%22%3A%2242%22%7D', + }); + }); + it('should return response', async () => { fetchMock.get('*', { foo: 'bar' }); const json = await fetchInstance.fetch('/my/path'); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index 4ee81f4b47aa0c..9a333161e1b7a5 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { omitBy } from 'lodash'; +import { isEmpty, omitBy } from 'lodash'; import { format } from 'url'; import { BehaviorSubject } from 'rxjs'; @@ -22,11 +22,12 @@ import { HttpFetchError } from './http_fetch_error'; import { HttpInterceptController } from './http_intercept_controller'; import { interceptRequest, interceptResponse } from './intercept'; import { HttpInterceptHaltError } from './http_intercept_halt_error'; -import { ExecutionContextContainer } from '../execution_context'; +import { ExecutionContextContainer, ExecutionContextSetup } from '../execution_context'; interface Params { basePath: IBasePath; kibanaVersion: string; + executionContext: ExecutionContextSetup; } const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; @@ -107,6 +108,7 @@ export class Fetch { }; private createRequest(options: HttpFetchOptionsWithPath): Request { + const context = this.params.executionContext.withGlobalContext(options.context); // Merge and destructure options out that are not applicable to the Fetch API. const { query, @@ -125,7 +127,7 @@ export class Fetch { 'Content-Type': 'application/json', ...options.headers, 'kbn-version': this.params.kibanaVersion, - ...(options.context ? new ExecutionContextContainer(options.context).toHeader() : {}), + ...(!isEmpty(context) ? new ExecutionContextContainer(context).toHeader() : {}), }), }; diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 2b41991904d974..698fa876433d41 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -14,6 +14,7 @@ import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.moc import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { HttpService } from './http_service'; import { Observable } from 'rxjs'; +import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; describe('interceptors', () => { afterEach(() => fetchMock.restore()); @@ -22,9 +23,10 @@ describe('interceptors', () => { fetchMock.get('*', {}); const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const executionContext = executionContextServiceMock.createSetupContract(); const httpService = new HttpService(); - const setup = httpService.setup({ fatalErrors, injectedMetadata }); + const setup = httpService.setup({ fatalErrors, injectedMetadata, executionContext }); const setupInterceptor = jest.fn(); setup.intercept({ request: setupInterceptor }); @@ -47,7 +49,8 @@ describe('#setup()', () => { const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); const fatalErrors = fatalErrorsServiceMock.createSetupContract(); const httpService = new HttpService(); - httpService.setup({ fatalErrors, injectedMetadata }); + const executionContext = executionContextServiceMock.createSetupContract(); + httpService.setup({ fatalErrors, injectedMetadata, executionContext }); const loadingServiceSetup = loadingServiceMock.setup.mock.results[0].value; // We don't verify that this Observable comes from Fetch#getLoadingCount$() to avoid complex mocking expect(loadingServiceSetup.addLoadingCountSource).toHaveBeenCalledWith(expect.any(Observable)); @@ -59,7 +62,8 @@ describe('#stop()', () => { const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); const fatalErrors = fatalErrorsServiceMock.createSetupContract(); const httpService = new HttpService(); - httpService.setup({ fatalErrors, injectedMetadata }); + const executionContext = executionContextServiceMock.createSetupContract(); + httpService.setup({ fatalErrors, injectedMetadata, executionContext }); httpService.start(); httpService.stop(); expect(loadingServiceMock.stop).toHaveBeenCalled(); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index a9719cfce67aff..390130da4e07dd 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -15,10 +15,12 @@ import { LoadingCountService } from './loading_count_service'; import { Fetch } from './fetch'; import { CoreService } from '../../types'; import { ExternalUrlService } from './external_url_service'; +import { ExecutionContextSetup } from '../execution_context'; interface HttpDeps { injectedMetadata: InjectedMetadataSetup; fatalErrors: FatalErrorsSetup; + executionContext: ExecutionContextSetup; } /** @internal */ @@ -27,14 +29,15 @@ export class HttpService implements CoreService { private readonly loadingCount = new LoadingCountService(); private service?: HttpSetup; - public setup({ injectedMetadata, fatalErrors }: HttpDeps): HttpSetup { + public setup({ injectedMetadata, fatalErrors, executionContext }: HttpDeps): HttpSetup { const kibanaVersion = injectedMetadata.getKibanaVersion(); const basePath = new BasePath( injectedMetadata.getBasePath(), injectedMetadata.getServerBasePath(), injectedMetadata.getPublicBaseUrl() ); - const fetchService = new Fetch({ basePath, kibanaVersion }); + + const fetchService = new Fetch({ basePath, kibanaVersion, executionContext }); const loadingCount = this.loadingCount.setup({ fatalErrors }); loadingCount.addLoadingCountSource(fetchService.getRequestCount$()); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index ded7db9c6f892d..50d8bf304ddf8f 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -65,6 +65,7 @@ import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; import { DeprecationsServiceStart } from './deprecations'; import type { ThemeServiceSetup, ThemeServiceStart } from './theme'; +import { ExecutionContextSetup, ExecutionContextStart } from './execution_context'; export type { PackageInfo, @@ -194,7 +195,11 @@ export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './type export { URL_MAX_LENGTH } from './core_app'; -export type { KibanaExecutionContext } from './execution_context'; +export type { + KibanaExecutionContext, + ExecutionContextSetup, + ExecutionContextStart, +} from './execution_context'; /** * Core services exposed to the `Plugin` setup lifecycle @@ -221,6 +226,8 @@ export interface CoreSetup, any, any]>, []>(() => Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract]) @@ -76,6 +78,7 @@ function createCoreStartMock({ basePath = '' } = {}) { application: applicationServiceMock.createStartContract(), chrome: chromeServiceMock.createStartContract(), docLinks: docLinksServiceMock.createStartContract(), + executionContext: executionContextServiceMock.createStartContract(), http: httpServiceMock.createStartContract({ basePath }), i18n: i18nServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 345aea4b6cac87..8c085d3de23693 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -88,6 +88,7 @@ export function createPluginSetupContext< registerAppUpdater: (statusUpdater$) => deps.application.registerAppUpdater(statusUpdater$), }, fatalErrors: deps.fatalErrors, + executionContext: deps.executionContext, http: deps.http, notifications: deps.notifications, uiSettings: deps.uiSettings, @@ -129,6 +130,7 @@ export function createPluginStartContext< getUrlForApp: deps.application.getUrlForApp, }, docLinks: deps.docLinks, + executionContext: deps.executionContext, http: deps.http, chrome: omit(deps.chrome, 'getComponent'), i18n: deps.i18n, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index c4e3b7990ba32b..40976424b7edd7 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -36,6 +36,7 @@ import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; import { themeServiceMock } from '../theme/theme_service.mock'; +import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; export let mockPluginInitializers: Map; @@ -85,6 +86,7 @@ describe('PluginsService', () => { mockSetupDeps = { application: applicationServiceMock.createInternalSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), + executionContext: executionContextServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createSetupContract(), @@ -100,6 +102,7 @@ describe('PluginsService', () => { mockStartDeps = { application: applicationServiceMock.createInternalStartContract(), docLinks: docLinksServiceMock.createStartContract(), + executionContext: executionContextServiceMock.createStartContract(), http: httpServiceMock.createStartContract(), chrome: chromeServiceMock.createStartContract(), i18n: i18nServiceMock.createStartContract(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 4cf845de4617db..e3f2822b5a7c8d 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -401,6 +401,8 @@ export interface CoreSetup; @@ -429,6 +431,8 @@ export interface CoreStart { // (undocumented) docLinks: DocLinksStart; // (undocumented) + executionContext: ExecutionContextStart; + // (undocumented) fatalErrors: FatalErrorsStart; // (undocumented) http: HttpStart; @@ -461,6 +465,7 @@ export class CoreSystem { // (undocumented) start(): Promise<{ application: InternalApplicationStart; + executionContext: ExecutionContextSetup; } | undefined>; // (undocumented) stop(): void; @@ -511,6 +516,20 @@ export interface ErrorToastOptions extends ToastOptions { toastMessage?: string; } +// @public +export interface ExecutionContextSetup { + clear(): void; + context$: Observable; + get(): KibanaExecutionContext; + // Warning: (ae-forgotten-export) The symbol "Labels" needs to be exported by the entry point index.d.ts + getAsLabels(): Labels_2; + set(c$: KibanaExecutionContext): void; + withGlobalContext(context?: KibanaExecutionContext): KibanaExecutionContext; +} + +// @public +export type ExecutionContextStart = ExecutionContextSetup; + // @public export interface FatalErrorInfo { // (undocumented) @@ -751,9 +770,10 @@ export interface IUiSettingsClient { // @public export type KibanaExecutionContext = { - readonly type: string; - readonly name: string; - readonly id: string; + readonly type?: string; + readonly name?: string; + readonly page?: string; + readonly id?: string; readonly description?: string; readonly url?: string; child?: KibanaExecutionContext; @@ -1522,6 +1542,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:173:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:183:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/execution_context/execution_context_container.ts b/src/core/server/execution_context/execution_context_container.ts index de895bcff5ecbd..1528df6c231408 100644 --- a/src/core/server/execution_context/execution_context_container.ts +++ b/src/core/server/execution_context/execution_context_container.ts @@ -50,9 +50,10 @@ export interface IExecutionContextContainer { } function stringify(ctx: KibanaExecutionContext): string { - const stringifiedCtx = `${encodeURIComponent(ctx.type)}:${encodeURIComponent( + const encodeURIComponentIfNotEmpty = (val?: string) => encodeURIComponent(val || ''); + const stringifiedCtx = `${encodeURIComponentIfNotEmpty(ctx.type)}:${encodeURIComponentIfNotEmpty( ctx.name - )}:${encodeURIComponent(ctx.id!)}`; + )}:${encodeURIComponentIfNotEmpty(ctx.id)}`; return ctx.child ? `${stringifiedCtx};${stringify(ctx.child)}` : stringifiedCtx; } diff --git a/src/core/server/execution_context/execution_context_service.mock.ts b/src/core/server/execution_context/execution_context_service.mock.ts index 68aab7a5b84f84..85768eb423f26b 100644 --- a/src/core/server/execution_context/execution_context_service.mock.ts +++ b/src/core/server/execution_context/execution_context_service.mock.ts @@ -26,6 +26,7 @@ const createExecutionContextMock = () => { get: jest.fn(), getParentContextFrom: jest.fn(), getAsHeader: jest.fn(), + getAsLabels: jest.fn(), }; mock.withContext.mockImplementation(withContextMock); return mock; @@ -38,6 +39,7 @@ const createInternalSetupContractMock = () => { const createSetupContractMock = () => { const mock: jest.Mocked = { withContext: jest.fn(), + getAsLabels: jest.fn(), }; mock.withContext.mockImplementation(withContextMock); return mock; diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts index 6e2b809e230431..03ae2cb36c9ec2 100644 --- a/src/core/server/execution_context/execution_context_service.ts +++ b/src/core/server/execution_context/execution_context_service.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ import { AsyncLocalStorage } from 'async_hooks'; +import apm from 'elastic-apm-node'; +import { isUndefined, omitBy } from 'lodash'; import type { Subscription } from 'rxjs'; import type { CoreService, KibanaExecutionContext } from '../../types'; @@ -39,6 +41,10 @@ export interface IExecutionContext { * returns serialized representation to send as a header **/ getAsHeader(): string | undefined; + /** + * returns apm labels + **/ + getAsLabels(): apm.Labels; } /** @@ -61,6 +67,7 @@ export interface ExecutionContextSetup { * The nested calls stack the registered context on top of each other. **/ withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; + getAsLabels(): apm.Labels; } /** @@ -97,6 +104,7 @@ export class ExecutionContextService setRequestId: this.setRequestId.bind(this), get: this.get.bind(this), getAsHeader: this.getAsHeader.bind(this), + getAsLabels: this.getAsLabels.bind(this), }; } @@ -108,6 +116,7 @@ export class ExecutionContextService withContext: this.withContext.bind(this), get: this.get.bind(this), getAsHeader: this.getAsHeader.bind(this), + getAsLabels: this.getAsLabels.bind(this), }; } @@ -161,4 +170,18 @@ export class ExecutionContextService return `${requestId}${executionContextStr}`; } + + private getAsLabels() { + if (!this.enabled) return {}; + const executionContext = this.contextStore.getStore()?.toJSON(); + + return omitBy( + { + name: executionContext?.name, + id: executionContext?.id, + page: executionContext?.page, + }, + isUndefined + ); + } } diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 4623b09b19e297..813f8e97843326 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -21,6 +21,7 @@ import agent from 'elastic-apm-node'; import type { Duration } from 'moment'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; +import apm from 'elastic-apm-node'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; import type { InternalExecutionContextSetup } from '../execution_context'; @@ -338,7 +339,11 @@ export class HttpServer { const requestId = getRequestId(request, config.requestId); const parentContext = executionContext?.getParentContextFrom(request.headers); - if (parentContext) executionContext?.set(parentContext); + + if (executionContext && parentContext) { + executionContext.set(parentContext); + apm.addLabels(executionContext.getAsLabels()); + } executionContext?.setRequestId(requestId); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 18abbe88c49135..983a12627b7f38 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -161,6 +161,7 @@ export function createPluginSetupContext( }, executionContext: { withContext: deps.executionContext.withContext, + getAsLabels: deps.executionContext.getAsLabels, }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index d7ed4928e1cf5d..5fe1942ed8453b 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -7,6 +7,7 @@ /// import { AddConfigDeprecation } from '@kbn/config'; +import apm from 'elastic-apm-node'; import Boom from '@hapi/boom'; import { ByteSizeValue } from '@kbn/config-schema'; import { CliArgs } from '@kbn/config'; @@ -994,6 +995,8 @@ export class EventLoopDelaysMonitor { // @public (undocumented) export interface ExecutionContextSetup { + // (undocumented) + getAsLabels(): apm.Labels; withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; } @@ -1319,9 +1322,10 @@ export interface IUiSettingsClient { // @public export type KibanaExecutionContext = { - readonly type: string; - readonly name: string; - readonly id: string; + readonly type?: string; + readonly name?: string; + readonly page?: string; + readonly id?: string; readonly description?: string; readonly url?: string; child?: KibanaExecutionContext; diff --git a/src/core/test_helpers/http_test_setup.ts b/src/core/test_helpers/http_test_setup.ts index 468034dffceb9b..67b340898aab47 100644 --- a/src/core/test_helpers/http_test_setup.ts +++ b/src/core/test_helpers/http_test_setup.ts @@ -9,6 +9,7 @@ import { HttpService } from '../public/http'; import { fatalErrorsServiceMock } from '../public/fatal_errors/fatal_errors_service.mock'; import { injectedMetadataServiceMock } from '../public/injected_metadata/injected_metadata_service.mock'; +import { executionContextServiceMock } from '../public/execution_context/execution_context_service.mock'; export type SetupTap = ( injectedMetadata: ReturnType, @@ -28,7 +29,8 @@ export function setup(tap: SetupTap = defaultTap) { tap(injectedMetadata, fatalErrors); const httpService = new HttpService(); - const http = httpService.setup({ fatalErrors, injectedMetadata }); + const executionContext = executionContextServiceMock.createSetupContract(); + const http = httpService.setup({ fatalErrors, injectedMetadata, executionContext }); return { httpService, injectedMetadata, fatalErrors, http }; } diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index a63a8d406db406..87026fc3fb9b2d 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -229,10 +229,6 @@ export function createTestServers({ writeTo: process.stdout, }); - log.indent(6); - log.info('starting elasticsearch'); - log.indent(4); - const es = createTestEsCluster( defaultsDeep({}, settings.es ?? {}, { log, @@ -240,8 +236,6 @@ export function createTestServers({ }) ); - log.indent(-4); - // Add time for KBN and adding users adjustTimeout(es.getStartTimeout() + 100000); diff --git a/src/core/types/execution_context.ts b/src/core/types/execution_context.ts index 1b985a73f410bd..d790b8d855fd4e 100644 --- a/src/core/types/execution_context.ts +++ b/src/core/types/execution_context.ts @@ -16,11 +16,13 @@ export type KibanaExecutionContext = { /** * Kibana application initated an operation. * */ - readonly type: string; // 'visualization' | 'actions' | 'server' | ..; - /** public name of a user-facing feature */ - readonly name: string; // 'TSVB' | 'Lens' | 'action_execution' | ..; + readonly type?: string; // 'visualization' | 'actions' | 'server' | ..; + /** public name of an application or a user-facing feature */ + readonly name?: string; // 'TSVB' | 'Lens' | 'action_execution' | ..; + /** a stand alone, logical unit such as an application page or tab */ + readonly page?: string; /** unique value to identify the source */ - readonly id: string; + readonly id?: string; /** human readable description. For example, a vis title, action name */ readonly description?: string; /** in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url */ diff --git a/src/dev/build/lib/runner.ts b/src/dev/build/lib/runner.ts index 1fccd884cc4f95..e12f7d24cfc491 100644 --- a/src/dev/build/lib/runner.ts +++ b/src/dev/build/lib/runner.ts @@ -33,29 +33,30 @@ export interface Task { export function createRunner({ config, log }: Options) { async function execTask(desc: string, task: Task | GlobalTask, lastArg: any) { log.info(desc); - log.indent(4); - - const start = Date.now(); - const time = () => { - const sec = (Date.now() - start) / 1000; - const minStr = sec > 60 ? `${Math.floor(sec / 60)} min ` : ''; - const secStr = `${Math.round(sec % 60)} sec`; - return chalk.dim(`${minStr}${secStr}`); - }; - try { - await task.run(config, log, lastArg); - log.success(chalk.green('✓'), time()); - } catch (error) { - if (!isErrorLogged(error)) { - log.error(`failure ${time()}`); - log.error(error); - markErrorLogged(error); - } + await log.indent(4, async () => { + const start = Date.now(); + const time = () => { + const sec = (Date.now() - start) / 1000; + const minStr = sec > 60 ? `${Math.floor(sec / 60)} min ` : ''; + const secStr = `${Math.round(sec % 60)} sec`; + return chalk.dim(`${minStr}${secStr}`); + }; + + try { + await task.run(config, log, lastArg); + log.success(chalk.green('✓'), time()); + } catch (error) { + if (!isErrorLogged(error)) { + log.error(`failure ${time()}`); + log.error(error); + markErrorLogged(error); + } - throw error; + throw error; + } + }); } finally { - log.indent(-4); log.write(''); } } diff --git a/src/dev/build/tasks/bundle_fleet_packages.ts b/src/dev/build/tasks/bundle_fleet_packages.ts index 7d0dc6a25a47e3..b2faed818b55b6 100644 --- a/src/dev/build/tasks/bundle_fleet_packages.ts +++ b/src/dev/build/tasks/bundle_fleet_packages.ts @@ -11,7 +11,7 @@ import JSON5 from 'json5'; import { readCliArgs } from '../args'; import { Task, read, downloadToDisk } from '../lib'; -const BUNDLED_PACKAGES_DIR = 'x-pack/plugins/fleet/server/bundled_packages'; +const BUNDLED_PACKAGES_DIR = 'x-pack/plugins/fleet/target/bundled_packages'; interface FleetPackage { name: string; diff --git a/src/dev/build/tasks/notice_file_task.ts b/src/dev/build/tasks/notice_file_task.ts index 43d95858e7b8de..2a446e73723e89 100644 --- a/src/dev/build/tasks/notice_file_task.ts +++ b/src/dev/build/tasks/notice_file_task.ts @@ -18,13 +18,15 @@ export const CreateNoticeFile: Task = { async run(config, log, build) { log.info('Generating notice from source'); - log.indent(4); - const noticeFromSource = await generateNoticeFromSource({ - productName: 'Kibana', - directory: build.resolvePath(), - log, - }); - log.indent(-4); + const noticeFromSource = await log.indent( + 4, + async () => + await generateNoticeFromSource({ + productName: 'Kibana', + directory: build.resolvePath(), + log, + }) + ); log.info('Discovering installed packages'); const packages = await getInstalledPackages({ diff --git a/src/dev/eslint/run_eslint_with_types.ts b/src/dev/eslint/run_eslint_with_types.ts index 0f2a10d07d681a..d7f2482fcb26b9 100644 --- a/src/dev/eslint/run_eslint_with_types.ts +++ b/src/dev/eslint/run_eslint_with_types.ts @@ -109,9 +109,9 @@ export function runEslintWithTypes() { return undefined; } else { log.error(`${project.name} failed`); - log.indent(4); - log.write(proc.all); - log.indent(-4); + log.indent(4, () => { + log.write(proc.all); + }); return project; } }, concurrency), diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 860886811da547..932fdaf6c2e280 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -61,9 +61,6 @@ export const IGNORE_FILE_GLOBS = [ 'x-pack/plugins/maps/server/fonts/**/*', - // Bundled package names typically use a format like ${pkgName}-${pkgVersion}, so don't lint them - 'x-pack/plugins/fleet/server/bundled_packages/**/*', - // Bazel default files '**/WORKSPACE.bazel', '**/BUILD.bazel', diff --git a/src/dev/prs/run_update_prs_cli.ts b/src/dev/prs/run_update_prs_cli.ts index 4d82c704cad275..cde7f495b1eb60 100644 --- a/src/dev/prs/run_update_prs_cli.ts +++ b/src/dev/prs/run_update_prs_cli.ts @@ -148,12 +148,9 @@ run( await init(); for (const pr of prs) { log.info('pr #%s', pr.number); - log.indent(4); - try { + await log.indent(4, async () => { await updatePr(pr); - } finally { - log.indent(-4); - } + }); } }, { diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index e5657dd4663a38..b4d350c44174cb 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -45,7 +45,7 @@ export const PROJECTS = [ { name: 'enterprise_search/shared/cypress' } ), createProject( - 'x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/tsconfig.json', + 'x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/tsconfig.json', { name: 'enterprise_search/overview/cypress' } ), createProject( diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index 7881a4a0ca8805..46cf778bf0139a 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -27,6 +27,7 @@ export const INTEGRATION_CATEGORY_DISPLAY = { kubernetes: 'Kubernetes', languages: 'Languages', message_queue: 'Message queue', + microsoft_365: 'Microsoft 365', monitoring: 'Monitoring', network: 'Network', notification: 'Notification', @@ -41,6 +42,7 @@ export const INTEGRATION_CATEGORY_DISPLAY = { // Kibana added communications: 'Communications', + enterprise_search: 'Enterprise search', file_storage: 'File storage', language_client: 'Language client', upload_file: 'Upload a file', diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 7aedbe9e110019..a32e6643a4e3a1 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useMemo } from 'react'; import { useDashboardSelector } from './state'; import { useDashboardAppState } from './hooks'; -import { useKibana } from '../../../kibana_react/public'; +import { useKibana, useExecutionContext } from '../../../kibana_react/public'; import { getDashboardBreadcrumb, getDashboardTitle, @@ -48,6 +48,12 @@ export function DashboardApp({ [core.notifications.toasts, history, uiSettings] ); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'app', + id: savedDashboardId || 'new', + }); + const dashboardState = useDashboardSelector((state) => state.dashboardStateReducer); const dashboardAppState = useDashboardAppState({ history, diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 98b3d761f350ec..26641dc52e3d54 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -220,11 +220,7 @@ export const useDashboardAppState = ({ savedDashboard, data, executionContext: { - type: 'application', - name: 'dashboard', - id: savedDashboard.id ?? 'unsaved_dashboard', description: savedDashboard.title, - url: history.location.pathname, }, }); if (canceled || !dashboardContainer) { diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 5b53fc47e06a41..65374ad723f232 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -35,6 +35,7 @@ import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays'; import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage'; +import { useExecutionContext } from '../../../../kibana_react/public'; export interface DashboardListingProps { kbnUrlStateStorage: IKbnUrlStateStorage; @@ -67,6 +68,11 @@ export const DashboardListing = ({ dashboardSessionStorage.getDashboardIdsWithUnsavedChanges() ); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'list', + }); + // Set breadcrumbs useEffect useEffect(() => { setBreadcrumbs([ diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index fa9b7ac86a7fa6..d717af0107e8c3 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -43,13 +43,10 @@ import { buildExistsFilter as oldBuildExistsFilter, toggleFilterNegated as oldtoggleFilterNegated, Filter as oldFilter, - RangeFilterMeta as oldRangeFilterMeta, RangeFilterParams as oldRangeFilterParams, ExistsFilter as oldExistsFilter, - PhrasesFilter as oldPhrasesFilter, PhraseFilter as oldPhraseFilter, MatchAllFilter as oldMatchAllFilter, - CustomFilter as oldCustomFilter, RangeFilter as oldRangeFilter, KueryNode as oldKueryNode, FilterMeta as oldFilterMeta, @@ -58,17 +55,11 @@ import { compareFilters as oldCompareFilters, COMPARE_ALL_OPTIONS as OLD_COMPARE_ALL_OPTIONS, dedupFilters as oldDedupFilters, - isFilter as oldIsFilter, onlyDisabledFiltersChanged as oldOnlyDisabledFiltersChanged, uniqFilters as oldUniqFilters, FilterStateStore, } from '@kbn/es-query'; -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -const isFilter = oldIsFilter; /** * @deprecated Import from the "@kbn/es-query" package directly instead. * @removeBy 8.1 @@ -295,12 +286,6 @@ const FILTERS = oldFILTERS; */ type Filter = oldFilter; -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -type RangeFilterMeta = oldRangeFilterMeta; - /** * @deprecated Import from the "@kbn/es-query" package directly instead. * @removeBy 8.1 @@ -313,12 +298,6 @@ type RangeFilterParams = oldRangeFilterParams; */ type ExistsFilter = oldExistsFilter; -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -type PhrasesFilter = oldPhrasesFilter; - /** * @deprecated Import from the "@kbn/es-query" package directly instead. * @removeBy 8.1 @@ -331,12 +310,6 @@ type PhraseFilter = oldPhraseFilter; */ type MatchAllFilter = oldMatchAllFilter; -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -type CustomFilter = oldCustomFilter; - /** * @deprecated Import from the "@kbn/es-query" package directly instead. * @removeBy 8.1 @@ -368,13 +341,10 @@ type EsQueryConfig = oldEsQueryConfig; export type { Filter, - RangeFilterMeta, RangeFilterParams, ExistsFilter, - PhrasesFilter, PhraseFilter, MatchAllFilter, - CustomFilter, RangeFilter, KueryNode, FilterMeta, @@ -415,7 +385,6 @@ export { buildExistsFilter, toggleFilterNegated, FILTERS, - isFilter, isFilterDisabled, dedupFilters, onlyDisabledFiltersChanged, diff --git a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts index 8510acf1572c79..963fe024c1f8fc 100644 --- a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts +++ b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.test.ts @@ -126,7 +126,7 @@ describe('getAggsFormats', () => { const mapping = { id: 'multi_terms', params: { - paramsPerField: Array(terms.length).fill({ id: 'terms' }), + paramsPerField: [{ id: 'terms' }, { id: 'terms' }, { id: 'terms' }], }, }; @@ -141,7 +141,7 @@ describe('getAggsFormats', () => { const mapping = { id: 'multi_terms', params: { - paramsPerField: Array(terms.length).fill({ id: 'terms' }), + paramsPerField: [{ id: 'terms' }, { id: 'terms' }, { id: 'terms' }], separator: ' - ', }, }; diff --git a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts index f14f981fdec65c..e514cc24f93cad 100644 --- a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts +++ b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts @@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n'; import { FieldFormat, FieldFormatInstanceType, + FieldFormatParams, FieldFormatsContentType, IFieldFormat, SerializedFieldFormat, @@ -133,11 +134,20 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta static id = 'multi_terms'; static hidden = true; + private formatCache: Map, FieldFormat> = new Map(); + convert = (val: unknown, type: FieldFormatsContentType) => { const params = this._params; - const formats = (params.paramsPerField as SerializedFieldFormat[]).map((fieldParams) => - getFieldFormat({ id: fieldParams.id, params: fieldParams }) - ); + const formats = (params.paramsPerField as SerializedFieldFormat[]).map((fieldParams) => { + const isCached = this.formatCache.has(fieldParams); + const cachedFormat = + this.formatCache.get(fieldParams) || + getFieldFormat({ id: fieldParams.id, params: fieldParams }); + if (!isCached) { + this.formatCache.set(fieldParams, cachedFormat); + } + return cachedFormat; + }); if (String(val) === '__other__') { return params.otherBucketLabel; diff --git a/src/plugins/data/public/deprecated.ts b/src/plugins/data/public/deprecated.ts index 0458a940482de2..6a6c7bbb2cd2c1 100644 --- a/src/plugins/data/public/deprecated.ts +++ b/src/plugins/data/public/deprecated.ts @@ -36,16 +36,12 @@ import { luceneStringToDsl, decorateQuery, FILTERS, - isFilter, isFilters, KueryNode, RangeFilter, - RangeFilterMeta, RangeFilterParams, ExistsFilter, - PhrasesFilter, PhraseFilter, - CustomFilter, MatchAllFilter, EsQueryConfig, FilterStateStore, @@ -139,16 +135,13 @@ export const esFilters = { export type { KueryNode, RangeFilter, - RangeFilterMeta, RangeFilterParams, ExistsFilter, - PhrasesFilter, PhraseFilter, - CustomFilter, MatchAllFilter, EsQueryConfig, }; -export { isFilter, isFilters }; +export { isFilters }; /** * @deprecated Import helpers from the "@kbn/es-query" package directly instead. diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index 968dd870489fe0..f1e2e903cadde3 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -119,6 +119,7 @@ describe('SearchInterceptor', () => { }), uiSettings: mockCoreSetup.uiSettings, http: mockCoreSetup.http, + executionContext: mockCoreSetup.executionContext, session: sessionService, theme: themeServiceMock.createSetupContract(), }); @@ -543,7 +544,12 @@ describe('SearchInterceptor', () => { .catch(() => {}); expect(fetchMock.mock.calls[0][0]).toEqual( expect.objectContaining({ - options: { sessionId, isStored: true, isRestore: true, strategy: 'ese' }, + options: { + sessionId, + isStored: true, + isRestore: true, + strategy: 'ese', + }, }) ); diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index 7dc1ce6dee0786..251e191d589e3d 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -61,6 +61,7 @@ import { SearchAbortController } from './search_abort_controller'; export interface SearchInterceptorDeps { bfetch: BfetchPublicSetup; http: CoreSetup['http']; + executionContext: CoreSetup['executionContext']; uiSettings: CoreSetup['uiSettings']; startServices: Promise<[CoreStart, any, unknown]>; toasts: ToastsSetup; @@ -297,10 +298,14 @@ export class SearchInterceptor { } }) as Promise; } else { + const { executionContext, ...rest } = options || {}; return this.batchedFetch( { request, - options: this.getSerializableOptions(options), + options: this.getSerializableOptions({ + ...rest, + executionContext: this.deps.executionContext.withGlobalContext(executionContext), + }), }, abortSignal ); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 961599de713df8..b21ad44c7bd6de 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -89,7 +89,7 @@ export class SearchService implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} public setup( - { http, getStartServices, notifications, uiSettings, theme }: CoreSetup, + { http, getStartServices, notifications, uiSettings, executionContext, theme }: CoreSetup, { bfetch, expressions, usageCollection, nowProvider }: SearchServiceSetupDependencies ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); @@ -108,6 +108,7 @@ export class SearchService implements Plugin { this.searchInterceptor = new SearchInterceptor({ bfetch, toasts: notifications.toasts, + executionContext, http, uiSettings, startServices: getStartServices(), diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts index 314de4254851fb..25b1bd0d7009d4 100644 --- a/src/plugins/data/server/search/routes/bsearch.ts +++ b/src/plugins/data/server/search/routes/bsearch.ts @@ -9,6 +9,7 @@ import { catchError, first } from 'rxjs/operators'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import type { ExecutionContextSetup } from 'src/core/server'; +import apm from 'elastic-apm-node'; import { IKibanaSearchRequest, IKibanaSearchResponse, @@ -33,9 +34,10 @@ export function registerBsearchRoute( */ onBatchItem: async ({ request: requestData, options }) => { const { executionContext, ...restOptions } = options || {}; + return executionContextService.withContext(executionContext, () => { + apm.addLabels(executionContextService.getAsLabels()); - return executionContextService.withContext(executionContext, () => - search + return search .search(requestData, restOptions) .pipe( first(), @@ -49,8 +51,8 @@ export function registerBsearchRoute( }; }) ) - .toPromise() - ); + .toPromise(); + }); }, }; }); diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index a3ec8fc0a9af20..bcfde68abd99c1 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -15,8 +15,14 @@ import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { euiThemeVars } from '@kbn/ui-theme'; -import { ApplicationStart, ChromeStart, ScopedHistory, CoreTheme } from 'src/core/public'; -import { KibanaThemeProvider } from '../../kibana_react/public'; +import type { + ApplicationStart, + ChromeStart, + ScopedHistory, + CoreTheme, + ExecutionContextStart, +} from 'src/core/public'; +import { KibanaThemeProvider, useExecutionContext } from '../../kibana_react/public'; import type { DocTitleService, BreadcrumbService } from './services'; import { DevToolApp } from './dev_tool'; @@ -24,6 +30,7 @@ import { DevToolApp } from './dev_tool'; export interface AppServices { docTitleService: DocTitleService; breadcrumbService: BreadcrumbService; + executionContext: ExecutionContextStart; } interface DevToolsWrapperProps { @@ -64,6 +71,11 @@ function DevToolsWrapper({ breadcrumbService.setBreadcrumbs(activeDevTool.title); }, [activeDevTool, docTitleService, breadcrumbService]); + useExecutionContext(appServices.executionContext, { + type: 'application', + page: activeDevTool.id, + }); + return (
diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 1876bf278513e6..ee729c8f4400c0 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -61,7 +61,7 @@ export class DevToolsPlugin implements Plugin { element.classList.add('devAppWrapper'); const [core] = await getStartServices(); - const { application, chrome } = core; + const { application, chrome, executionContext } = core; this.docTitleService.setup(chrome.docTitle.change); this.breadcrumbService.setup(chrome.setBreadcrumbs); @@ -69,6 +69,7 @@ export class DevToolsPlugin implements Plugin { const appServices = { breadcrumbService: this.breadcrumbService, docTitleService: this.docTitleService, + executionContext, }; const { renderApp } = await import('./application'); diff --git a/src/plugins/discover/public/application/context/context_app.test.tsx b/src/plugins/discover/public/application/context/context_app.test.tsx index c9089a6c1111c1..cf1e7a98e01a37 100644 --- a/src/plugins/discover/public/application/context/context_app.test.tsx +++ b/src/plugins/discover/public/application/context/context_app.test.tsx @@ -46,6 +46,9 @@ describe('ContextApp test', () => { toastNotifications: { addDanger: () => {} }, navigation: mockNavigationPlugin, core: { + executionContext: { + set: jest.fn(), + }, notifications: { toasts: [] }, theme: { theme$: themeServiceMock.createStartContract().theme$ }, }, diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index 8d2a6b2c048155..dcf1c6b11e68ed 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -25,6 +25,7 @@ import { ContextAppContent } from './context_app_content'; import { SurrDocType } from './services/context'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useExecutionContext } from '../../../../kibana_react/public'; import { generateFilters } from '../../../../data/public'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -36,11 +37,17 @@ export interface ContextAppProps { export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { const services = useDiscoverServices(); - const { uiSettings, capabilities, indexPatterns, navigation, filterManager } = services; + const { uiSettings, capabilities, indexPatterns, navigation, filterManager, core } = services; const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'context', + id: indexPattern.id || '', + }); + /** * Context app state */ diff --git a/src/plugins/discover/public/application/doc/single_doc_route.tsx b/src/plugins/discover/public/application/doc/single_doc_route.tsx index d11c6bdca76a04..e2bdc6dc799e6d 100644 --- a/src/plugins/discover/public/application/doc/single_doc_route.tsx +++ b/src/plugins/discover/public/application/doc/single_doc_route.tsx @@ -16,6 +16,7 @@ import { withQueryParams } from '../../utils/with_query_params'; import { useMainRouteBreadcrumb } from '../../utils/use_navigation_props'; import { Doc } from './components/doc'; import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useExecutionContext } from '../../../../kibana_react/public'; export interface SingleDocRouteProps { /** @@ -31,11 +32,17 @@ export interface DocUrlParams { const SingleDoc = ({ id }: SingleDocRouteProps) => { const services = useDiscoverServices(); - const { chrome, timefilter } = services; + const { chrome, timefilter, core } = services; const { indexPatternId, index } = useParams(); const breadcrumb = useMainRouteBreadcrumb(); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'single-doc', + id: indexPatternId, + }); + useEffect(() => { chrome.setBreadcrumbs([ ...getRootBreadcrumbs(breadcrumb), diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index d5950085b94c72..dcf229d36b1e0e 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -24,6 +24,7 @@ import { LoadingIndicator } from '../../components/common/loading_indicator'; import { DiscoverError } from '../../components/common/error_alert'; import { useDiscoverServices } from '../../utils/use_discover_services'; import { getUrlTracker } from '../../kibana_services'; +import { useExecutionContext } from '../../../../kibana_react/public'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -50,6 +51,12 @@ export function DiscoverMainRoute() { >([]); const { id } = useParams(); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'app', + id: id || 'new', + }); + const navigateToOverview = useCallback(() => { core.application.navigateToApp('kibanaOverview', { path: '#' }); }, [core.application]); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts index 4c68eff54f579f..b1f736fa4b2242 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts @@ -117,7 +117,7 @@ describe('test fetchCharts', () => { }); }); - test('fetch$ is called with execution context containing savedSearch id', async () => { + test('fetch$ is called with request specific execution context', async () => { const fetch$Mock = jest.fn().mockReturnValue(of(requestResult)); savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; @@ -126,10 +126,6 @@ describe('test fetchCharts', () => { expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` Object { "description": "fetch chart data and total hits", - "id": "the-saved-search-id-with-timefield", - "name": "discover", - "type": "application", - "url": "/", } `); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.ts index 00cb9c43caccf2..1ea2594a89d97f 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.ts @@ -40,11 +40,7 @@ export function fetchChart( const chartAggConfigs = updateSearchSource(searchSource, interval, data); const executionContext = { - type: 'application', - name: 'discover', description: 'fetch chart data and total hits', - url: window.location.pathname, - id: savedSearch.id ?? '', }; const fetch$ = searchSource diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts index 000d3282c38b3c..1e73f5de3a3f66 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts @@ -57,10 +57,6 @@ describe('test fetchDocuments', () => { expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` Object { "description": "fetch total hits", - "id": "the-saved-search-id", - "name": "discover", - "type": "application", - "url": "/", } `); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts index dbf972265547ee..8338839e8b0acd 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -32,11 +32,7 @@ export const fetchDocuments = ( } const executionContext = { - type: 'application', - name: 'discover', description: 'fetch documents', - url: window.location.pathname, - id: savedSearch.id ?? '', }; const fetch$ = searchSource diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts index ba7b6a765aa2e9..a5485c1a2e2e90 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts @@ -51,10 +51,6 @@ describe('test fetchTotalHits', () => { expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` Object { "description": "fetch total hits", - "id": "the-saved-search-id", - "name": "discover", - "type": "application", - "url": "/", } `); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts index af2d55e23cf323..e696570f05cf0b 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts @@ -30,11 +30,7 @@ export function fetchTotalHits( } const executionContext = { - type: 'application', - name: 'discover', description: 'fetch total hits', - url: window.location.pathname, - id: savedSearch.id ?? '', }; const fetch$ = searchSource diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index baed839d411239..94e8b505f1dd20 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -39,6 +39,8 @@ export { createReactOverlays } from './overlays'; export { useUiSetting, useUiSetting$ } from './ui_settings'; +export { useExecutionContext } from './use_execution_context'; + export type { TableListViewProps, TableListViewState } from './table_list_view'; export { TableListView } from './table_list_view'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.4.js b/src/plugins/kibana_react/public/use_execution_context/index.ts similarity index 78% rename from packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.4.js rename to src/plugins/kibana_react/public/use_execution_context/index.ts index 6dc8aa803613d3..f36d094eb86d44 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.4.js +++ b/src/plugins/kibana_react/public/use_execution_context/index.ts @@ -6,10 +6,4 @@ * Side Public License, v 1. */ -export default function () { - return { - screenshots: { - directory: 'bar', - }, - }; -} +export { useExecutionContext } from './use_execution_context'; diff --git a/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts b/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts new file mode 100644 index 00000000000000..e2c538056153c3 --- /dev/null +++ b/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { KibanaExecutionContext, CoreStart } from 'kibana/public'; +import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; + +/** + * Set and clean up application level execution context + * @param executionContext + * @param context + */ +export function useExecutionContext( + executionContext: CoreStart['executionContext'], + context: KibanaExecutionContext +) { + useDeepCompareEffect(() => { + executionContext.set(context); + + return () => { + executionContext.clear(); + }; + }, [context]); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index adfe8da335a148..30b3bd4b4e4048 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -158,6 +158,9 @@ export const applicationUsageSchema = { security_logout: commonSchema, security_overwritten_session: commonSchema, securitySolutionUI: commonSchema, + /** + * @deprecated legacy key for users that still have bookmarks to the old siem name. "securitySolutionUI" key is the replacement + */ siem: commonSchema, space_selector: commonSchema, uptime: commonSchema, diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/schema.ts index 5252ab24395aaa..b0db5e6534c676 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/schema.ts @@ -54,14 +54,17 @@ const uiMetricFromDataPluginSchema: MakeSchemaFrom = { security_login: commonSchema, security_logout: commonSchema, security_overwritten_session: commonSchema, - securitySolution: commonSchema, - 'securitySolution:overview': commonSchema, - 'securitySolution:detections': commonSchema, - 'securitySolution:hosts': commonSchema, - 'securitySolution:network': commonSchema, - 'securitySolution:timelines': commonSchema, - 'securitySolution:case': commonSchema, - 'securitySolution:administration': commonSchema, + securitySolutionUI: commonSchema, + 'securitySolutionUI:overview': commonSchema, + 'securitySolutionUI:detections': commonSchema, + 'securitySolutionUI:hosts': commonSchema, + 'securitySolutionUI:network': commonSchema, + 'securitySolutionUI:timelines': commonSchema, + 'securitySolutionUI:case': commonSchema, + 'securitySolutionUI:administration': commonSchema, + /** + * @deprecated legacy key for users that still have bookmarks to the old siem name. "securitySolutionUI" key is the replacement + */ siem: commonSchema, space_selector: commonSchema, uptime: commonSchema, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index dcbf919698243e..c21536ccf472a7 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8869,7 +8869,26 @@ } } }, - "securitySolution:overview": { + "securitySolutionUI": { + "type": "array", + "items": { + "properties": { + "key": { + "type": "keyword", + "_meta": { + "description": "The event that is tracked" + } + }, + "value": { + "type": "long", + "_meta": { + "description": "The value of the event" + } + } + } + } + }, + "securitySolutionUI:overview": { "type": "array", "items": { "properties": { @@ -8888,7 +8907,7 @@ } } }, - "securitySolution:detections": { + "securitySolutionUI:detections": { "type": "array", "items": { "properties": { @@ -8907,7 +8926,7 @@ } } }, - "securitySolution:hosts": { + "securitySolutionUI:hosts": { "type": "array", "items": { "properties": { @@ -8926,7 +8945,7 @@ } } }, - "securitySolution:network": { + "securitySolutionUI:network": { "type": "array", "items": { "properties": { @@ -8945,7 +8964,7 @@ } } }, - "securitySolution:timelines": { + "securitySolutionUI:timelines": { "type": "array", "items": { "properties": { @@ -8964,7 +8983,7 @@ } } }, - "securitySolution:case": { + "securitySolutionUI:case": { "type": "array", "items": { "properties": { @@ -8983,7 +9002,7 @@ } } }, - "securitySolution:administration": { + "securitySolutionUI:administration": { "type": "array", "items": { "properties": { diff --git a/src/plugins/vis_types/timeseries/common/calculate_label.test.ts b/src/plugins/vis_types/timeseries/common/calculate_label.test.ts index 7083711246e7bc..7e612ed1aaddd6 100644 --- a/src/plugins/vis_types/timeseries/common/calculate_label.test.ts +++ b/src/plugins/vis_types/timeseries/common/calculate_label.test.ts @@ -9,6 +9,7 @@ import { calculateLabel } from './calculate_label'; import type { Metric } from './types'; import { SanitizedFieldType } from './types'; +import { KBN_FIELD_TYPES } from '../../../data/common'; describe('calculateLabel(metric, metrics)', () => { test('returns the metric.alias if set', () => { @@ -90,7 +91,7 @@ describe('calculateLabel(metric, metrics)', () => { { id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' }, metric, ] as unknown as Metric[]; - const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: 'field' }]; + const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: KBN_FIELD_TYPES.DATE }]; expect(() => calculateLabel(metric, metrics, fields)).toThrowError('Field "3" not found'); }); @@ -101,7 +102,7 @@ describe('calculateLabel(metric, metrics)', () => { { id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' }, metric, ] as unknown as Metric[]; - const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: 'field' }]; + const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: KBN_FIELD_TYPES.DATE }]; expect(calculateLabel(metric, metrics, fields, false)).toBe('Max of 3'); }); diff --git a/src/plugins/vis_types/timeseries/common/fields_utils.test.ts b/src/plugins/vis_types/timeseries/common/fields_utils.test.ts index 228dfbfd2db9df..6dd00d803b7c33 100644 --- a/src/plugins/vis_types/timeseries/common/fields_utils.test.ts +++ b/src/plugins/vis_types/timeseries/common/fields_utils.test.ts @@ -6,8 +6,16 @@ * Side Public License, v 1. */ -import { toSanitizedFieldType } from './fields_utils'; -import type { FieldSpec } from '../../../data/common'; +import { + getFieldsForTerms, + toSanitizedFieldType, + getMultiFieldLabel, + createCachedFieldValueFormatter, +} from './fields_utils'; +import { FieldSpec, KBN_FIELD_TYPES } from '../../../data/common'; +import { DataView } from '../../../data_views/common'; +import { stubLogstashDataView } from '../../../data/common/stubs'; +import { FieldFormatsRegistry, StringFormat } from '../../../field_formats/common'; describe('fields_utils', () => { describe('toSanitizedFieldType', () => { @@ -59,4 +67,92 @@ describe('fields_utils', () => { expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`); }); }); + + describe('getFieldsForTerms', () => { + test('should return fields as array', () => { + expect(getFieldsForTerms('field')).toEqual(['field']); + expect(getFieldsForTerms(['field', 'field1'])).toEqual(['field', 'field1']); + }); + + test('should exclude empty values', () => { + expect(getFieldsForTerms([null, ''])).toEqual([]); + }); + + test('should return empty array in case of undefined field', () => { + expect(getFieldsForTerms(undefined)).toEqual([]); + }); + }); + + describe('getMultiFieldLabel', () => { + test('should return label for single field', () => { + expect( + getMultiFieldLabel( + ['field'], + [{ name: 'field', label: 'Label', type: KBN_FIELD_TYPES.DATE }] + ) + ).toBe('Label'); + }); + + test('should return label for multi fields', () => { + expect( + getMultiFieldLabel( + ['field', 'field1'], + [ + { name: 'field', label: 'Label', type: KBN_FIELD_TYPES.DATE }, + { name: 'field2', label: 'Label1', type: KBN_FIELD_TYPES.DATE }, + ] + ) + ).toBe('Label + 1 other'); + }); + + test('should return label for multi fields (2 others)', () => { + expect( + getMultiFieldLabel( + ['field', 'field1', 'field2'], + [ + { name: 'field', label: 'Label', type: KBN_FIELD_TYPES.DATE }, + { name: 'field1', label: 'Label1', type: KBN_FIELD_TYPES.DATE }, + { name: 'field3', label: 'Label2', type: KBN_FIELD_TYPES.DATE }, + ] + ) + ).toBe('Label + 2 others'); + }); + }); + + describe('createCachedFieldValueFormatter', () => { + let dataView: DataView; + + beforeEach(() => { + dataView = stubLogstashDataView; + }); + + test('should use data view formatters', () => { + const getFormatterForFieldSpy = jest.spyOn(dataView, 'getFormatterForField'); + + const cache = createCachedFieldValueFormatter(dataView); + + cache('bytes', '10001'); + cache('bytes', '20002'); + + expect(getFormatterForFieldSpy).toHaveBeenCalledTimes(1); + }); + + test('should use default formatters in case of Data view not defined', () => { + const fieldFormatServiceMock = { + getDefaultInstance: jest.fn().mockReturnValue(new StringFormat()), + } as unknown as FieldFormatsRegistry; + + const cache = createCachedFieldValueFormatter( + null, + [{ name: 'field', label: 'Label', type: KBN_FIELD_TYPES.STRING }], + fieldFormatServiceMock + ); + + cache('field', '10001'); + cache('field', '20002'); + + expect(fieldFormatServiceMock.getDefaultInstance).toHaveBeenCalledTimes(1); + expect(fieldFormatServiceMock.getDefaultInstance).toHaveBeenCalledWith('string'); + }); + }); }); diff --git a/src/plugins/vis_types/timeseries/common/fields_utils.ts b/src/plugins/vis_types/timeseries/common/fields_utils.ts index d6987b9cdae9c0..02a62b9246d547 100644 --- a/src/plugins/vis_types/timeseries/common/fields_utils.ts +++ b/src/plugins/vis_types/timeseries/common/fields_utils.ts @@ -5,11 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; -import { FieldSpec } from '../../../data/common'; -import { isNestedField } from '../../../data/common'; -import { FetchedIndexPattern, SanitizedFieldType } from './types'; +import { isNestedField, FieldSpec, DataView } from '../../../data/common'; import { FieldNotFoundError } from './errors'; +import type { FetchedIndexPattern, SanitizedFieldType } from './types'; +import { FieldFormat, FieldFormatsRegistry, FIELD_FORMAT_IDS } from '../../../field_formats/common'; export const extractFieldLabel = ( fields: SanitizedFieldType[], @@ -49,3 +50,63 @@ export const toSanitizedFieldType = (fields: FieldSpec[]) => type: field.type, } as SanitizedFieldType) ); + +export const getFieldsForTerms = (fields: string | Array | undefined): string[] => { + return fields ? ([fields].flat().filter(Boolean) as string[]) : []; +}; + +export const getMultiFieldLabel = (fieldForTerms: string[], fields?: SanitizedFieldType[]) => { + const firstFieldLabel = fields ? extractFieldLabel(fields, fieldForTerms[0]) : fieldForTerms[0]; + + if (fieldForTerms.length > 1) { + return i18n.translate('visTypeTimeseries.fieldUtils.multiFieldLabel', { + defaultMessage: '{firstFieldLabel} + {count} {count, plural, one {other} other {others}}', + values: { + firstFieldLabel, + count: fieldForTerms.length - 1, + }, + }); + } + return firstFieldLabel ?? ''; +}; + +export const createCachedFieldValueFormatter = ( + dataView?: DataView | null, + fields?: SanitizedFieldType[], + fieldFormatService?: FieldFormatsRegistry, + excludedFieldFormatsIds: FIELD_FORMAT_IDS[] = [] +) => { + const cache = new Map(); + + return (fieldName: string, value: string, contentType: 'text' | 'html' = 'text') => { + const cachedFormatter = cache.get(fieldName); + if (cachedFormatter) { + return cachedFormatter.convert(value, contentType); + } + + if (dataView && !excludedFieldFormatsIds.includes(dataView.fieldFormatMap?.[fieldName]?.id)) { + const field = dataView.fields.getByName(fieldName); + if (field) { + const formatter = dataView.getFormatterForField(field); + + if (formatter) { + cache.set(fieldName, formatter); + return formatter.convert(value, contentType); + } + } + } else if (fieldFormatService && fields) { + const f = fields.find((item) => item.name === fieldName); + + if (f) { + const formatter = fieldFormatService.getDefaultInstance(f.type); + + if (formatter) { + cache.set(fieldName, formatter); + return formatter.convert(value, contentType); + } + } + } + }; +}; + +export const MULTI_FIELD_VALUES_SEPARATOR = ' › '; diff --git a/src/plugins/vis_types/timeseries/common/types/index.ts b/src/plugins/vis_types/timeseries/common/types/index.ts index 01b200c6774d1a..001ea02eb355aa 100644 --- a/src/plugins/vis_types/timeseries/common/types/index.ts +++ b/src/plugins/vis_types/timeseries/common/types/index.ts @@ -7,7 +7,7 @@ */ import { Filter } from '@kbn/es-query'; -import { IndexPattern, Query } from '../../../../data/common'; +import { IndexPattern, KBN_FIELD_TYPES, Query } from '../../../../data/common'; import { Panel } from './panel_model'; export type { Metric, Series, Panel, MetricType } from './panel_model'; @@ -28,7 +28,7 @@ export interface FetchedIndexPattern { export interface SanitizedFieldType { name: string; - type: string; + type: KBN_FIELD_TYPES; label?: string; } diff --git a/src/plugins/vis_types/timeseries/common/types/panel_model.ts b/src/plugins/vis_types/timeseries/common/types/panel_model.ts index 40bd5632c3a80d..1ccf7412a3e98f 100644 --- a/src/plugins/vis_types/timeseries/common/types/panel_model.ts +++ b/src/plugins/vis_types/timeseries/common/types/panel_model.ts @@ -6,10 +6,15 @@ * Side Public License, v 1. */ -import { METRIC_TYPES, Query } from '../../../../data/common'; +import { Query, METRIC_TYPES, KBN_FIELD_TYPES } from '../../../../data/common'; import { PANEL_TYPES, TOOLTIP_MODES, TSVB_METRIC_TYPES } from '../enums'; -import { IndexPatternValue, Annotation } from './index'; -import { ColorRules, BackgroundColorRules, BarColorRules, GaugeColorRules } from './color_rules'; +import type { IndexPatternValue, Annotation } from './index'; +import type { + ColorRules, + BackgroundColorRules, + BarColorRules, + GaugeColorRules, +} from './color_rules'; interface MetricVariable { field?: string; @@ -109,7 +114,7 @@ export interface Series { steps: number; terms_direction?: string; terms_exclude?: string; - terms_field?: string; + terms_field?: string | Array; terms_include?: string; terms_order_by?: string; terms_size?: string; @@ -155,10 +160,10 @@ export interface Panel { markdown_scrollbars: number; markdown_vertical_align?: string; max_bars: number; - pivot_id?: string; + pivot_id?: string | Array; pivot_label?: string; pivot_rows?: string; - pivot_type?: string; + pivot_type?: KBN_FIELD_TYPES | Array; series: Series[]; show_grid: number; show_legend: number; diff --git a/src/plugins/vis_types/timeseries/common/types/vis_data.ts b/src/plugins/vis_types/timeseries/common/types/vis_data.ts index 07c078a6e8aaec..de507fb9ecc33e 100644 --- a/src/plugins/vis_types/timeseries/common/types/vis_data.ts +++ b/src/plugins/vis_types/timeseries/common/types/vis_data.ts @@ -46,7 +46,7 @@ export interface PanelSeries { export interface PanelData { id: string; - label: string; + label: string | string[]; labelFormatted?: string; data: PanelDataArray[]; seriesId: string; diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select.tsx deleted file mode 100644 index d5665211b7f732..00000000000000 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select.tsx +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { i18n } from '@kbn/i18n'; -import React, { ReactNode } from 'react'; -import { - EuiComboBox, - EuiComboBoxProps, - EuiComboBoxOptionOption, - EuiFormRow, - htmlIdGenerator, -} from '@elastic/eui'; -import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; -import type { SanitizedFieldType, IndexPatternValue } from '../../../../common/types'; -import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; - -import { isFieldEnabled } from '../../../../common/check_ui_restrictions'; - -interface FieldSelectProps { - label: string | ReactNode; - type: string; - fields: Record; - indexPattern: IndexPatternValue; - value?: string | null; - onChange: (options: Array>) => void; - disabled?: boolean; - restrict?: string[]; - placeholder?: string; - uiRestrictions?: TimeseriesUIRestrictions; - 'data-test-subj'?: string; -} - -const defaultPlaceholder = i18n.translate('visTypeTimeseries.fieldSelect.selectFieldPlaceholder', { - defaultMessage: 'Select field...', -}); - -const isFieldTypeEnabled = (fieldRestrictions: string[], fieldType: string) => - fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true; - -const sortByLabel = (a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOption) => { - const getNormalizedString = (option: EuiComboBoxOptionOption) => - (option.label || '').toLowerCase(); - - return getNormalizedString(a).localeCompare(getNormalizedString(b)); -}; - -export function FieldSelect({ - label, - type, - fields, - indexPattern = '', - value = '', - onChange, - disabled = false, - restrict = [], - placeholder = defaultPlaceholder, - uiRestrictions, - 'data-test-subj': dataTestSubj = 'metricsIndexPatternFieldsSelect', -}: FieldSelectProps) { - const htmlId = htmlIdGenerator(); - - let selectedOptions: Array> = []; - let newPlaceholder = placeholder; - const fieldsSelector = getIndexPatternKey(indexPattern); - - const groupedOptions: EuiComboBoxProps['options'] = Object.values( - (fields[fieldsSelector] || []).reduce>>( - (acc, field) => { - if (placeholder === field?.name) { - newPlaceholder = field.label ?? field.name; - } - - if ( - isFieldTypeEnabled(restrict, field.type) && - isFieldEnabled(field.name, type, uiRestrictions) - ) { - const item: EuiComboBoxOptionOption = { - value: field.name, - label: field.label ?? field.name, - }; - - const fieldTypeOptions = acc[field.type]?.options; - - if (fieldTypeOptions) { - fieldTypeOptions.push(item); - } else { - acc[field.type] = { - options: [item], - label: field.type, - }; - } - - if (value === item.value) { - selectedOptions.push(item); - } - } - - return acc; - }, - {} - ) - ); - - // sort groups - groupedOptions.sort(sortByLabel); - - // sort items - groupedOptions.forEach((group) => { - if (Array.isArray(group.options)) { - group.options.sort(sortByLabel); - } - }); - - const isInvalid = Boolean(value && fields[fieldsSelector] && !selectedOptions.length); - - if (value && !selectedOptions.length) { - selectedOptions = [{ label: value, id: 'INVALID_FIELD' }]; - } - - return ( - - - - ); -} diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select.tsx new file mode 100644 index 00000000000000..27f4d96381fda4 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select.tsx @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useCallback, useMemo, ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiComboBoxOptionOption, + EuiComboBoxProps, + EuiFormRow, + htmlIdGenerator, + DragDropContextProps, +} from '@elastic/eui'; + +import { FieldSelectItem } from './field_select_item'; +import { IndexPatternValue, SanitizedFieldType } from '../../../../../common/types'; +import { TimeseriesUIRestrictions } from '../../../../../common/ui_restrictions'; +import { getIndexPatternKey } from '../../../../../common/index_patterns_utils'; +import { MultiFieldSelect } from './multi_field_select'; +import { + addNewItem, + deleteItem, + swapItems, + getGroupedOptions, + findInGroupedOptions, + INVALID_FIELD_ID, + MAX_MULTI_FIELDS_ITEMS, + updateItem, +} from './field_select_utils'; + +interface FieldSelectProps { + label: string | ReactNode; + type: string; + uiRestrictions?: TimeseriesUIRestrictions; + restrict?: string[]; + value?: string | Array | null; + fields: Record; + indexPattern: IndexPatternValue; + onChange: (selectedValues: Array) => void; + disabled?: boolean; + placeholder?: string; + allowMultiSelect?: boolean; + 'data-test-subj'?: string; + fullWidth?: boolean; +} + +const getPreselectedFields = ( + placeholder?: string, + options?: Array> +) => placeholder && findInGroupedOptions(options, placeholder)?.label; + +export function FieldSelect({ + label, + fullWidth, + type, + value, + fields, + indexPattern, + uiRestrictions, + restrict, + onChange, + disabled, + placeholder, + allowMultiSelect = false, + 'data-test-subj': dataTestSubj, +}: FieldSelectProps) { + const htmlId = htmlIdGenerator(); + const fieldsSelector = getIndexPatternKey(indexPattern); + const selectedIds = useMemo(() => [value ?? null].flat(), [value]); + + const groupedOptions = useMemo( + () => getGroupedOptions(type, selectedIds, fields[fieldsSelector], uiRestrictions, restrict), + [fields, fieldsSelector, restrict, selectedIds, type, uiRestrictions] + ); + + const selectedOptionsMap = useMemo(() => { + const map = new Map['selectedOptions']>(); + if (selectedIds) { + const addIntoSet = (item: string) => { + const option = findInGroupedOptions(groupedOptions, item); + if (option) { + map.set(item, [option]); + } else { + map.set(item, [{ label: item, id: INVALID_FIELD_ID }]); + } + }; + + selectedIds.forEach((v) => v && addIntoSet(v)); + } + return map; + }, [groupedOptions, selectedIds]); + + const invalidSelectedOptions = useMemo( + () => + [...selectedOptionsMap.values()] + .flat() + .filter((item) => item?.label && item?.id === INVALID_FIELD_ID) + .map((item) => item!.label), + [selectedOptionsMap] + ); + + const onFieldSelectItemChange = useCallback( + (index: number = 0, [selectedItem]) => { + onChange(updateItem(selectedIds, selectedItem?.value, index)); + }, + [selectedIds, onChange] + ); + + const onNewItemAdd = useCallback( + (index?: number) => onChange(addNewItem(selectedIds, index)), + [selectedIds, onChange] + ); + + const onDeleteItem = useCallback( + (index?: number) => onChange(deleteItem(selectedIds, index)), + [onChange, selectedIds] + ); + + const onDragEnd: DragDropContextProps['onDragEnd'] = useCallback( + ({ source, destination }) => { + if (destination && source.index !== destination?.index) { + onChange(swapItems(selectedIds, source.index, destination.index)); + } + }, + [onChange, selectedIds] + ); + + const FieldSelectItemFactory = useMemo( + () => (props: { value?: string | null; index?: number }) => + ( + = MAX_MULTI_FIELDS_ITEMS} + disableDelete={!allowMultiSelect || selectedIds?.length <= 1} + /> + ), + [ + groupedOptions, + selectedOptionsMap, + disabled, + onNewItemAdd, + onDeleteItem, + onFieldSelectItemChange, + placeholder, + allowMultiSelect, + selectedIds?.length, + ] + ); + + return ( + + {selectedIds?.length > 1 ? ( + + ) : ( + + )} + + ); +} diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select_item.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select_item.tsx new file mode 100644 index 00000000000000..79ebd247fffa89 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select_item.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiComboBoxProps, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + +import { AddDeleteButtons } from '../../add_delete_buttons'; +import { INVALID_FIELD_ID } from './field_select_utils'; + +export interface FieldSelectItemProps { + onChange: (options: Array>) => void; + options: EuiComboBoxProps['options']; + selectedOptions: EuiComboBoxProps['selectedOptions']; + placeholder?: string; + disabled?: boolean; + disableAdd?: boolean; + disableDelete?: boolean; + onNewItemAdd?: () => void; + onDeleteItem?: () => void; +} + +const defaultPlaceholder = i18n.translate('visTypeTimeseries.fieldSelect.selectFieldPlaceholder', { + defaultMessage: 'Select field...', +}); + +export function FieldSelectItem({ + options, + selectedOptions, + placeholder = defaultPlaceholder, + disabled, + disableAdd, + disableDelete, + + onChange, + onDeleteItem, + onNewItemAdd, +}: FieldSelectItemProps) { + const isInvalid = Boolean(selectedOptions?.find((item) => item.id === INVALID_FIELD_ID)); + + return ( + + + + + + + + + ); +} diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select_utils.ts b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select_utils.ts new file mode 100644 index 00000000000000..40d80a014e36b2 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/field_select_utils.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; +import { isFieldEnabled } from '../../../../../common/check_ui_restrictions'; + +import type { SanitizedFieldType } from '../../../../..//common/types'; +import type { TimeseriesUIRestrictions } from '../../../../../common/ui_restrictions'; + +export const INVALID_FIELD_ID = 'INVALID_FIELD'; +export const MAX_MULTI_FIELDS_ITEMS = 4; + +export const getGroupedOptions = ( + type: string, + selectedIds: Array, + fields: SanitizedFieldType[] = [], + uiRestrictions: TimeseriesUIRestrictions | undefined, + restrict: string[] = [] +): EuiComboBoxProps['options'] => { + const isFieldTypeEnabled = (fieldType: string) => + restrict.length ? restrict.includes(fieldType) : true; + + const sortByLabel = (a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOption) => { + const getNormalizedString = (option: EuiComboBoxOptionOption) => + (option.label || '').toLowerCase(); + + return getNormalizedString(a).localeCompare(getNormalizedString(b)); + }; + + const groupedOptions: EuiComboBoxProps['options'] = Object.values( + fields.reduce>>((acc, field) => { + if (isFieldTypeEnabled(field.type) && isFieldEnabled(field.name, type, uiRestrictions)) { + const item: EuiComboBoxOptionOption = { + value: field.name, + label: field.label ?? field.name, + disabled: selectedIds.includes(field.name), + }; + + const fieldTypeOptions = acc[field.type]?.options; + + if (fieldTypeOptions) { + fieldTypeOptions.push(item); + } else { + acc[field.type] = { + options: [item], + label: field.type, + }; + } + } + + return acc; + }, {}) + ); + + // sort groups + groupedOptions.sort(sortByLabel); + + // sort items + groupedOptions.forEach((group) => { + if (Array.isArray(group.options)) { + group.options.sort(sortByLabel); + } + }); + + return groupedOptions; +}; + +export const findInGroupedOptions = ( + groupedOptions: EuiComboBoxProps['options'], + fieldName: string +) => + (groupedOptions || []) + .map((i) => i.options) + .flat() + .find((i) => i?.value === fieldName); + +export const updateItem = ( + existingItems: Array, + value: string | null = null, + index: number = 0 +) => { + const arr = [...existingItems]; + arr[index] = value; + return arr; +}; + +export const addNewItem = (existingItems: Array, insertAfter: number = 0) => { + const arr = [...existingItems]; + arr.splice(insertAfter + 1, 0, null); + return arr; +}; + +export const deleteItem = (existingItems: Array, index: number = 0) => + existingItems.filter((item, i) => i !== index); + +export const swapItems = ( + existingItems: Array, + source: number = 0, + destination: number = 0 +) => { + const arr = [...existingItems]; + arr.splice(destination, 0, arr.splice(source, 1)[0]); + return arr; +}; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.3.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/index.ts similarity index 60% rename from packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.3.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/index.ts index b68a5115553f54..5dc0b1edaaff39 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/__fixtures__/config.3.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/index.ts @@ -6,12 +6,4 @@ * Side Public License, v 1. */ -export default async function ({ readConfigFile }) { - const config4 = await readConfigFile(require.resolve('./config.4')); - return { - testFiles: ['baz'], - screenshots: { - ...config4.get('screenshots'), - }, - }; -} +export { FieldSelect } from './field_select'; diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/multi_field_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/multi_field_select.tsx new file mode 100644 index 00000000000000..7b96a599c8a4fa --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select/multi_field_select.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { i18n } from '@kbn/i18n'; +import { + EuiDragDropContext, + EuiDroppable, + DragDropContextProps, + EuiDraggable, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiIcon, +} from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; + +const DROPPABLE_ID = 'onDragEnd'; + +const dragAriaLabel = i18n.translate('visTypeTimeseries.fieldSelect.dragAriaLabel', { + defaultMessage: 'Drag field', +}); + +export function MultiFieldSelect(props: { + values: Array; + onDragEnd: DragDropContextProps['onDragEnd']; + WrappedComponent: FunctionComponent<{ value?: string | null; index?: number }>; +}) { + return ( + + + {props.values.map((value, index) => ( + + {(provided) => ( + + + + + + + + + + + )} + + ))} + + + ); +} diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.js index 9f8285bc97e297..b24ac14717561c 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.js @@ -168,7 +168,11 @@ export const FilterRatioAgg = (props) => { restrict={getSupportedFieldsByMetricType(model.metric_agg)} indexPattern={indexPattern} value={model.field} - onChange={handleSelectChange('field')} + onChange={(value) => + handleChange({ + field: value?.[0], + }) + } /> ) : null} diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.test.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.test.js index 38305395bfbb6b..bebbbc6f8614f7 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.test.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.test.js @@ -72,6 +72,7 @@ describe('TSVB Filter Ratio', () => { label: 'number', options: [ { + disabled: false, label: 'system.cpu.user.pct', value: 'system.cpu.user.pct', }, @@ -95,6 +96,7 @@ describe('TSVB Filter Ratio', () => { "label": "date", "options": Array [ Object { + "disabled": false, "label": "@timestamp", "value": "@timestamp", }, @@ -104,6 +106,7 @@ describe('TSVB Filter Ratio', () => { "label": "number", "options": Array [ Object { + "disabled": false, "label": "system.cpu.user.pct", "value": "system.cpu.user.pct", }, diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile.js index b9512249de9454..c098eb83ddf982 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile.js @@ -90,7 +90,11 @@ export function PercentileAgg(props) { restrict={RESTRICT_FIELDS} indexPattern={indexPattern} value={model.field} - onChange={handleSelectChange('field')} + onChange={(value) => + handleChange({ + field: value?.[0], + }) + } /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx index 664c59b27fa397..57dfa23c815d8e 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx @@ -45,7 +45,7 @@ interface PercentileRankAggProps { series: Series; dragHandleProps: DragHandleProps; onAdd(): void; - onChange(): void; + onChange(partialModel: Record): void; onDelete(): void; } @@ -111,7 +111,11 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => { restrict={RESTRICT_FIELDS} indexPattern={indexPattern} value={model.field ?? ''} - onChange={handleSelectChange('field')} + onChange={(value) => + props.onChange({ + field: value?.[0], + }) + } /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/positive_rate.js index 20ae5ecd24310e..35786efa98c5fa 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/positive_rate.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/positive_rate.js @@ -111,7 +111,11 @@ export const PositiveRateAgg = (props) => { restrict={[KBN_FIELD_TYPES.NUMBER]} indexPattern={indexPattern} value={model.field} - onChange={handleSelectChange('field')} + onChange={(value) => + handleChange({ + field: value?.[0], + }) + } uiRestrictions={props.uiRestrictions} fullWidth /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/std_agg.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/std_agg.js index 722e9021b8a60f..61579c9656d5e7 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/std_agg.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/std_agg.js @@ -68,7 +68,11 @@ export function StandardAgg(props) { restrict={restrictFields} indexPattern={indexPattern} value={model.field} - onChange={handleSelectChange('field')} + onChange={(value) => + handleChange({ + field: value?.[0], + }) + } uiRestrictions={uiRestrictions} fullWidth /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/std_deviation.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/std_deviation.js index f9a54cb1117407..375d576f8cf285 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/std_deviation.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/std_deviation.js @@ -119,7 +119,11 @@ const StandardDeviationAggUi = (props) => { restrict={RESTRICT_FIELDS} indexPattern={indexPattern} value={model.field} - onChange={handleSelectChange('field')} + onChange={(value) => + handleChange({ + field: value?.[0], + }) + } /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/top_hit.js index 7fa708331ac55e..7dec7d94236e02 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/top_hit.js @@ -180,7 +180,11 @@ const TopHitAggUi = (props) => { restrict={aggWithOptionsRestrictFields} indexPattern={indexPattern} value={model.field} - onChange={handleSelectChange('field')} + onChange={(value) => + handleChange({ + field: value?.[0], + }) + } /> @@ -242,7 +246,11 @@ const TopHitAggUi = (props) => { } restrict={ORDER_DATE_RESTRICT_FIELDS} value={model.order_by} - onChange={handleSelectChange('order_by')} + onChange={(value) => + handleChange({ + order_by: value?.[0], + }) + } indexPattern={indexPattern} fields={fields} data-test-subj="topHitOrderByFieldSelect" diff --git a/src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx b/src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx index 856948cb7601ec..562fb75089e194 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx @@ -148,8 +148,12 @@ export const AnnotationRow = ({ /> } restrict={RESTRICT_FIELDS} - value={model.time_field} - onChange={handleChange(TIME_FIELD_KEY)} + value={model[TIME_FIELD_KEY]} + onChange={(value) => + onChange({ + [TIME_FIELD_KEY]: value?.[0] ?? undefined, + }) + } indexPattern={model.index_pattern} fields={fields} /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js b/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js index 217b3948e1cd83..7b3ae5f3e16ef7 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js @@ -259,7 +259,11 @@ export const IndexPattern = ({ restrict={RESTRICT_FIELDS} value={model[timeFieldName]} disabled={disabled} - onChange={handleSelectChange(timeFieldName)} + onChange={(value) => + onChange({ + [timeFieldName]: value?.[0], + }) + } indexPattern={model[indexPatternName]} fields={fields} placeholder={ diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.test.ts index eb6ea561fec840..2eaee960991109 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.test.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.test.ts @@ -11,6 +11,7 @@ import { TSVB_METRIC_TYPES } from '../../../../common/enums'; import { checkIfNumericMetric } from './check_if_numeric_metric'; import type { Metric } from '../../../../common/types'; +import type { VisFields } from '../../lib/fetch_fields'; describe('checkIfNumericMetric(metric, fields, indexPattern)', () => { const indexPattern = { id: 'some_id' }; @@ -20,7 +21,7 @@ describe('checkIfNumericMetric(metric, fields, indexPattern)', () => { { name: 'string field', type: 'string' }, { name: 'date field', type: 'date' }, ], - }; + } as VisFields; it('should return true for Count metric', () => { const metric = { type: METRIC_TYPES.COUNT } as Metric; diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts index 15151a9e21bc5a..08ee275d144ea3 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts @@ -201,11 +201,11 @@ describe('convert series to datatables', () => { expect(tables.series1.rows.length).toEqual(8); const expected1 = series[0].data.map((d) => { - d.push(parseInt(series[0].label, 10)); + d.push(parseInt([series[0].label].flat()[0], 10)); return d; }); const expected2 = series[1].data.map((d) => { - d.push(parseInt(series[1].label, 10)); + d.push(parseInt([series[1].label].flat()[0], 10)); return d; }); expect(tables.series1.rows).toEqual([...expected1, ...expected2]); diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts index 8e7c1694357c8f..62397e3b1d8c2b 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts @@ -10,6 +10,7 @@ import { DatatableRow, DatatableColumn, DatatableColumnType } from 'src/plugins/ import { Query } from 'src/plugins/data/common'; import { TimeseriesVisParams } from '../../../types'; import type { PanelData, Metric } from '../../../../common/types'; +import { getMultiFieldLabel, getFieldsForTerms } from '../../../../common/fields_utils'; import { BUCKET_TYPES, TSVB_METRIC_TYPES } from '../../../../common/enums'; import { fetchIndexPattern } from '../../../../common/index_patterns_utils'; import { getDataStart } from '../../../services'; @@ -131,7 +132,7 @@ export const convertSeriesToDataTable = async ( id++; columns.push({ id, - name: layer.terms_field || '', + name: getMultiFieldLabel(getFieldsForTerms(layer.terms_field)), isMetric: false, type: BUCKET_TYPES.TERMS, }); @@ -154,7 +155,7 @@ export const convertSeriesToDataTable = async ( const row: DatatableRow = [rowData[0], rowData[1]]; // If the layer is split by terms aggregation, the data array should also contain the split value. if (isGroupedByTerms || filtersColumn) { - row.push(seriesPerLayer[j].label); + row.push([seriesPerLayer[j].label].flat()[0]); } return row; }); diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_vars.js index 867ba673cf1dd9..34efcf678d7a66 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_vars.js +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_vars.js @@ -16,6 +16,7 @@ import { getMetricsField } from './get_metrics_field'; import { createFieldFormatter } from './create_field_formatter'; import { labelDateFormatter } from './label_date_formatter'; import moment from 'moment'; +import { getFieldsForTerms } from '../../../../common/fields_utils'; export const convertSeriesToVars = (series, model, getConfig = null, fieldFormatMap) => { const variables = {}; @@ -50,10 +51,16 @@ export const convertSeriesToVars = (series, model, getConfig = null, fieldFormat }), }, }; - const rowLabel = - seriesModel.split_mode === BUCKET_TYPES.TERMS - ? createFieldFormatter(seriesModel.terms_field, fieldFormatMap)(row.label) - : row.label; + + let rowLabel = row.label; + if (seriesModel.split_mode === BUCKET_TYPES.TERMS) { + const fieldsForTerms = getFieldsForTerms(seriesModel.terms_field); + + if (fieldsForTerms.length === 1) { + rowLabel = createFieldFormatter(fieldsForTerms[0], fieldFormatMap)(row.label); + } + } + set(variables, varName, data); set(variables, `${label}.label`, rowLabel); diff --git a/src/plugins/vis_types/timeseries/public/application/components/panel_config/table.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/table.tsx index 57ae699d281a01..cefcd9c2e54219 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/panel_config/table.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/panel_config/table.tsx @@ -23,7 +23,6 @@ import { EuiHorizontalRule, EuiCode, EuiText, - EuiComboBoxOptionOption, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -65,16 +64,27 @@ export class TablePanelConfig extends Component< this.setState({ selectedTab }); } - handlePivotChange = (selectedOption: Array>) => { + handlePivotChange = (selectedOptions: Array) => { const { fields, model } = this.props; - const pivotId = get(selectedOption, '[0].value', null); - const field = fields[getIndexPatternKey(model.index_pattern)].find((f) => f.name === pivotId); - const pivotType = get(field, 'type', model.pivot_type); - this.props.onChange({ - pivot_id: pivotId, - pivot_type: pivotType, - }); + const getPivotType = (fieldName?: string | null): KBN_FIELD_TYPES | null => { + const field = fields[getIndexPatternKey(model.index_pattern)].find( + (f) => f.name === fieldName + ); + return get(field, 'type', null); + }; + + this.props.onChange( + selectedOptions.length === 1 + ? { + pivot_id: selectedOptions[0] || undefined, + pivot_type: getPivotType(selectedOptions[0]) || undefined, + } + : { + pivot_id: selectedOptions, + pivot_type: selectedOptions.map((item) => getPivotType(item)), + } + ); }; handleTextChange = @@ -129,6 +139,8 @@ export class TablePanelConfig extends Component< onChange={this.handlePivotChange} uiRestrictions={this.context.uiRestrictions} type={BUCKET_TYPES.TERMS} + allowMultiSelect={true} + fullWidth={true} /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_types/timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index 524e35f9d29e1b..b7c5095535fbc4 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_types/timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -20,13 +20,14 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js } labelType="label" > - ({ - ...field, - disabled: !isGroupByFieldsEnabled(field.value, uiRestrictions), - })); - - const selectedValue = props.value || 'everything'; - const selectedOption = modeOptions.find((option) => { - return selectedValue === option.value; - }); - - return ( - - ); -} - -GroupBySelectUi.propTypes = { - onChange: PropTypes.func, - value: PropTypes.string, - uiRestrictions: PropTypes.object, -}; - -export const GroupBySelect = injectI18n(GroupBySelectUi); diff --git a/src/plugins/vis_types/timeseries/public/application/components/splits/group_by_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/splits/group_by_select.tsx new file mode 100644 index 00000000000000..9c0e26b642f8b4 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/splits/group_by_select.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; +import { isGroupByFieldsEnabled } from '../../../../common/check_ui_restrictions'; +import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; + +interface GroupBySelectProps { + id: string; + onChange: EuiComboBoxProps['onChange']; + value?: string; + uiRestrictions: TimeseriesUIRestrictions; +} + +const getAvailableOptions = () => [ + { + label: i18n.translate('visTypeTimeseries.splits.groupBySelect.modeOptions.everythingLabel', { + defaultMessage: 'Everything', + }), + value: 'everything', + }, + { + label: i18n.translate('visTypeTimeseries.splits.groupBySelect.modeOptions.filterLabel', { + defaultMessage: 'Filter', + }), + value: 'filter', + }, + { + label: i18n.translate('visTypeTimeseries.splits.groupBySelect.modeOptions.filtersLabel', { + defaultMessage: 'Filters', + }), + value: 'filters', + }, + { + label: i18n.translate('visTypeTimeseries.splits.groupBySelect.modeOptions.termsLabel', { + defaultMessage: 'Terms', + }), + value: 'terms', + }, +]; + +export const GroupBySelect = ({ + id, + onChange, + value = 'everything', + uiRestrictions, +}: GroupBySelectProps) => { + const modeOptions = getAvailableOptions().map((field) => ({ + ...field, + disabled: !isGroupByFieldsEnabled(field.value, uiRestrictions), + })); + + const selectedOption: EuiComboBoxOptionOption | undefined = modeOptions.find( + (option) => value === option.value + ); + + return ( + + ); +}; diff --git a/src/plugins/vis_types/timeseries/public/application/components/splits/terms.js b/src/plugins/vis_types/timeseries/public/application/components/splits/terms.js index 80810f552d3a19..4810932c9b4ac9 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_types/timeseries/public/application/components/splits/terms.js @@ -7,7 +7,7 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useCallback } from 'react'; import { get, find } from 'lodash'; import { GroupBySelect } from './group_by_select'; import { createTextHandler } from '../lib/create_text_handler'; @@ -91,6 +91,15 @@ export const SplitByTermsUI = ({ const selectedField = find(fields[fieldsSelector], ({ name }) => name === model.terms_field); const selectedFieldType = get(selectedField, 'type'); + const onTermsFieldChange = useCallback( + (selectedOptions) => { + onChange({ + terms_field: selectedOptions.length === 1 ? selectedOptions[0] : selectedOptions, + }); + }, + [onChange] + ); + if ( seriesQuantity && model.stacked === STACKED_OPTIONS.PERCENT && @@ -142,11 +151,12 @@ export const SplitByTermsUI = ({ ]} data-test-subj="groupByField" indexPattern={indexPattern} - onChange={handleSelectChange('terms_field')} + onChange={onTermsFieldChange} value={model.terms_field} fields={fields} uiRestrictions={uiRestrictions} type={'terms'} + allowMultiSelect={true} /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx index ae699880784a9a..7787f0f6929b25 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx @@ -190,6 +190,7 @@ function TimeseriesVisualization({ onUiState={handleUiState} syncColors={syncColors} palettesService={palettesService} + indexPattern={indexPattern} fieldFormatMap={indexPattern?.fieldFormatMap} /> diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts index 653b2985ed1aeb..512a9d1ff9169d 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts @@ -15,6 +15,7 @@ import { PaletteRegistry } from 'src/plugins/charts/public'; import { TimeseriesVisParams } from '../../../types'; import type { TimeseriesVisData, PanelData } from '../../../../common/types'; import type { FieldFormatMap } from '../../../../../../data/common'; +import { FetchedIndexPattern } from '../../../../common/types'; /** * Lazy load each visualization type, since the only one is presented on the screen at the same time. @@ -62,5 +63,7 @@ export interface TimeseriesVisProps { getConfig: IUiSettingsClient['get']; syncColors: boolean; palettesService: PaletteRegistry; + indexPattern?: FetchedIndexPattern['indexPattern']; + /** @deprecated please use indexPattern.fieldFormatMap instead **/ fieldFormatMap?: FieldFormatMap; } diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js index f62120ed5f3ee7..f3cb92b31b3093 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js @@ -221,7 +221,11 @@ export class TableSeriesConfig extends Component { fields={this.props.fields} indexPattern={this.props.panel.index_pattern} value={model.aggregate_by} - onChange={handleSelectChange('aggregate_by')} + onChange={(value) => + this.props.onChange({ + aggregate_by: value?.[0], + }) + } fullWidth restrict={[ KBN_FIELD_TYPES.NUMBER, diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js index 3e828a1b833bf0..549aa1b70082f8 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js @@ -18,11 +18,17 @@ import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; import { ExternalUrlErrorModal } from '../../lib/external_url_error_modal'; -import { FIELD_FORMAT_IDS } from '../../../../../../../../plugins/field_formats/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { getFieldFormats, getCoreStart } from '../../../../services'; import { DATA_FORMATTERS } from '../../../../../common/enums'; -import { getValueOrEmpty } from '../../../../../common/empty_label'; +import { FIELD_FORMAT_IDS } from '../../../../../../../../plugins/field_formats/common'; + +import { + createCachedFieldValueFormatter, + getFieldsForTerms, + getMultiFieldLabel, + MULTI_FIELD_VALUES_SEPARATOR, +} from '../../../../../common/fields_utils'; function getColor(rules, colorKey, value) { let color; @@ -49,12 +55,7 @@ function sanitizeUrl(url) { class TableVis extends Component { constructor(props) { super(props); - - const fieldFormatsService = getFieldFormats(); - const DateFormat = fieldFormatsService.getType(FIELD_FORMAT_IDS.DATE); - - this.dateFormatter = new DateFormat({}, this.props.getConfig); - + this.fieldFormatsService = getFieldFormats(); this.state = { accessDeniedDrilldownUrl: null, }; @@ -74,17 +75,21 @@ class TableVis extends Component { } }; - renderRow = (row) => { + renderRow = (row, pivotIds, fieldValuesFormatter) => { const { model, fieldFormatMap, getConfig } = this.props; - let rowDisplay = getValueOrEmpty( - model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key - ); + let rowDisplay = row.key; + + if (pivotIds.length) { + rowDisplay = pivotIds + .map((item, index) => { + const value = [row.key ?? null].flat()[index]; + const formatted = fieldValuesFormatter(item, value, 'html'); - // we should skip url field formatting for key if tsvb have drilldown_url - if (fieldFormatMap?.[model.pivot_id]?.id !== FIELD_FORMAT_IDS.URL || !model.drilldown_url) { - const formatter = createFieldFormatter(model?.pivot_id, fieldFormatMap, 'html'); - rowDisplay = ; // eslint-disable-line react/no-danger + // eslint-disable-next-line react/no-danger + return ; + }) + .reduce((prev, curr) => [prev, MULTI_FIELD_VALUES_SEPARATOR, curr]); } if (model.drilldown_url) { @@ -150,7 +155,7 @@ class TableVis extends Component { ); }; - renderHeader() { + renderHeader(pivotIds) { const { model, uiState, onUiState, visData } = this.props; const stateKey = `${model.type}.sort`; const sort = uiState.get(stateKey, { @@ -210,7 +215,7 @@ class TableVis extends Component { ); }); - const label = visData.pivot_label || model.pivot_label || model.pivot_id; + const label = visData.pivot_label || model.pivot_label || getMultiFieldLabel(pivotIds); let sortIcon; if (sort.column === '_default_') { sortIcon = sort.order === 'asc' ? 'sortUp' : 'sortDown'; @@ -240,13 +245,26 @@ class TableVis extends Component { closeExternalUrlErrorModal = () => this.setState({ accessDeniedDrilldownUrl: null }); render() { - const { visData } = this.props; + const { visData, model, indexPattern } = this.props; const { accessDeniedDrilldownUrl } = this.state; - const header = this.renderHeader(); + const fields = (model.pivot_type ? [model.pivot_type ?? null].flat() : []).map( + (type, index) => ({ + name: [model.pivot_id ?? null].flat()[index], + type, + }) + ); + const fieldValuesFormatter = createCachedFieldValueFormatter( + indexPattern, + fields, + this.fieldFormatsService, + model.drilldown_url ? [FIELD_FORMAT_IDS.URL] : [] + ); + const pivotIds = getFieldsForTerms(model.pivot_id); + const header = this.renderHeader(pivotIds); let rows = null; if (isArray(visData.series) && visData.series.length) { - rows = visData.series.map(this.renderRow); + rows = visData.series.map((item) => this.renderRow(item, pivotIds, fieldValuesFormatter)); } return ( @@ -285,6 +303,7 @@ TableVis.propTypes = { uiState: PropTypes.object, pageNumber: PropTypes.number, getConfig: PropTypes.func, + indexPattern: PropTypes.object, }; // default export required for React.Lazy diff --git a/src/plugins/vis_types/timeseries/public/trigger_action/index.ts b/src/plugins/vis_types/timeseries/public/trigger_action/index.ts index d3329bee803a1c..8e757b98c125d3 100644 --- a/src/plugins/vis_types/timeseries/public/trigger_action/index.ts +++ b/src/plugins/vis_types/timeseries/public/trigger_action/index.ts @@ -16,6 +16,7 @@ import { getDataSourceInfo } from './get_datasource_info'; import { getFieldType } from './get_field_type'; import { getSeries } from './get_series'; import { getYExtents } from './get_extents'; +import { getFieldsForTerms } from '../../common/fields_utils'; const SUPPORTED_FORMATTERS = ['bytes', 'percent', 'number']; @@ -99,13 +100,21 @@ export const triggerTSVBtoLensConfiguration = async ( } const palette = layer.palette as PaletteOutput; + const splitFields = getFieldsForTerms(layer.terms_field); // in case of terms in a date field, we want to apply the date_histogram let splitWithDateHistogram = false; - if (layer.terms_field && layer.split_mode === 'terms') { - const fieldType = await getFieldType(indexPatternId, layer.terms_field); - if (fieldType === 'date') { - splitWithDateHistogram = true; + if (layer.terms_field && layer.split_mode === 'terms' && splitFields) { + for (const f of splitFields) { + const fieldType = await getFieldType(indexPatternId, f); + + if (fieldType === 'date') { + if (splitFields.length === 1) { + splitWithDateHistogram = true; + } else { + return null; + } + } } } @@ -114,7 +123,7 @@ export const triggerTSVBtoLensConfiguration = async ( timeFieldName: timeField, chartType, axisPosition: layer.separate_axis ? layer.axis_position : model.axis_position, - ...(layer.terms_field && { splitField: layer.terms_field }), + ...(layer.terms_field && { splitFields }), splitWithDateHistogram, ...(layer.split_mode !== 'everything' && { splitMode: layer.split_mode }), ...(splitFilters.length > 0 && { splitFilters }), diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts index 2b63749fac6427..9f8bf18683ca5b 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts @@ -14,7 +14,7 @@ import { handleErrorResponse } from './handle_error_response'; import { processBucket } from './table/process_bucket'; import { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher'; -import { extractFieldLabel } from '../../../common/fields_utils'; +import { getFieldsForTerms, getMultiFieldLabel } from '../../../common/fields_utils'; import { isAggSupported } from './helpers/check_aggs'; import { isConfigurationFeatureEnabled } from '../../../common/check_ui_restrictions'; import { FilterCannotBeAppliedError, PivotNotSelectedForTableError } from '../../../common/errors'; @@ -62,12 +62,15 @@ export async function getTableData( }); const calculatePivotLabel = async () => { - if (panel.pivot_id && panelIndex.indexPattern?.id) { - const fields = await extractFields({ id: panelIndex.indexPattern.id }); + const pivotIds = getFieldsForTerms(panel.pivot_id); - return extractFieldLabel(fields, panel.pivot_id); + if (pivotIds.length) { + const fields = panelIndex.indexPattern?.id + ? await extractFields({ id: panelIndex.indexPattern.id }) + : []; + + return getMultiFieldLabel(pivotIds, fields); } - return panel.pivot_id; }; const meta: DataResponseMeta = { @@ -85,7 +88,7 @@ export async function getTableData( } }); - if (!panel.pivot_id) { + if (!getFieldsForTerms(panel.pivot_id).length) { throw new PivotNotSelectedForTableError(); } diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts index 1754fa6569dcd9..a28883b72980b2 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts @@ -54,7 +54,7 @@ export async function getSplits { expect(doc.aggs.test.meta).toMatchInlineSnapshot(` Object { - "index": undefined, + "dataViewId": undefined, + "indexPatternString": undefined, "intervalString": "900000ms", "panelId": "panelId", "seriesId": "test", diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js index 497b0106deec47..6cefb08c23686f 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js @@ -7,12 +7,13 @@ */ import { overwrite } from '../../helpers'; +import { getFieldsForTerms } from '../../../../../common/fields_utils'; export function splitByEverything(req, panel, series) { return (next) => (doc) => { if ( series.split_mode === 'everything' || - (series.split_mode === 'terms' && !series.terms_field) + (series.split_mode === 'terms' && !getFieldsForTerms(series.terms_field).length) ) { overwrite(doc, `aggs.${series.id}.filter.match_all`, {}); } diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js index 9c2bdbe03f8866..07e8ef4a0e5fde 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js @@ -10,25 +10,41 @@ import { overwrite } from '../../helpers'; import { basicAggs } from '../../../../../common/basic_aggs'; import { getBucketsPath } from '../../helpers/get_buckets_path'; import { bucketTransform } from '../../helpers/bucket_transform'; -import { validateField } from '../../../../../common/fields_utils'; +import { getFieldsForTerms, validateField } from '../../../../../common/fields_utils'; export function splitByTerms(req, panel, series, esQueryConfig, seriesIndex) { return (next) => (doc) => { - if (series.split_mode === 'terms' && series.terms_field) { - const termsField = series.terms_field; + const termsIds = getFieldsForTerms(series.terms_field); + + if (series.split_mode === 'terms' && termsIds.length) { + const termsType = termsIds.length > 1 ? 'multi_terms' : 'terms'; const orderByTerms = series.terms_order_by; - validateField(termsField, seriesIndex); + termsIds.forEach((termsField) => { + validateField(termsField, seriesIndex); + }); const direction = series.terms_direction || 'desc'; const metric = series.metrics.find((item) => item.id === orderByTerms); - overwrite(doc, `aggs.${series.id}.terms.field`, termsField); - overwrite(doc, `aggs.${series.id}.terms.size`, series.terms_size); + + if (termsType === 'multi_terms') { + overwrite( + doc, + `aggs.${series.id}.${termsType}.terms`, + termsIds.map((item) => ({ + field: item, + })) + ); + } else { + overwrite(doc, `aggs.${series.id}.${termsType}.field`, termsIds[0]); + } + + overwrite(doc, `aggs.${series.id}.${termsType}.size`, series.terms_size); if (series.terms_include) { - overwrite(doc, `aggs.${series.id}.terms.include`, series.terms_include); + overwrite(doc, `aggs.${series.id}.${termsType}.include`, series.terms_include); } if (series.terms_exclude) { - overwrite(doc, `aggs.${series.id}.terms.exclude`, series.terms_exclude); + overwrite(doc, `aggs.${series.id}.${termsType}.exclude`, series.terms_exclude); } if (metric && metric.type !== 'count' && ~basicAggs.indexOf(metric.type)) { const sortAggKey = `${orderByTerms}-SORT`; @@ -37,12 +53,12 @@ export function splitByTerms(req, panel, series, esQueryConfig, seriesIndex) { orderByTerms, sortAggKey ); - overwrite(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction }); + overwrite(doc, `aggs.${series.id}.${termsType}.order`, { [bucketPath]: direction }); overwrite(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) }); } else if (['_key', '_count'].includes(orderByTerms)) { - overwrite(doc, `aggs.${series.id}.terms.order`, { [orderByTerms]: direction }); + overwrite(doc, `aggs.${series.id}.${termsType}.order`, { [orderByTerms]: direction }); } else { - overwrite(doc, `aggs.${series.id}.terms.order`, { _count: direction }); + overwrite(doc, `aggs.${series.id}.${termsType}.order`, { _count: direction }); } } return next(doc); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts index a458c870be7d9a..246c133e936085 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts @@ -24,7 +24,8 @@ export const dateHistogram: TableRequestProcessorsFunction = const meta: TableSearchRequestMeta = { timeField, - index: panel.use_kibana_indexes ? seriesIndex.indexPattern?.id : undefined, + dataViewId: panel.use_kibana_indexes ? seriesIndex.indexPattern?.id : undefined, + indexPatternString: seriesIndex.indexPatternString, panelId: panel.id, }; diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/pivot.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/pivot.ts index 692d4ea23bc596..4c28cfb442e144 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/pivot.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/pivot.ts @@ -8,7 +8,7 @@ import { get, last } from 'lodash'; import { overwrite, getBucketsPath, bucketTransform } from '../../helpers'; - +import { getFieldsForTerms } from '../../../../../common/fields_utils'; import { basicAggs } from '../../../../../common/basic_aggs'; import type { TableRequestProcessorsFunction } from './types'; @@ -18,15 +18,29 @@ export const pivot: TableRequestProcessorsFunction = (next) => (doc) => { const { sort } = req.body.state; + const pivotIds = getFieldsForTerms(panel.pivot_id); + const termsType = pivotIds.length > 1 ? 'multi_terms' : 'terms'; + + if (pivotIds.length) { + if (termsType === 'multi_terms') { + overwrite( + doc, + `aggs.pivot.${termsType}.terms`, + pivotIds.map((item: string) => ({ + field: item, + })) + ); + } else { + overwrite(doc, `aggs.pivot.${termsType}.field`, pivotIds[0]); + } + + overwrite(doc, `aggs.pivot.${termsType}.size`, panel.pivot_rows); - if (panel.pivot_id) { - overwrite(doc, 'aggs.pivot.terms.field', panel.pivot_id); - overwrite(doc, 'aggs.pivot.terms.size', panel.pivot_rows); if (sort) { const series = panel.series.find((item) => item.id === sort.column); const metric = series && last(series.metrics); if (metric && metric.type === 'count') { - overwrite(doc, 'aggs.pivot.terms.order', { _count: sort.order }); + overwrite(doc, `aggs.pivot.${termsType}.order`, { _count: sort.order }); } else if (metric && series && basicAggs.includes(metric.type)) { const sortAggKey = `${metric.id}-SORT`; const fn = bucketTransform[metric.type]; @@ -34,10 +48,10 @@ export const pivot: TableRequestProcessorsFunction = metric.id, sortAggKey ); - overwrite(doc, `aggs.pivot.terms.order`, { [bucketPath]: sort.order }); + overwrite(doc, `aggs.pivot.${termsType}.order`, { [bucketPath]: sort.order }); overwrite(doc, `aggs.pivot.aggs`, { [sortAggKey]: fn(metric) }); } else { - overwrite(doc, 'aggs.pivot.terms.order', { + overwrite(doc, `aggs.pivot.${termsType}.order`, { _key: get(sort, 'order', 'asc'), }); } diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/types.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/types.ts index 58124c825e9166..2c6aa2a495766e 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/types.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/types.ts @@ -7,5 +7,6 @@ */ export interface BaseMeta { - index?: string; + dataViewId?: string; + indexPatternString?: string; } diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/format_label.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/format_label.ts index 6d824c1c7f43ed..813cf1a5b9a6fa 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/format_label.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/format_label.ts @@ -6,46 +6,67 @@ * Side Public License, v 1. */ -import { KBN_FIELD_TYPES } from '@kbn/field-types'; -import { BUCKET_TYPES, PANEL_TYPES } from '../../../../../common/enums'; +import { BUCKET_TYPES, PANEL_TYPES, TSVB_METRIC_TYPES } from '../../../../../common/enums'; +import { + createCachedFieldValueFormatter, + getFieldsForTerms, + MULTI_FIELD_VALUES_SEPARATOR, +} from '../../../../../common/fields_utils'; import type { Panel, PanelData, Series } from '../../../../../common/types'; import type { FieldFormatsRegistry } from '../../../../../../../field_formats/common'; import type { createFieldsFetcher } from '../../../search_strategies/lib/fields_fetcher'; import type { CachedIndexPatternFetcher } from '../../../search_strategies/lib/cached_index_pattern_fetcher'; +import type { BaseMeta } from '../../request_processors/types'; +import { SanitizedFieldType } from '../../../../../common/types'; export function formatLabel( resp: unknown, panel: Panel, series: Series, - meta: any, + meta: BaseMeta, extractFields: ReturnType, fieldFormatService: FieldFormatsRegistry, cachedIndexPatternFetcher: CachedIndexPatternFetcher ) { return (next: (results: PanelData[]) => unknown) => async (results: PanelData[]) => { const { terms_field: termsField, split_mode: splitMode } = series; + const termsIds = getFieldsForTerms(termsField); - const isKibanaIndexPattern = panel.use_kibana_indexes || panel.index_pattern === ''; - // no need to format labels for markdown as they also used there as variables keys const shouldFormatLabels = - isKibanaIndexPattern && - termsField && + // no need to format labels for series_agg + !series.metrics.some((m) => m.type === TSVB_METRIC_TYPES.SERIES_AGG) && + termsIds.length && splitMode === BUCKET_TYPES.TERMS && + // no need to format labels for markdown as they also used there as variables keys panel.type !== PANEL_TYPES.MARKDOWN; if (shouldFormatLabels) { - const { indexPattern } = await cachedIndexPatternFetcher({ id: meta.index }); - const getFieldFormatByName = (fieldName: string) => - fieldFormatService.deserialize(indexPattern?.fieldFormatMap?.[fieldName]); + const fetchedIndex = meta.dataViewId + ? await cachedIndexPatternFetcher({ id: meta.dataViewId }) + : undefined; + + let fields: SanitizedFieldType[] = []; + + if (!fetchedIndex?.indexPattern && meta.indexPatternString) { + fields = await extractFields(meta.indexPatternString); + } + + const formatField = createCachedFieldValueFormatter( + fetchedIndex?.indexPattern, + fields, + fieldFormatService + ); results .filter(({ seriesId }) => series.id === seriesId) .forEach((item) => { - const formattedLabel = getFieldFormatByName(termsField!).convert(item.label); - item.label = formattedLabel; - const termsFieldType = indexPattern?.fields.find(({ name }) => name === termsField)?.type; - if (termsFieldType === KBN_FIELD_TYPES.DATE) { - item.labelFormatted = formattedLabel; + const formatted = termsIds + .map((i, index) => formatField(i, [item.label].flat()[index])) + .join(MULTI_FIELD_VALUES_SEPARATOR); + + if (formatted) { + item.label = formatted; + item.labelFormatted = formatted; } }); } diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/series_agg.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/series_agg.js index 532f5fd07f597d..305d6c6fc5b34c 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/series_agg.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/series_agg.js @@ -33,7 +33,7 @@ export function seriesAgg(resp, panel, series, meta, extractFields) { return (fn && fn(acc)) || acc; }, targetSeries); - const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : []; + const fieldsForSeries = meta.dataViewId ? await extractFields({ id: meta.dataViewId }) : []; results.push({ id: `${series.id}`, diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/series_agg.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/series_agg.ts index b4bc082bab849a..45829a930605fd 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/series_agg.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/series_agg.ts @@ -36,7 +36,7 @@ export const seriesAgg: TableResponseProcessorsFunction = const fn = SeriesAgg[series.aggregate_function]; const data = fn(targetSeries); - const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : []; + const fieldsForSeries = meta.dataViewId ? await extractFields({ id: meta.dataViewId }) : []; results.push({ id: `${series.id}`, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index efc3bbf8314f8a..3d3c98ce4aaeae 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -39,7 +39,7 @@ import { ExpressionAstExpression, } from '../../../../plugins/expressions/public'; import { Vis, SerializedVis } from '../vis'; -import { getExpressions, getTheme, getUiActions } from '../services'; +import { getExecutionContext, getExpressions, getTheme, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { getSavedVisualization } from '../utils/saved_visualize_utils'; @@ -398,20 +398,18 @@ export class VisualizeEmbeddable }; private async updateHandler() { - const parentContext = this.parent?.getInput().executionContext; + const parentContext = this.parent?.getInput().executionContext || getExecutionContext().get(); const child: KibanaExecutionContext = { type: 'visualization', name: this.vis.type.name, - id: this.vis.id ?? 'an_unsaved_vis', + id: this.vis.id ?? 'new', description: this.vis.title || this.input.title || this.vis.type.name, url: this.output.editUrl, }; - const context = parentContext - ? { - ...parentContext, - child, - } - : child; + const context = { + ...parentContext, + child, + }; const expressionParams: IExpressionLoaderParams = { searchContext: { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 92bcf1dfe6a964..88b9d35d5255f9 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -36,6 +36,7 @@ import { setDocLinks, setSpaces, setTheme, + setExecutionContext, } from './services'; import { createVisEmbeddableFromObject, @@ -372,6 +373,7 @@ export class VisualizationsPlugin setTimeFilter(data.query.timefilter.timefilter); setAggs(data.search.aggs); setOverlays(core.overlays); + setExecutionContext(core.executionContext); setChrome(core.chrome); if (spaces) { diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index 37aea45fa3f589..8564c8225f1a79 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -16,6 +16,7 @@ import type { SavedObjectsStart, DocLinksStart, ThemeServiceStart, + ExecutionContextSetup, } from '../../../core/public'; import type { TypesStart } from './vis_types'; import { createGetterSetter } from '../../../plugins/kibana_utils/public'; @@ -65,4 +66,7 @@ export const [getOverlays, setOverlays] = createGetterSetter('Over export const [getChrome, setChrome] = createGetterSetter('Chrome'); +export const [getExecutionContext, setExecutionContext] = + createGetterSetter('ExecutionContext'); + export const [getSpaces, setSpaces] = createGetterSetter('Spaces', false); diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index b89af7bd2cdbf6..60332c0f66964b 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -98,7 +98,7 @@ export interface VisualizeEditorLayersContext { chartType?: string; axisPosition?: string; termsParams?: Record; - splitField?: string; + splitFields?: string[]; splitMode?: string; splitFilters?: SplitByFilters[]; palette?: PaletteOutput; diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx index 45241ec501084c..c281b211768a10 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { EventEmitter } from 'events'; -import { useKibana } from '../../../../kibana_react/public'; +import { useExecutionContext, useKibana } from '../../../../kibana_react/public'; import { useChromeVisibility, useSavedVisInstance, @@ -41,6 +41,14 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { originatingApp, visualizationIdFromUrl ); + + const editorName = savedVisInstance?.vis.type.title.toLowerCase().replace(' ', '_') || ''; + useExecutionContext(services.executionContext, { + type: 'application', + page: `editor${editorName ? `:${editorName}` : ''}`, + id: visualizationIdFromUrl || 'new', + }); + const { appState, hasUnappliedChanges } = useVisualizeAppState( services, eventEmitter, diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx index cf219b1cda117d..a180cf78feeb2e 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -21,7 +21,7 @@ import { findListItems } from '../../utils/saved_visualize_utils'; import { showNewVisModal } from '../../wizard'; import { getTypes } from '../../services'; import { SavedObjectsFindOptionsReference } from '../../../../../core/public'; -import { useKibana, TableListView } from '../../../../kibana_react/public'; +import { useKibana, TableListView, useExecutionContext } from '../../../../kibana_react/public'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../visualizations/public'; import { VisualizeServices } from '../types'; import { VisualizeConstants } from '../../../common/constants'; @@ -31,6 +31,7 @@ export const VisualizeListing = () => { const { services: { application, + executionContext, chrome, history, toastNotifications, @@ -49,6 +50,11 @@ export const VisualizeListing = () => { const closeNewVisModal = useRef(() => {}); const listingLimit = savedObjectsPublic.settings.getListingLimit(); + useExecutionContext(executionContext, { + type: 'application', + page: 'list', + }); + useEffect(() => { if (pathname === '/new') { // In case the user navigated to the page via the /visualize/new URL we start the dialog immediately diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index 4d3915f5f22946..c49aefe91925f8 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be(35); + expect(resp.body.length).to.be(36); // Test for sample data card expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above( diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index 1161e7b493f419..7c4751220fa1f1 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -40,29 +40,33 @@ export class TestUser extends FtrService { super(ctx); } - async restoreDefaults(shouldRefreshBrowser: boolean = true) { - if (this.enabled) { - await this.setRoles(this.config.get('security.defaultRoles'), shouldRefreshBrowser); + async restoreDefaults(options?: { skipBrowserRefresh?: boolean }) { + if (!this.enabled) { + return; } + + await this.setRoles(this.config.get('security.defaultRoles'), options); } - async setRoles(roles: string[], shouldRefreshBrowser: boolean = true) { - if (this.enabled) { - this.log.debug(`set roles = ${roles}`); - await this.user.create(TEST_USER_NAME, { - password: TEST_USER_PASSWORD, - roles, - full_name: 'test user', - }); - - if (this.browser && this.testSubjects && shouldRefreshBrowser) { - if (await this.testSubjects.exists('kibanaChrome', { allowHidden: true })) { - await this.browser.refresh(); - // accept alert if it pops up - const alert = await this.browser.getAlert(); - await alert?.accept(); - await this.testSubjects.find('kibanaChrome', this.config.get('timeouts.find') * 10); - } + async setRoles(roles: string[], options?: { skipBrowserRefresh?: boolean }) { + if (!this.enabled) { + return; + } + + this.log.debug(`set roles = ${roles}`); + await this.user.create(TEST_USER_NAME, { + password: TEST_USER_PASSWORD, + roles, + full_name: 'test user', + }); + + if (this.browser && this.testSubjects && !options?.skipBrowserRefresh) { + if (await this.testSubjects.exists('kibanaChrome', { allowHidden: true })) { + await this.browser.refresh(); + // accept alert if it pops up + const alert = await this.browser.getAlert(); + await alert?.accept(); + await this.testSubjects.find('kibanaChrome', this.config.get('timeouts.find') * 10); } } } diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts index 24b10e1df04956..3cca75234675bc 100644 --- a/test/functional/apps/discover/_huge_fields.ts +++ b/test/functional/apps/discover/_huge_fields.ts @@ -18,7 +18,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('test large number of fields in sidebar', function () { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/huge_fields'); - await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], { + skipBrowserRefresh: true, + }); await kibanaServer.uiSettings.update({ 'timepicker:timeDefaults': `{ "from": "2016-10-05T00:00:00", "to": "2016-10-06T00:00:00"}`, }); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 4080ca2a0ba75d..ec3852b309d3f7 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await security.testUser.setRoles( ['kibana_admin', 'test_logstash_reader', 'kibana_sample_admin'], - false + { skipBrowserRefresh: true } ); await visualize.navigateToNewVisualization(); await visualize.clickVisualBuilder(); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 4054656028b6eb..f08845b230710d 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -802,9 +802,7 @@ export class VisualBuilderPageObject extends FtrService { } public async checkSelectedMetricsGroupByValue(value: string) { - const groupBy = await this.find.byCssSelector( - '.tvbAggRow--split [data-test-subj="comboBoxInput"]' - ); + const groupBy = await this.testSubjects.find('groupBySelect'); return await this.comboBox.isOptionSelected(groupBy, value); } diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts index 94daa0030cd600..15f5a37edb910e 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts @@ -33,6 +33,10 @@ describe('wrapScopedClusterClient', () => { jest.useRealTimers(); }); + afterEach(() => { + jest.resetAllMocks(); + }); + test('searches with asInternalUser when specified', async () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); @@ -119,6 +123,35 @@ describe('wrapScopedClusterClient', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong!"`); }); + test('handles empty search result object', async () => { + const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const childClient = elasticsearchServiceMock.createElasticsearchClient(); + + ( + scopedClusterClient.asInternalUser as unknown as jest.Mocked + ).child.mockReturnValue(childClient as unknown as Client); + const asInternalUserWrappedSearchFn = childClient.search; + // @ts-ignore incomplete return type + asInternalUserWrappedSearchFn.mockResolvedValue({}); + + const wrappedSearchClientFactory = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + }); + + const wrappedSearchClient = wrappedSearchClientFactory.client(); + await wrappedSearchClient.asInternalUser.search(esQuery); + + expect(asInternalUserWrappedSearchFn).toHaveBeenCalledTimes(1); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + + const stats = wrappedSearchClientFactory.getMetrics(); + expect(stats.numSearches).toEqual(1); + expect(stats.esSearchDurationMs).toEqual(0); + }); + test('keeps track of number of queries', async () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts index 00ee3302c88c51..dfe32a48ce4384 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts @@ -158,7 +158,7 @@ function getWrappedSearchFn(opts: WrapEsClientOpts) { took = (result as SearchResponse).took; } - opts.logMetricsFn({ esSearchDuration: took, totalSearchDuration: durationMs }); + opts.logMetricsFn({ esSearchDuration: took ?? 0, totalSearchDuration: durationMs }); return result; } catch (e) { throw e; diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index d58630e18cd4d8..a94b30b59104cd 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -11,6 +11,7 @@ import { Alert, AlertFactoryDoneUtils } from './alert'; import { elasticsearchServiceMock, savedObjectsClientMock, + uiSettingsServiceMock, } from '../../../../src/core/server/mocks'; import { AlertInstanceContext, AlertInstanceState } from './types'; @@ -105,6 +106,7 @@ const createAlertServicesMock = < done: jest.fn().mockReturnValue(alertFactoryMockDone), }, savedObjectsClient: savedObjectsClientMock.create(), + uiSettingsClient: uiSettingsServiceMock.createClient(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), shouldWriteAlerts: () => true, shouldStopExecution: () => true, diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 760aa6e0050a9b..939068e23e2b4a 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -384,6 +384,7 @@ export class AlertingPlugin { taskRunnerFactory.initialize({ logger, savedObjects: core.savedObjects, + uiSettings: core.uiSettings, elasticsearch: core.elasticsearch, getRulesClientWithRequest, spaceIdToNamespace, diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json index d6ebd25d4af375..806e72fa33d5d0 100644 --- a/x-pack/plugins/alerting/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerting/server/saved_objects/mappings.json @@ -155,6 +155,10 @@ } } } + }, + "snoozeEndTime": { + "type": "date", + "format": "strict_date_time" } } } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 99feefb472df1c..bdebc66911e94d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -30,6 +30,7 @@ import { executionContextServiceMock, savedObjectsServiceMock, elasticsearchServiceMock, + uiSettingsServiceMock, } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; @@ -95,6 +96,7 @@ describe('Task Runner', () => { const ruleTypeRegistry = ruleTypeRegistryMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); const elasticsearchService = elasticsearchServiceMock.createInternalStart(); + const uiSettingsService = uiSettingsServiceMock.createStartContract(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -106,6 +108,7 @@ describe('Task Runner', () => { const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { savedObjects: savedObjectsService, + uiSettings: uiSettingsService, elasticsearch: elasticsearchService, actionsPlugin: actionsMock.createStart(), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index dbc7749a0fbdf6..c05bdc3cf7bd94 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -352,14 +352,17 @@ export class TaskRunner< }] namespace`, }; + const savedObjectsClient = this.context.savedObjects.getScopedClient(fakeRequest, { + includedHiddenTypes: ['alert', 'action'], + }); + updatedRuleTypeState = await this.context.executionContext.withContext(ctx, () => this.ruleType.executor({ alertId: ruleId, executionId: this.executionId, services: { - savedObjectsClient: this.context.savedObjects.getScopedClient(fakeRequest, { - includedHiddenTypes: ['alert', 'action'], - }), + savedObjectsClient, + uiSettingsClient: this.context.uiSettings.asScopedToClient(savedObjectsClient), scopedClusterClient: wrappedScopedClusterClient.client(), alertFactory: createAlertFactory< InstanceState, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index d70b36ff48a8f3..add8d7a24912da 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -25,6 +25,7 @@ import { executionContextServiceMock, savedObjectsServiceMock, elasticsearchServiceMock, + uiSettingsServiceMock, } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; @@ -94,6 +95,7 @@ describe('Task Runner Cancel', () => { const ruleTypeRegistry = ruleTypeRegistryMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); const elasticsearchService = elasticsearchServiceMock.createInternalStart(); + const uiSettingsService = uiSettingsServiceMock.createStartContract(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -103,6 +105,7 @@ describe('Task Runner Cancel', () => { const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { savedObjects: savedObjectsService, + uiSettings: uiSettingsService, elasticsearch: elasticsearchService, actionsPlugin: actionsMock.createStart(), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 6dea8df475503c..d4e92015d41129 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -16,6 +16,7 @@ import { httpServiceMock, savedObjectsServiceMock, elasticsearchServiceMock, + uiSettingsServiceMock, } from '../../../../../src/core/server/mocks'; import { actionsMock } from '../../../actions/server/mocks'; import { rulesClientMock } from '../mocks'; @@ -28,6 +29,7 @@ const executionContext = executionContextServiceMock.createSetupContract(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); +const uiSettingsService = uiSettingsServiceMock.createStartContract(); const elasticsearchService = elasticsearchServiceMock.createInternalStart(); const ruleType: UntypedNormalizedRuleType = { id: 'test', @@ -77,6 +79,7 @@ describe('Task Runner Factory', () => { const taskRunnerFactoryInitializerParams: jest.Mocked = { savedObjects: savedObjectsService, + uiSettings: uiSettingsService, elasticsearch: elasticsearchService, getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), actionsPlugin: actionsMock.createStart(), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index f60370dd7daf7f..0b8ffe2f93d7bc 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -15,6 +15,7 @@ import type { ExecutionContextStart, SavedObjectsServiceStart, ElasticsearchServiceStart, + UiSettingsServiceStart, } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; @@ -35,6 +36,7 @@ import { NormalizedRuleType } from '../rule_type_registry'; export interface TaskRunnerContext { logger: Logger; savedObjects: SavedObjectsServiceStart; + uiSettings: UiSettingsServiceStart; elasticsearch: ElasticsearchServiceStart; getRulesClientWithRequest(request: KibanaRequest): PublicMethodsOf; actionsPlugin: ActionsPluginStartContract; diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 95c1a07e241b21..8a0b61fed787a4 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -11,6 +11,7 @@ import type { RequestHandlerContext, SavedObjectReference, ElasticsearchClient, + IUiSettingsClient, } from 'src/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { AlertFactoryDoneUtils, PublicAlert } from './alert'; @@ -78,6 +79,7 @@ export interface AlertServices< ActionGroupIds extends string = never > { savedObjectsClient: SavedObjectsClientContract; + uiSettingsClient: IUiSettingsClient; scopedClusterClient: IScopedClusterClient; alertFactory: { create: (id: string) => PublicAlert; @@ -248,6 +250,7 @@ export interface RawRule extends SavedObjectAttributes { meta?: AlertMeta; executionStatus: RawRuleExecutionStatus; monitoring?: RuleMonitoring; + snoozeEndTime?: string; } export type AlertInfoParams = Pick< diff --git a/x-pack/plugins/apm/common/environment_filter_values.ts b/x-pack/plugins/apm/common/environment_filter_values.ts index f0bd386f36de89..ddd1ffd9b8d451 100644 --- a/x-pack/plugins/apm/common/environment_filter_values.ts +++ b/x-pack/plugins/apm/common/environment_filter_values.ts @@ -12,6 +12,13 @@ import { Environment } from './environment_rt'; const ENVIRONMENT_ALL_VALUE = 'ENVIRONMENT_ALL' as const; const ENVIRONMENT_NOT_DEFINED_VALUE = 'ENVIRONMENT_NOT_DEFINED' as const; +export const allOptionText = i18n.translate( + 'xpack.apm.filter.environment.allLabel', + { + defaultMessage: 'All', + } +); + export function getEnvironmentLabel(environment: string) { if (!environment || environment === ENVIRONMENT_NOT_DEFINED_VALUE) { return i18n.translate('xpack.apm.filter.environment.notDefinedLabel', { @@ -20,19 +27,24 @@ export function getEnvironmentLabel(environment: string) { } if (environment === ENVIRONMENT_ALL_VALUE) { - return i18n.translate('xpack.apm.filter.environment.allLabel', { - defaultMessage: 'All', - }); + return allOptionText; } return environment; } -export const ENVIRONMENT_ALL = { +// #TODO Once we replace the select dropdown we can remove it +// EuiSelect > EuiSelectOption accepts text attribute +export const ENVIRONMENT_ALL_SELECT_OPTION = { value: ENVIRONMENT_ALL_VALUE, text: getEnvironmentLabel(ENVIRONMENT_ALL_VALUE), }; +export const ENVIRONMENT_ALL = { + value: ENVIRONMENT_ALL_VALUE, + label: getEnvironmentLabel(ENVIRONMENT_ALL_VALUE), +}; + export const ENVIRONMENT_NOT_DEFINED = { value: ENVIRONMENT_NOT_DEFINED_VALUE, text: getEnvironmentLabel(ENVIRONMENT_NOT_DEFINED_VALUE), diff --git a/x-pack/plugins/apm/dev_docs/testing.md b/x-pack/plugins/apm/dev_docs/testing.md index f6a8298ef9d0c5..6c35979add784d 100644 --- a/x-pack/plugins/apm/dev_docs/testing.md +++ b/x-pack/plugins/apm/dev_docs/testing.md @@ -37,6 +37,7 @@ Once the tests finish, the instances will be terminated. ``` node scripts/test/api --server ``` + Start Elasticsearch and Kibana instances. ### Run all tests @@ -44,6 +45,7 @@ Start Elasticsearch and Kibana instances. ``` node scripts/test/api --runner ``` + Run all tests. The test server needs to be running, see [Start Test Server](#start-test-server). ### Update snapshots (from Kibana root) @@ -53,6 +55,7 @@ To update snapshots append `--updateSnapshots` to the `functional_test_runner` c ``` node scripts/functional_test_runner --config x-pack/test/apm_api_integration/[basic | trial]/config.ts --quiet --updateSnapshots ``` + The test server needs to be running, see [Start Test Server](#start-test-server). The API tests are located in [`x-pack/test/apm_api_integration/`](/x-pack/test/apm_api_integration/). @@ -66,11 +69,23 @@ The API tests are located in [`x-pack/test/apm_api_integration/`](/x-pack/test/a ## E2E Tests (Cypress) +The E2E tests are located in [`x-pack/plugins/apm/ftr_e2e`](../ftr_e2e) + +### Start test server + ``` -node scripts/test/e2e [--trial] [--help] +node x-pack/plugins/apm/scripts/test/e2e.js --server ``` -The E2E tests are located in [`x-pack/plugins/apm/ftr_e2e`](../ftr_e2e) +### Run tests + +``` +node x-pack/plugins/apm/scripts/test/e2e.js --open +``` + +### A11y checks + +Accessibility tests are added on the e2e with `checkA11y()`, they will run together with cypress. --- diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts index 2c2e93d463c508..22ac5a72733e40 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/dependencies.spec.ts @@ -6,6 +6,7 @@ */ import { synthtrace } from '../../../synthtrace'; import { opbeans } from '../../fixtures/synthtrace/opbeans'; +import { checkA11y } from '../../support/commands'; const start = '2021-10-10T00:00:00.000Z'; const end = '2021-10-10T00:15:00.000Z'; @@ -43,6 +44,17 @@ describe('Dependencies', () => { cy.contains('h1', 'postgresql'); }); + + it('has no detectable a11y violations on load', () => { + cy.visit( + `/app/apm/services/opbeans-java/dependencies?${new URLSearchParams( + timeRange + )}` + ); + cy.contains('a[role="tab"]', 'Dependencies'); + // set skipFailures to true to not fail the test when there are accessibility failures + checkA11y({ skipFailures: true }); + }); }); describe('dependency overview page', () => { @@ -62,6 +74,18 @@ describe('Dependencies', () => { cy.contains('h1', 'opbeans-java'); }); + + it('has no detectable a11y violations on load', () => { + cy.visit( + `/app/apm/backends/overview?${new URLSearchParams({ + ...timeRange, + backendName: 'postgresql', + })}` + ); + cy.contains('h1', 'postgresql'); + // set skipFailures to true to not fail the test when there are accessibility failures + checkA11y({ skipFailures: true }); + }); }); describe('service overview page', () => { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts index f2479b74007322..beaf1837c834c8 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts @@ -7,6 +7,7 @@ import url from 'url'; import { synthtrace } from '../../../../synthtrace'; +import { checkA11y } from '../../../support/commands'; import { generateData } from './generate_data'; const start = '2021-10-10T00:00:00.000Z'; @@ -39,6 +40,13 @@ describe('Error details', () => { await synthtrace.clean(); }); + it('has no detectable a11y violations on load', () => { + cy.visit(errorDetailsPageHref); + cy.contains('Error group 00000'); + // set skipFailures to true to not fail the test when there are accessibility failures + checkA11y({ skipFailures: true }); + }); + describe('when error has no occurrences', () => { it('shows an empty message', () => { cy.visit( diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts index d08e22092d5927..6ff4795cbcb18b 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts @@ -7,6 +7,7 @@ import url from 'url'; import { synthtrace } from '../../../../synthtrace'; +import { checkA11y } from '../../../support/commands'; import { generateData } from './generate_data'; const start = '2021-10-10T00:00:00.000Z'; @@ -41,6 +42,13 @@ describe('Errors page', () => { await synthtrace.clean(); }); + it('has no detectable a11y violations on load', () => { + cy.visit(javaServiceErrorsPageHref); + cy.contains('Error occurrences'); + // set skipFailures to true to not fail the test when there are accessibility failures + checkA11y({ skipFailures: true }); + }); + describe('when service has no errors', () => { it('shows empty message', () => { cy.visit(nodeServiceErrorsPageHref); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts index f74a1d122e4266..40afece0ce908c 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts @@ -7,6 +7,7 @@ import url from 'url'; import { synthtrace } from '../../../../synthtrace'; import { opbeans } from '../../../fixtures/synthtrace/opbeans'; +import { checkA11y } from '../../../support/commands'; const timeRange = { rangeFrom: '2021-10-10T00:00:00.000Z', @@ -53,6 +54,12 @@ describe('When navigating to the service inventory', () => { cy.visit(serviceInventoryHref); }); + it('has no detectable a11y violations on load', () => { + cy.contains('h1', 'Services'); + // set skipFailures to true to not fail the test when there are accessibility failures + checkA11y({ skipFailures: true }); + }); + it('has a list of services', () => { cy.contains('opbeans-node'); cy.contains('opbeans-java'); @@ -93,7 +100,7 @@ describe('When navigating to the service inventory', () => { cy.wait(aliasNames); }); - it.skip('when selecting a different time range and clicking the update button', () => { + it('when selecting a different time range and clicking the update button', () => { cy.wait(aliasNames); cy.selectAbsoluteTimeRange( diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index 31586651cbb846..fcd9e472cc7eb4 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -8,6 +8,7 @@ import url from 'url'; import { synthtrace } from '../../../../synthtrace'; import { opbeans } from '../../../fixtures/synthtrace/opbeans'; +import { checkA11y } from '../../../support/commands'; const start = '2021-10-10T00:00:00.000Z'; const end = '2021-10-10T00:15:00.000Z'; @@ -102,6 +103,13 @@ describe('Service Overview', () => { cy.loginAsReadOnlyUser(); cy.visit(baseUrl); }); + + it('has no detectable a11y violations on load', () => { + cy.contains('opbeans-node'); + // set skipFailures to true to not fail the test when there are accessibility failures + checkA11y({ skipFailures: true }); + }); + it('transaction latency chart', () => { cy.get('[data-test-subj="latencyChart"]'); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts index 3deb4b8619f603..fb8468f42474ec 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts @@ -8,11 +8,12 @@ import url from 'url'; import { synthtrace } from '../../../../synthtrace'; import { opbeans } from '../../../fixtures/synthtrace/opbeans'; +import { checkA11y } from '../../../support/commands'; const start = '2021-10-10T00:00:00.000Z'; const end = '2021-10-10T00:15:00.000Z'; -const serviceOverviewHref = url.format({ +const serviceTransactionsHref = url.format({ pathname: '/app/apm/services/opbeans-node/transactions', query: { rangeFrom: start, rangeTo: end }, }); @@ -35,8 +36,18 @@ describe('Transactions Overview', () => { cy.loginAsReadOnlyUser(); }); + it('has no detectable a11y violations on load', () => { + cy.visit(serviceTransactionsHref); + cy.contains('aria-selected="true"', 'Transactions').should( + 'have.class', + 'euiTab-isSelected' + ); + // set skipFailures to true to not fail the test when there are accessibility failures + checkA11y({ skipFailures: true }); + }); + it('persists transaction type selected when navigating to Overview tab', () => { - cy.visit(serviceOverviewHref); + cy.visit(serviceTransactionsHref); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index cb66d6db809f34..91edae9046f6d9 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -6,6 +6,11 @@ */ import 'cypress-real-events/support'; import { Interception } from 'cypress/types/net-stubbing'; +import 'cypress-axe'; +import { + AXE_CONFIG, + AXE_OPTIONS, +} from 'test/accessibility/services/a11y/constants'; Cypress.Commands.add('loginAsReadOnlyUser', () => { cy.loginAs({ username: 'apm_read_user', password: 'changeme' }); @@ -78,3 +83,25 @@ Cypress.Commands.add( }); } ); + +// A11y configuration + +const axeConfig = { + ...AXE_CONFIG, +}; +const axeOptions = { + ...AXE_OPTIONS, + runOnly: [...AXE_OPTIONS.runOnly, 'best-practice'], +}; + +export const checkA11y = ({ skipFailures }: { skipFailures: boolean }) => { + // https://github.com/component-driven/cypress-axe#cychecka11y + cy.injectAxe(); + cy.configureAxe(axeConfig); + const context = '.kbnAppWrapper'; // Scopes a11y checks to only our app + /** + * We can get rid of the last two params when we don't need to add skipFailures + * params = (context, options, violationCallback, skipFailures) + */ + cy.checkA11y(context, axeOptions, undefined, skipFailures); +}; diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index 171953ea522eb4..4abbd97d98db46 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiComboBoxOptionOption, EuiFieldNumber } from '@elastic/eui'; +import { EuiFieldNumber } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { @@ -16,22 +16,11 @@ import { import { ENVIRONMENT_ALL, getEnvironmentLabel, + allOptionText, } from '../../../common/environment_filter_values'; import { SuggestionsSelect } from '../shared/suggestions_select'; import { PopoverExpression } from './service_alert_trigger/popover_expression'; -const allOptionText = i18n.translate('xpack.apm.alerting.fields.allOption', { - defaultMessage: 'All', -}); -const allOption: EuiComboBoxOptionOption = { - label: allOptionText, - value: allOptionText, -}; -const environmentAllOption: EuiComboBoxOptionOption = { - label: ENVIRONMENT_ALL.text, - value: ENVIRONMENT_ALL.value, -}; - export function ServiceField({ allowAll = true, currentValue, @@ -43,13 +32,13 @@ export function ServiceField({ }) { return ( + >; + isDisabled: boolean; value?: string; - disabled: boolean; - onChange: (event: React.ChangeEvent) => void; + onChange: (value?: string) => void; } export function FormRowSelect({ @@ -30,10 +30,25 @@ export function FormRowSelect({ fieldLabel, isLoading, options, - value, - disabled, + isDisabled, onChange, }: Props) { + const [selectedOptions, setSelected] = useState< + Array> | undefined + >([]); + + const handleOnChange = ( + nextSelectedOptions: Array> + ) => { + const [selectedOption] = nextSelectedOptions; + setSelected(nextSelectedOptions); + onChange(selectedOption.value); + }; + + useEffect(() => { + setSelected(undefined); + }, [isLoading]); + return ( - diff --git a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx new file mode 100644 index 00000000000000..f3f680ff4a9ffa --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { SuggestionsSelect } from '../../../../../shared/suggestions_select'; +import { ENVIRONMENT_ALL } from '../../../../../../../common/environment_filter_values'; + +interface Props { + title: string; + field: string; + description: string; + fieldLabel: string; + value?: string; + allowAll?: boolean; + onChange: (value?: string) => void; +} + +export function FormRowSuggestionsSelect({ + title, + field, + description, + fieldLabel, + value, + allowAll = true, + onChange, +}: Props) { + return ( + {title}} + description={description} + > + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx index 6f141a0ad8d566..9f8d3ca1318b56 100644 --- a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx @@ -18,7 +18,8 @@ import { import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/use_fetcher'; import { FormRowSelect } from './form_row_select'; import { APMLink } from '../../../../../shared/links/apm/apm_link'; - +import { FormRowSuggestionsSelect } from './form_row_suggestions_select'; +import { SERVICE_NAME } from '../../../../../../../common/elasticsearch_fieldnames'; interface Props { newConfig: AgentConfigurationIntake; setNewConfig: React.Dispatch>; @@ -26,17 +27,6 @@ interface Props { } export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { - const { data: serviceNamesData, status: serviceNamesStatus } = useFetcher( - (callApmApi) => { - return callApmApi('GET /api/apm/settings/agent-configuration/services', { - isCachable: true, - }); - }, - [], - { preservePreviousData: false } - ); - const serviceNames = serviceNamesData?.serviceNames ?? []; - const { data: environmentsData, status: environmentsStatus } = useFetcher( (callApmApi) => { if (newConfig.service.name) { @@ -81,14 +71,10 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { { defaultMessage: 'already configured' } ); - const serviceNameOptions = serviceNames.map((name) => ({ - text: getOptionLabel(name), - value: name, - })); const environmentOptions = environments.map( ({ name, alreadyConfigured }) => ({ disabled: alreadyConfigured, - text: `${getOptionLabel(name)} ${ + label: `${getOptionLabel(name)} ${ alreadyConfigured ? `(${ALREADY_CONFIGURED_TRANSLATED})` : '' }`, value: name, @@ -98,7 +84,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { return ( <> {/* Service name options */} - { - e.preventDefault(); - const name = e.target.value; + onChange={(name) => { setNewConfig((prev) => ({ ...prev, service: { name, environment: '' }, })); }} /> - {/* Environment options */} { - e.preventDefault(); - const environment = e.target.value; + onChange={(environment) => { setNewConfig((prev) => ({ ...prev, service: { name: prev.service.name, environment }, })); }} /> - - {/* Cancel button */} diff --git a/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/filters_section.tsx b/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/filters_section.tsx index 3ef8697cde8d98..3b1438b4dddb04 100644 --- a/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/filters_section.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/filters_section.tsx @@ -7,7 +7,6 @@ import { EuiButtonEmpty, - EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiSelect, @@ -27,6 +26,7 @@ import { FILTER_SELECT_OPTIONS, getSelectOptions, } from './helper'; +import { SuggestionsSelect } from '../../../../shared/suggestions_select'; export function FiltersSection({ filters, @@ -117,15 +117,17 @@ export function FiltersSection({ /> - onChangeFilter(key, e.target.value, idx)} - value={value} + onChange={(selectedValue) => + onChangeFilter(key, selectedValue as string, idx) + } + defaultValue={value} isInvalid={!isEmpty(key) && isEmpty(value)} /> diff --git a/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx b/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx index 64d137cae0c27e..9a3d677b3f0788 100644 --- a/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx @@ -11,7 +11,7 @@ import { History } from 'history'; import React from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { - ENVIRONMENT_ALL, + ENVIRONMENT_ALL_SELECT_OPTION, ENVIRONMENT_NOT_DEFINED, } from '../../../../common/environment_filter_values'; import { fromQuery, toQuery } from '../links/url_helpers'; @@ -51,7 +51,7 @@ function getOptions(environments: string[]) { })); return [ - ENVIRONMENT_ALL, + ENVIRONMENT_ALL_SELECT_OPTION, ...(environments.includes(ENVIRONMENT_NOT_DEFINED.value) ? [ENVIRONMENT_NOT_DEFINED] : []), diff --git a/x-pack/plugins/apm/public/components/shared/suggestions_select/index.tsx b/x-pack/plugins/apm/public/components/shared/suggestions_select/index.tsx index 2d735ec4ea7083..8b8907af4bc215 100644 --- a/x-pack/plugins/apm/public/components/shared/suggestions_select/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/suggestions_select/index.tsx @@ -6,17 +6,20 @@ */ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { debounce } from 'lodash'; +import { throttle } from 'lodash'; import React, { useCallback, useState } from 'react'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; interface SuggestionsSelectProps { allOption?: EuiComboBoxOptionOption; - customOptionText: string; + customOptionText?: string; defaultValue?: string; field: string; onChange: (value?: string) => void; + isClearable?: boolean; + isInvalid?: boolean; placeholder: string; + dataTestSubj?: string; } export function SuggestionsSelect({ @@ -26,13 +29,12 @@ export function SuggestionsSelect({ field, onChange, placeholder, + isInvalid, + dataTestSubj, + isClearable = true, }: SuggestionsSelectProps) { - const allowAll = !!allOption; let defaultOption: EuiComboBoxOptionOption | undefined; - if (allowAll && !defaultValue) { - defaultOption = allOption; - } if (defaultValue) { defaultOption = { label: defaultValue, value: defaultValue }; } @@ -57,6 +59,11 @@ export function SuggestionsSelect({ const handleChange = useCallback( (changedOptions: Array>) => { setSelectedOptions(changedOptions); + + if (changedOptions.length === 0) { + onChange(''); + } + if (changedOptions.length === 1) { onChange( changedOptions[0].value @@ -91,17 +98,19 @@ export function SuggestionsSelect({ return ( ); } diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx index 851472cfedabe4..f2919fc12cad6a 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx @@ -375,8 +375,12 @@ describe('TransactionActionMenu component', () => { const getFilterKeyValue = (key: string) => { return { [(component.getAllByText(key)[0] as HTMLOptionElement).text]: ( - component.getAllByTestId(`${key}.value`)[0] as HTMLInputElement - ).value, + component + .getByTestId(`${key}.value`) + .querySelector( + '[data-test-subj="comboBoxInput"] span' + ) as HTMLSpanElement + ).textContent, }; }; expect(getFilterKeyValue('service.name')).toEqual({ diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/routes/settings/agent_configuration/__snapshots__/queries.test.ts.snap index b6b4f2208d04f5..6009dd3ad7b969 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -148,31 +148,6 @@ Object { } `; -exports[`agent configuration queries getServiceNames fetches service names 1`] = ` -Object { - "apm": Object { - "events": Array [ - "transaction", - "error", - "metric", - ], - }, - "body": Object { - "aggs": Object { - "services": Object { - "terms": Object { - "field": "service.name", - "min_doc_count": 0, - "size": 50, - }, - }, - }, - "size": 0, - "timeout": "1ms", - }, -} -`; - exports[`agent configuration queries listConfigurations fetches configurations 1`] = ` Object { "index": "myIndex", diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_service_names.ts deleted file mode 100644 index 18e359c5b94259..00000000000000 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_service_names.ts +++ /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 { ProcessorEvent } from '../../../../common/processor_event'; -import { Setup } from '../../../lib/helpers/setup_request'; -import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; -import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; -import { getProcessorEventForTransactions } from '../../../lib/helpers/transactions'; - -export async function getServiceNames({ - setup, - searchAggregatedTransactions, - size, -}: { - setup: Setup; - searchAggregatedTransactions: boolean; - size: number; -}) { - const { apmEventClient } = setup; - - const params = { - apm: { - events: [ - getProcessorEventForTransactions(searchAggregatedTransactions), - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - body: { - timeout: '1ms', - size: 0, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - min_doc_count: 0, - size, - }, - }, - }, - }, - }; - - const resp = await apmEventClient.search( - 'get_service_names_for_agent_config', - params - ); - const serviceNames = - resp.aggregations?.services.buckets - .map((bucket) => bucket.key as string) - .sort() || []; - return [ALL_OPTION_VALUE, ...serviceNames]; -} diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/queries.test.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/queries.test.ts index 4ffc8ed98184bb..49a97c1ca4f774 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/queries.test.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/queries.test.ts @@ -6,7 +6,6 @@ */ import { getExistingEnvironmentsForService } from './get_environments/get_existing_environments_for_service'; -import { getServiceNames } from './get_service_names'; import { listConfigurations } from './list_configurations'; import { searchConfigurations } from './search_configurations'; import { @@ -52,20 +51,6 @@ describe('agent configuration queries', () => { }); }); - describe('getServiceNames', () => { - it('fetches service names', async () => { - mock = await inspectSearchParams((setup) => - getServiceNames({ - setup, - searchAggregatedTransactions: false, - size: 50, - }) - ); - - expect(mock.params).toMatchSnapshot(); - }); - }); - describe('listConfigurations', () => { it('fetches configurations', async () => { mock = await inspectSearchParams((setup) => diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts index 53a55cc1b99b42..f2cfbe857ba48c 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts @@ -10,7 +10,6 @@ import Boom from '@hapi/boom'; import { toBooleanRt } from '@kbn/io-ts-utils'; import { maxSuggestions } from '../../../../../observability/common'; import { setupRequest } from '../../../lib/helpers/setup_request'; -import { getServiceNames } from './get_service_names'; import { createOrUpdateConfiguration } from './create_or_update_configuration'; import { searchConfigurations } from './search_configurations'; import { findExactConfiguration } from './find_exact_configuration'; @@ -256,33 +255,6 @@ const agentConfigurationSearchRoute = createApmServerRoute({ * Utility endpoints (not documented as part of the public API) */ -// get list of services -const listAgentConfigurationServicesRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/settings/agent-configuration/services', - options: { tags: ['access:apm'] }, - handler: async (resources): Promise<{ serviceNames: string[] }> => { - const setup = await setupRequest(resources); - const { start, end } = resources.params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions({ - apmEventClient: setup.apmEventClient, - config: setup.config, - kuery: '', - start, - end, - }); - const size = await resources.context.core.uiSettings.client.get( - maxSuggestions - ); - const serviceNames = await getServiceNames({ - searchAggregatedTransactions, - setup, - size, - }); - - return { serviceNames }; - }, -}); - // get environments for service const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/settings/agent-configuration/environments', @@ -342,7 +314,6 @@ export const agentConfigurationRouteRepository = { ...deleteAgentConfigurationRoute, ...createOrUpdateAgentConfigurationRoute, ...agentConfigurationSearchRoute, - ...listAgentConfigurationServicesRoute, ...listAgentConfigurationEnvironmentsRoute, ...agentConfigurationAgentNameRoute, }; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 95135f4a0e9a09..8abc0805b8a3fc 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -185,7 +185,7 @@ export interface RuleEcs { id?: string[]; rule_id?: string[]; name?: string[]; - false_positives: string[]; + false_positives?: string[]; saved_id?: string[]; timeline_id?: string[]; timeline_title?: string[]; diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index c4535c8f8da2f6..046eb67d38b248 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -254,3 +254,17 @@ export const MAX_LENGTH_ERROR = (field: string, length: number) => export const LINK_APPROPRIATE_LICENSE = i18n.translate('xpack.cases.common.appropriateLicense', { defaultMessage: 'appropriate license', }); + +export const CASE_SUCCESS_TOAST = (title: string) => + i18n.translate('xpack.cases.actions.caseSuccessToast', { + values: { title }, + defaultMessage: 'An alert has been added to "{title}"', + }); + +export const CASE_SUCCESS_SYNC_TEXT = i18n.translate('xpack.cases.actions.caseSuccessSyncText', { + defaultMessage: 'Alerts in this case have their status synched with the case status', +}); + +export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', { + defaultMessage: 'View Case', +}); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx new file mode 100644 index 00000000000000..9bd6a6675a5c16 --- /dev/null +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useToasts } from '../common/lib/kibana'; +import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../common/mock'; +import { CaseToastSuccessContent, useCasesToast } from './use_cases_toast'; +import { mockCase } from '../containers/mock'; +import React from 'react'; +import userEvent from '@testing-library/user-event'; + +jest.mock('../common/lib/kibana'); + +const useToastsMock = useToasts as jest.Mock; + +describe('Use cases toast hook', () => { + describe('Toast hook', () => { + const successMock = jest.fn(); + useToastsMock.mockImplementation(() => { + return { + addSuccess: successMock, + }; + }); + it('should create a success tost when invoked with a case', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach(mockCase); + expect(successMock).toHaveBeenCalled(); + }); + }); + describe('Toast content', () => { + let appMockRender: AppMockRenderer; + const onViewCaseClick = jest.fn(); + beforeEach(() => { + appMockRender = createAppMockRenderer(); + onViewCaseClick.mockReset(); + }); + + it('renders a correct successfull message with synced alerts', () => { + const result = appMockRender.render( + + ); + expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent( + 'Alerts in this case have their status synched with the case status' + ); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); + expect(onViewCaseClick).not.toHaveBeenCalled(); + }); + + it('renders a correct successfull message with not synced alerts', () => { + const result = appMockRender.render( + + ); + expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); + expect(onViewCaseClick).not.toHaveBeenCalled(); + }); + + it('Calls the onViewCaseClick when clicked', () => { + const result = appMockRender.render( + + ); + userEvent.click(result.getByTestId('toaster-content-case-view-link')); + expect(onViewCaseClick).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx new file mode 100644 index 00000000000000..98cc7fa1d8faa0 --- /dev/null +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonEmpty, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { Case } from '../../common'; +import { useToasts } from '../common/lib/kibana'; +import { useCaseViewNavigation } from '../common/navigation'; +import { CASE_SUCCESS_SYNC_TEXT, CASE_SUCCESS_TOAST, VIEW_CASE } from './translations'; + +const LINE_CLAMP = 3; +const Title = styled.span` + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: ${LINE_CLAMP}; + -webkit-box-orient: vertical; + overflow: hidden; +`; +const EuiTextStyled = styled(EuiText)` + ${({ theme }) => ` + margin-bottom: ${theme.eui?.paddingSizes?.s ?? 8}px; + `} +`; + +export const useCasesToast = () => { + const { navigateToCaseView } = useCaseViewNavigation(); + + const toasts = useToasts(); + + return { + showSuccessAttach: (theCase: Case) => { + const onViewCaseClick = () => { + navigateToCaseView({ + detailName: theCase.id, + }); + }; + return toasts.addSuccess({ + color: 'success', + iconType: 'check', + title: toMountPoint({CASE_SUCCESS_TOAST(theCase.title)}), + text: toMountPoint( + + ), + }); + }, + }; +}; +export const CaseToastSuccessContent = ({ + syncAlerts, + onViewCaseClick, +}: { + syncAlerts: boolean; + onViewCaseClick: () => void; +}) => { + return ( + <> + {syncAlerts && ( + + {CASE_SUCCESS_SYNC_TEXT} + + )} + + {VIEW_CASE} + + + ); +}; +CaseToastSuccessContent.displayName = 'CaseToastSuccessContent'; diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx index 08c99c51593997..ba553b28a34e09 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx @@ -26,7 +26,7 @@ export interface AllCasesSelectorModalProps { */ alertData?: Omit; hiddenStatuses?: CaseStatusWithAllStatus[]; - onRowClick: (theCase?: Case) => void; + onRowClick?: (theCase?: Case) => void; updateCase?: (newCase: Case) => void; onClose?: () => void; attachments?: CaseAttachments; @@ -52,7 +52,9 @@ export const AllCasesSelectorModal = React.memo( const onClick = useCallback( (theCase?: Case) => { closeModal(); - onRowClick(theCase); + if (onRowClick) { + onRowClick(theCase); + } }, [closeModal, onRowClick] ); diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_cases_add_to_existing_case_modal.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx similarity index 89% rename from x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_cases_add_to_existing_case_modal.test.tsx rename to x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx index 6eeff6102ae6a3..6a224949db8be2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_cases_add_to_existing_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { CasesContext } from '../../cases_context'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesAddToExistingCaseModal } from './use_cases_add_to_existing_case_modal'; +jest.mock('../../../common/use_cases_toast'); describe('use cases add to existing case modal hook', () => { const dispatch = jest.fn(); @@ -65,7 +66,7 @@ describe('use cases add to existing case modal hook', () => { ); }); - it('should dispatch the close action when invoked', () => { + it('should dispatch the close action for modal and flyout when invoked', () => { const { result } = renderHook( () => { return useCasesAddToExistingCaseModal(defaultParams()); @@ -78,5 +79,10 @@ describe('use cases add to existing case modal hook', () => { type: CasesContextStoreActionsList.CLOSE_ADD_TO_CASE_MODAL, }) ); + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: CasesContextStoreActionsList.CLOSE_CREATE_CASE_FLYOUT, + }) + ); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx index b2ad07c2375dfe..5341f5be4183d5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx @@ -7,15 +7,36 @@ import { useCallback } from 'react'; import { AllCasesSelectorModalProps } from '.'; +import { useCasesToast } from '../../../common/use_cases_toast'; +import { Case } from '../../../containers/types'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesContext } from '../../cases_context/use_cases_context'; +import { useCasesAddToNewCaseFlyout } from '../../create/flyout/use_cases_add_to_new_case_flyout'; export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps) => { + const createNewCaseFlyout = useCasesAddToNewCaseFlyout({ + attachments: props.attachments, + onClose: props.onClose, + // TODO there's no need for onSuccess to be async. This will be fixed + // in a follow up clean up + onSuccess: async (theCase?: Case) => { + if (props.onRowClick) { + return props.onRowClick(theCase); + } + }, + }); const { dispatch } = useCasesContext(); + const casesToasts = useCasesToast(); + const closeModal = useCallback(() => { dispatch({ type: CasesContextStoreActionsList.CLOSE_ADD_TO_CASE_MODAL, }); + // in case the flyout was also open when selecting + // create a new case + dispatch({ + type: CasesContextStoreActionsList.CLOSE_CREATE_CASE_FLYOUT, + }); }, [dispatch]); const openModal = useCallback(() => { @@ -23,6 +44,19 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, payload: { ...props, + onRowClick: (theCase?: Case) => { + // when the case is undefined in the modal + // the user clicked "create new case" + if (theCase === undefined) { + closeModal(); + createNewCaseFlyout.open(); + } else { + casesToasts.showSuccessAttach(theCase); + if (props.onRowClick) { + props.onRowClick(theCase); + } + } + }, onClose: () => { closeModal(); if (props.onClose) { @@ -37,7 +71,7 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps }, }, }); - }, [closeModal, dispatch, props]); + }, [casesToasts, closeModal, createNewCaseFlyout, dispatch, props]); return { open: openModal, close: closeModal, diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx index 103e24c4b7a656..e569b1ee799526 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { CasesContext } from '../../cases_context'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesAddToNewCaseFlyout } from './use_cases_add_to_new_case_flyout'; +jest.mock('../../../common/use_cases_toast'); describe('use cases add to new case flyout hook', () => { const dispatch = jest.fn(); diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx index e4ae4d72f48dab..5422ab9be995d9 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx @@ -6,12 +6,15 @@ */ import { useCallback } from 'react'; +import { useCasesToast } from '../../../common/use_cases_toast'; +import { Case } from '../../../containers/types'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesContext } from '../../cases_context/use_cases_context'; import { CreateCaseFlyoutProps } from './create_case_flyout'; export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { const { dispatch } = useCasesContext(); + const casesToasts = useCasesToast(); const closeFlyout = useCallback(() => { dispatch({ @@ -30,6 +33,14 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { return props.onClose(); } }, + onSuccess: async (theCase: Case) => { + if (theCase) { + casesToasts.showSuccessAttach(theCase); + } + if (props.onSuccess) { + return props.onSuccess(theCase); + } + }, afterCaseCreated: async (...args) => { closeFlyout(); if (props.afterCaseCreated) { @@ -38,7 +49,7 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { }, }, }); - }, [closeFlyout, dispatch, props]); + }, [casesToasts, closeFlyout, dispatch, props]); return { open: openFlyout, close: closeFlyout, diff --git a/x-pack/plugins/cases/public/index.tsx b/x-pack/plugins/cases/public/index.tsx index 0190df8204fc1a..42dd1a94199917 100644 --- a/x-pack/plugins/cases/public/index.tsx +++ b/x-pack/plugins/cases/public/index.tsx @@ -21,7 +21,7 @@ export type { GetCreateCaseFlyoutProps } from './methods/get_create_case_flyout' export type { GetAllCasesSelectorModalProps } from './methods/get_all_cases_selector_modal'; export type { GetRecentCasesProps } from './methods/get_recent_cases'; -export type { CaseAttachments } from './types'; +export type { CaseAttachments, SupportedCaseAttachment } from './types'; export type { ICasesDeepLinkId } from './common/navigation'; export { diff --git a/x-pack/plugins/cases/public/methods/get_rule_id_from_event.ts b/x-pack/plugins/cases/public/methods/get_rule_id_from_event.ts new file mode 100644 index 00000000000000..b07ba032e7b191 --- /dev/null +++ b/x-pack/plugins/cases/public/methods/get_rule_id_from_event.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { get } from 'lodash/fp'; +import { Ecs } from '../../common'; + +type Maybe = T | null; +interface Event { + data: EventNonEcsData[]; + ecs: Ecs; +} +interface EventNonEcsData { + field: string; + value?: Maybe; +} + +export function getRuleIdFromEvent(event: Event): { + id: string; + name: string; +} { + const ruleUuidData = event && event.data.find(({ field }) => field === ALERT_RULE_UUID); + const ruleNameData = event && event.data.find(({ field }) => field === ALERT_RULE_NAME); + const ruleUuidValueData = ruleUuidData && ruleUuidData.value && ruleUuidData.value[0]; + const ruleNameValueData = ruleNameData && ruleNameData.value && ruleNameData.value[0]; + + const ruleUuid = + ruleUuidValueData ?? + get(`ecs.${ALERT_RULE_UUID}[0]`, event) ?? + get(`ecs.signal.rule.id[0]`, event) ?? + null; + const ruleName = + ruleNameValueData ?? + get(`ecs.${ALERT_RULE_NAME}[0]`, event) ?? + get(`ecs.signal.rule.name[0]`, event) ?? + null; + + return { + id: ruleUuid, + name: ruleName, + }; +} diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts index f7f80170a87750..a3876e9e19322e 100644 --- a/x-pack/plugins/cases/public/mocks.ts +++ b/x-pack/plugins/cases/public/mocks.ts @@ -5,12 +5,13 @@ * 2.0. */ +import { mockCasesContext } from './mocks/mock_cases_context'; import { CasesUiStart } from './types'; -const createStartContract = (): jest.Mocked => ({ +export const mockCasesContract = (): jest.Mocked => ({ canUseCases: jest.fn(), getCases: jest.fn(), - getCasesContext: jest.fn(), + getCasesContext: jest.fn().mockImplementation(() => mockCasesContext), getAllCasesSelectorModal: jest.fn(), getAllCasesSelectorModalNoProvider: jest.fn(), getCreateCaseFlyout: jest.fn(), @@ -20,8 +21,11 @@ const createStartContract = (): jest.Mocked => ({ getUseCasesAddToNewCaseFlyout: jest.fn(), getUseCasesAddToExistingCaseModal: jest.fn(), }, + helpers: { + getRuleIdFromEvent: jest.fn(), + }, }); export const casesPluginMock = { - createStartContract, + createStartContract: mockCasesContract, }; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_cases_context.tsx b/x-pack/plugins/cases/public/mocks/mock_cases_context.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/mock/mock_cases_context.tsx rename to x-pack/plugins/cases/public/mocks/mock_cases_context.tsx diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 9dbc6ea35125ad..31185ae78f076c 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -21,6 +21,7 @@ import { CasesUiConfigType } from '../common/ui/types'; import { getCasesContextLazy } from './methods/get_cases_context'; import { useCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout'; import { useCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal'; +import { getRuleIdFromEvent } from './methods/get_rule_id_from_event'; /** * @public @@ -52,6 +53,9 @@ export class CasesUiPlugin implements Plugin ({ type TableProps = PropsOf; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/126664 +describe.skip('', () => { it('renders the zero state when status success and data has a length of zero ', async () => { const props: TableProps = { status: 'success', diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/multi_select_picker/multi_select_picker.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/multi_select_picker/multi_select_picker.tsx index 8b95296b5f8236..24523450a0e7d3 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/multi_select_picker/multi_select_picker.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/multi_select_picker/multi_select_picker.tsx @@ -15,13 +15,16 @@ import { EuiPopoverTitle, EuiSpacer, } from '@elastic/eui'; -import React, { FC, ReactNode, useEffect, useState } from 'react'; +import React, { FC, ReactNode, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { euiDarkVars as euiThemeDark, euiLightVars as euiThemeLight } from '@kbn/ui-theme'; +import { useDataVisualizerKibana } from '../../../kibana_context'; export interface Option { name?: string | ReactNode; value: string; checked?: 'on' | 'off'; + disabled?: boolean; } const NoFilterItems = () => { @@ -41,6 +44,15 @@ const NoFilterItems = () => { ); }; +export function useCurrentEuiTheme() { + const { services } = useDataVisualizerKibana(); + const uiSettings = services.uiSettings; + return useMemo( + () => (uiSettings.get('theme:darkMode') ? euiThemeDark : euiThemeLight), + [uiSettings] + ); +} + export const MultiSelectPicker: FC<{ options: Option[]; onChange?: (items: string[]) => void; @@ -48,6 +60,8 @@ export const MultiSelectPicker: FC<{ checkedOptions: string[]; dataTestSubj: string; }> = ({ options, onChange, title, checkedOptions, dataTestSubj }) => { + const euiTheme = useCurrentEuiTheme(); + const [items, setItems] = useState(options); const [searchTerm, setSearchTerm] = useState(''); @@ -68,6 +82,7 @@ export const MultiSelectPicker: FC<{ const closePopover = () => { setIsPopoverOpen(false); + setSearchTerm(''); }; const handleOnChange = (index: number) => { @@ -126,7 +141,13 @@ export const MultiSelectPicker: FC<{ checked={checked ? 'on' : undefined} key={index} onClick={() => handleOnChange(index)} - style={{ flexDirection: 'row' }} + style={{ + flexDirection: 'row', + color: + item.disabled === true + ? euiTheme.euiColorDisabledText + : euiTheme.euiTextColor, + }} data-test-subj={`${dataTestSubj}-option-${item.value}${ checked ? '-checked' : '' }`} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_name_filter.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_name_filter.tsx index 634bf25dbc8a01..c2d0e5d61402dc 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_name_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_name_filter.tsx @@ -35,6 +35,8 @@ export const DataVisualizerFieldNamesFilter: FC = ({ field.fieldName !== undefined ) { options.push({ value: field.fieldName }); + } else { + options.push({ value: field.fieldName, disabled: true }); } }); } diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index f84668068f413a..7a6203c994f4d9 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -7,21 +7,37 @@ import { i18n } from '@kbn/i18n'; -export const ENTERPRISE_SEARCH_PLUGIN = { +export const ENTERPRISE_SEARCH_OVERVIEW_PLUGIN = { ID: 'enterpriseSearch', - NAME: i18n.translate('xpack.enterpriseSearch.productName', { + NAME: i18n.translate('xpack.enterpriseSearch.overview.productName', { defaultMessage: 'Enterprise Search', }), - NAV_TITLE: i18n.translate('xpack.enterpriseSearch.navTitle', { + NAV_TITLE: i18n.translate('xpack.enterpriseSearch.overview.navTitle', { defaultMessage: 'Overview', }), - DESCRIPTION: i18n.translate('xpack.enterpriseSearch.FeatureCatalogue.description', { + DESCRIPTION: i18n.translate('xpack.enterpriseSearch.overview.description', { defaultMessage: 'Create search experiences with a refined set of APIs and tools.', }), URL: '/app/enterprise_search/overview', LOGO: 'logoEnterpriseSearch', }; +export const ENTERPRISE_SEARCH_CONTENT_PLUGIN = { + ID: 'enterpriseSearchContent', + NAME: i18n.translate('xpack.enterpriseSearch.content.productName', { + defaultMessage: 'Enterprise Search', + }), + NAV_TITLE: i18n.translate('xpack.enterpriseSearch.content.navTitle', { + defaultMessage: 'Content', + }), + DESCRIPTION: i18n.translate('xpack.enterpriseSearch.content.description', { + defaultMessage: + 'Enterprise search offers a number of ways to easily make your data searchable. Choose from the web crawler, Elasticsearch indices, API, direct uploads, or thrid party connectors.', // TODO: Make sure this content is correct. + }), + URL: '/app/enterprise_search/content', + LOGO: 'logoEnterpriseSearch', +}; + export const APP_SEARCH_PLUGIN = { ID: 'appSearch', NAME: i18n.translate('xpack.enterpriseSearch.appSearch.productName', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx index dce56a05f8f8cc..c2b40a2c0aa084 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EnterpriseSearchPageTemplate } from '../../../../shared/layout'; +import { EnterpriseSearchPageTemplateWrapper } from '../../../../shared/layout'; import { rerender } from '../../../../test_helpers'; jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); @@ -55,7 +55,7 @@ describe('Curation', () => { setMockValues({ dataLoading: true }); const wrapper = shallow(); - expect(wrapper.is(EnterpriseSearchPageTemplate)).toBe(true); + expect(wrapper.is(EnterpriseSearchPageTemplateWrapper)).toBe(true); }); it('renders a view for automated curations', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx index d1b0f43d976a8a..1c49c077e7a6aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EnterpriseSearchPageTemplate } from '../../../../shared/layout'; +import { EnterpriseSearchPageTemplateWrapper } from '../../../../shared/layout'; import { AutomatedCuration } from './automated_curation'; import { CurationLogic } from './curation_logic'; @@ -26,7 +26,7 @@ export const Curation: React.FC = () => { }, [curationId]); if (dataLoading) { - return ; + return ; } return isAutomated ? : ; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/page_template.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/page_template.test.tsx index 8f47d5f1c46444..b26cc00379f34d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/page_template.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/page_template.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { SetAppSearchChrome } from '../../../shared/kibana_chrome'; -import { EnterpriseSearchPageTemplate } from '../../../shared/layout'; +import { EnterpriseSearchPageTemplateWrapper } from '../../../shared/layout'; import { SendAppSearchTelemetry } from '../../../shared/telemetry'; import { AppSearchPageTemplate } from './page_template'; @@ -27,7 +27,7 @@ describe('AppSearchPageTemplate', () => { ); - expect(wrapper.type()).toEqual(EnterpriseSearchPageTemplate); + expect(wrapper.type()).toEqual(EnterpriseSearchPageTemplateWrapper); expect(wrapper.prop('solutionNav')).toEqual({ name: 'App Search', items: [] }); expect(wrapper.find('.hello').text()).toEqual('world'); }); @@ -35,7 +35,9 @@ describe('AppSearchPageTemplate', () => { describe('page chrome', () => { it('takes a breadcrumb array & renders a product-specific page chrome', () => { const wrapper = shallow(); - const setPageChrome = wrapper.find(EnterpriseSearchPageTemplate).prop('setPageChrome') as any; + const setPageChrome = wrapper + .find(EnterpriseSearchPageTemplateWrapper) + .prop('setPageChrome') as any; expect(setPageChrome.type).toEqual(SetAppSearchChrome); expect(setPageChrome.props.trail).toEqual(['Some page']); @@ -51,7 +53,7 @@ describe('AppSearchPageTemplate', () => { }); }); - it('passes down any ...pageTemplateProps that EnterpriseSearchPageTemplate accepts', () => { + it('passes down any ...pageTemplateProps that EnterpriseSearchPageTemplateWrapper accepts', () => { const wrapper = shallow( { /> ); - expect(wrapper.find(EnterpriseSearchPageTemplate).prop('pageHeader')!.pageTitle).toEqual( + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('pageHeader')!.pageTitle).toEqual( 'hello world' ); - expect(wrapper.find(EnterpriseSearchPageTemplate).prop('isLoading')).toEqual(false); - expect(wrapper.find(EnterpriseSearchPageTemplate).prop('emptyState')).toEqual(
); + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('isLoading')).toEqual(false); + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('emptyState')).toEqual(
); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/page_template.tsx index 31f2eb3215e05a..d336bcc6a4c5fb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/page_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/page_template.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetAppSearchChrome } from '../../../shared/kibana_chrome'; -import { EnterpriseSearchPageTemplate, PageTemplateProps } from '../../../shared/layout'; +import { EnterpriseSearchPageTemplateWrapper, PageTemplateProps } from '../../../shared/layout'; import { SendAppSearchTelemetry } from '../../../shared/telemetry'; import { useAppSearchNav } from './nav'; @@ -21,7 +21,7 @@ export const AppSearchPageTemplate: React.FC = ({ ...pageTemplateProps }) => { return ( - = ({ > {pageViewTelemetry && } {children} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/assets/app_search.png similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/assets/app_search.png diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/assets/workplace_search.png similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/assets/workplace_search.png diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/error_connecting/error_connecting.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/error_connecting/error_connecting.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/error_connecting/error_connecting.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/error_connecting/error_connecting.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/error_connecting/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/error_connecting/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/license_callout/constants.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/license_callout/constants.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/license_callout/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/index.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/license_callout/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/license_callout/license_callout.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/license_callout/license_callout.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/license_callout/license_callout.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/license_callout/license_callout.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.scss similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/index.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/lock_light.svg b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/lock_light.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/lock_light.svg rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/lock_light.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/assets/getting_started.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/assets/getting_started.png similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/assets/getting_started.png rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/assets/getting_started.png diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide.tsx similarity index 93% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide.tsx index 1a25d1a7a8d1e2..c8889320bb3464 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide.tsx @@ -11,7 +11,7 @@ import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { ENTERPRISE_SEARCH_OVERVIEW_PLUGIN } from '../../../../../common/constants'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; @@ -20,7 +20,7 @@ import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide_cta.scss similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide_cta.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide_cta.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide_cta.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide_cta.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/setup_guide/setup_guide_cta.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/trial_callout/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/index.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/trial_callout/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/trial_callout/trial_callout.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/trial_callout/trial_callout.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/trial_callout/trial_callout.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/trial_callout/trial_callout.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/constants.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/constants.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress.json b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress.json similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress.json rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress.json diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/integration/overview.spec.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/integration/overview.spec.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/integration/overview.spec.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/integration/overview.spec.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/tsconfig.json b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/tsconfig.json similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/tsconfig.json rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/cypress/tsconfig.json diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/index.test.tsx similarity index 87% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/index.test.tsx index 4aef227582d318..e5d883ee819f77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/index.test.tsx @@ -18,15 +18,15 @@ import { ErrorConnecting } from './components/error_connecting'; import { ProductSelector } from './components/product_selector'; import { SetupGuide } from './components/setup_guide'; -import { EnterpriseSearch } from './'; +import { EnterpriseSearchOverview } from './'; -describe('EnterpriseSearch', () => { +describe('EnterpriseSearchOverview', () => { it('renders the Setup Guide and Product Selector', () => { setMockValues({ errorConnectingMessage: '', config: { host: 'localhost' }, }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(SetupGuide)).toHaveLength(1); expect(wrapper.find(ProductSelector)).toHaveLength(1); @@ -37,7 +37,7 @@ describe('EnterpriseSearch', () => { errorConnectingMessage: '502 Bad Gateway', config: { host: 'localhost' }, }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(VersionMismatchPage)).toHaveLength(0); const errorConnecting = wrapper.find(ErrorConnecting); @@ -61,7 +61,7 @@ describe('EnterpriseSearch', () => { config: { host: 'localhost' }, }); const wrapper = shallow( - + ); expect(wrapper.find(VersionMismatchPage)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/index.tsx similarity index 96% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/index.tsx index 5f1c7b5072be23..ca4b91d0e03b97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/index.tsx @@ -21,7 +21,7 @@ import { ProductSelector } from './components/product_selector'; import { SetupGuide } from './components/setup_guide'; import { ROOT_PATH, SETUP_GUIDE_PATH } from './routes'; -export const EnterpriseSearch: React.FC = ({ +export const EnterpriseSearchOverview: React.FC = ({ access = {}, workplaceSearch, enterpriseSearchVersion, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/routes.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search/routes.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/routes.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 356a3c26b910e9..cb47dfe124780e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -15,7 +15,7 @@ import { licensingMock } from '../../../licensing/public/mocks'; import { securityMock } from '../../../security/public/mocks'; import { AppSearch } from './app_search'; -import { EnterpriseSearch } from './enterprise_search'; +import { EnterpriseSearchOverview } from './enterprise_search_overview'; import { KibanaLogic } from './shared/kibana'; import { WorkplaceSearch } from './workplace_search'; @@ -62,8 +62,8 @@ describe('renderApp', () => { describe('Enterprise Search apps', () => { afterEach(() => unmount()); - it('renders EnterpriseSearch', () => { - mount(EnterpriseSearch); + it('renders EnterpriseSearchOverview', () => { + mount(EnterpriseSearchOverview); expect(mockContainer.querySelector('.kbnPageTemplate')).not.toBeNull(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 5855dc6990f6a7..8864600475c122 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -10,7 +10,7 @@ import { useValues } from 'kea'; import { EuiBreadcrumb } from '@elastic/eui'; import { - ENTERPRISE_SEARCH_PLUGIN, + ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../../../../common/constants'; @@ -97,8 +97,8 @@ export const useEuiBreadcrumbs = (breadcrumbs: Breadcrumbs): EuiBreadcrumb[] => export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useEuiBreadcrumbs([ { - text: ENTERPRISE_SEARCH_PLUGIN.NAME, - path: ENTERPRISE_SEARCH_PLUGIN.URL, + text: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAME, + path: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, shouldNotCreateHref: true, }, ...breadcrumbs, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts index 650aa00d1801d9..8b91b7e57a7813 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts @@ -6,7 +6,7 @@ */ import { - ENTERPRISE_SEARCH_PLUGIN, + ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../../../../common/constants'; @@ -30,7 +30,7 @@ export const generateTitle = (pages: Title) => pages.join(' - '); */ export const enterpriseSearchTitle = (page: Title = []) => - generateTitle([...page, ENTERPRISE_SEARCH_PLUGIN.NAME]); + generateTitle([...page, ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAME]); export const appSearchTitle = (page: Title = []) => generateTitle([...page, APP_SEARCH_PLUGIN.NAME]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts index 79919e925c625e..790d72943a1bc8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts @@ -6,5 +6,5 @@ */ export type { PageTemplateProps } from './page_template'; -export { EnterpriseSearchPageTemplate } from './page_template'; +export { EnterpriseSearchPageTemplateWrapper } from './page_template'; export { generateNavLink } from './nav_link_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.test.tsx index 8d480b69b3fe53..22c976dfa7638f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.test.tsx @@ -17,25 +17,25 @@ import { KibanaPageTemplate } from '../../../../../../../src/plugins/kibana_reac import { FlashMessages } from '../flash_messages'; import { Loading } from '../loading'; -import { EnterpriseSearchPageTemplate } from './page_template'; +import { EnterpriseSearchPageTemplateWrapper } from './page_template'; -describe('EnterpriseSearchPageTemplate', () => { +describe('EnterpriseSearchPageTemplateWrapper', () => { beforeEach(() => { jest.clearAllMocks(); setMockValues({ readOnlyMode: false }); }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.type()).toEqual(KibanaPageTemplate); }); it('renders children', () => { const wrapper = shallow( - +
world
-
+
); expect(wrapper.find('.hello').text()).toEqual('world'); @@ -44,9 +44,9 @@ describe('EnterpriseSearchPageTemplate', () => { describe('loading state', () => { it('renders a loading icon in place of children', () => { const wrapper = shallow( - +
- + ); expect(wrapper.find(Loading).exists()).toBe(true); @@ -55,9 +55,9 @@ describe('EnterpriseSearchPageTemplate', () => { it('renders children & does not render a loading icon when the page is done loading', () => { const wrapper = shallow( - +
- + ); expect(wrapper.find(Loading).exists()).toBe(false); @@ -68,12 +68,12 @@ describe('EnterpriseSearchPageTemplate', () => { describe('empty state', () => { it('renders a custom empty state in place of children', () => { const wrapper = shallow( - Nothing here yet!
} >
- + ); expect(wrapper.find('.emptyState').exists()).toBe(true); @@ -85,12 +85,12 @@ describe('EnterpriseSearchPageTemplate', () => { it('does not render the custom empty state if the page is not empty', () => { const wrapper = shallow( - Nothing here yet!
} >
- + ); expect(wrapper.find('.emptyState').exists()).toBe(false); @@ -99,7 +99,7 @@ describe('EnterpriseSearchPageTemplate', () => { it('does not render an empty state if the page is still loading', () => { const wrapper = shallow( - } @@ -114,14 +114,14 @@ describe('EnterpriseSearchPageTemplate', () => { describe('read-only mode', () => { it('renders a callout if in read-only mode', () => { setMockValues({ readOnlyMode: true }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiCallOut).exists()).toBe(true); }); it('does not render a callout if not in read-only mode', () => { setMockValues({ readOnlyMode: false }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiCallOut).exists()).toBe(false); }); @@ -129,7 +129,7 @@ describe('EnterpriseSearchPageTemplate', () => { describe('flash messages', () => { it('renders FlashMessages by default', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(FlashMessages).exists()).toBe(true); }); @@ -137,7 +137,7 @@ describe('EnterpriseSearchPageTemplate', () => { it('does not render FlashMessages if hidden', () => { // Example use case: manually showing flash messages in an open flyout or modal // and not wanting to duplicate flash messages on the overlayed page - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(FlashMessages).exists()).toBe(false); }); @@ -147,14 +147,16 @@ describe('EnterpriseSearchPageTemplate', () => { const SetPageChrome = () =>
; it('renders a product-specific ', () => { - const wrapper = shallow(} />); + const wrapper = shallow( + } /> + ); expect(wrapper.find(SetPageChrome).exists()).toBe(true); }); it('invokes page chrome immediately (without waiting for isLoading to be finished)', () => { const wrapper = shallow( - } isLoading /> + } isLoading /> ); expect(wrapper.find(SetPageChrome).exists()).toBe(true); @@ -166,14 +168,14 @@ describe('EnterpriseSearchPageTemplate', () => { describe('EuiPageTemplate props', () => { it('overrides the restrictWidth prop', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(KibanaPageTemplate).prop('restrictWidth')).toEqual(true); }); it('passes down any ...pageTemplateProps that EuiPageTemplate accepts', () => { const wrapper = shallow( - { it('sets enterpriseSearchPageTemplate classNames while still accepting custom classNames', () => { const wrapper = shallow( - + ); expect(wrapper.find(KibanaPageTemplate).prop('className')).toEqual( @@ -200,7 +205,9 @@ describe('EnterpriseSearchPageTemplate', () => { it('automatically sets the Enterprise Search logo onto passed solution navs', () => { const wrapper = shallow( - + ); expect(wrapper.find(KibanaPageTemplate).prop('solutionNav')).toEqual({ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.tsx index 7528fa14b7ae4f..934d571418d0e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.tsx @@ -26,7 +26,7 @@ import { Loading } from '../loading'; import './page_template.scss'; /* - * EnterpriseSearchPageTemplate is a light wrapper for KibanaPageTemplate (which + * EnterpriseSearchPageTemplateWrapper is a light wrapper for KibanaPageTemplate (which * is a light wrapper for EuiPageTemplate). It should contain only concerns shared * between both AS & WS, which should have their own AppSearchPageTemplate & * WorkplaceSearchPageTemplate sitting on top of this template (:nesting_dolls:), @@ -46,7 +46,7 @@ export type PageTemplateProps = KibanaPageTemplateProps & { pageViewTelemetry?: string; }; -export const EnterpriseSearchPageTemplate: React.FC = ({ +export const EnterpriseSearchPageTemplateWrapper: React.FC = ({ children, className, hideFlashMessages, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index b5309d8fedc1bf..f262579f56c56e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -8,6 +8,7 @@ import { groups } from './groups.mock'; import { IndexingRule } from '../types'; +import { SourceConfigData } from '../views/content_sources/components/add_source/add_source_logic'; import { staticSourceData } from '../views/content_sources/source_data'; import { mergeServerAndStaticData } from '../views/content_sources/sources_logic'; @@ -339,23 +340,23 @@ export const mergedConfiguredSources = mergeServerAndStaticData( contentSources ); -export const sourceConfigData = { +export const sourceConfigData: SourceConfigData = { serviceType: 'confluence_cloud', name: 'Confluence', configured: true, needsPermissions: true, accountContextOnly: false, - supportedByLicense: true, privateSourcesEnabled: false, categories: ['wiki', 'atlassian', 'intranet'], configuredFields: { - isOauth1: false, clientId: 'CyztADsSECRETCSAUCEh1a', clientSecret: 'GSjJxqSECRETCSAUCEksHk', baseUrl: 'https://mine.atlassian.net', privateKey: '-----BEGIN PRIVATE KEY-----\nkeykeykeykey==\n-----END PRIVATE KEY-----\n', publicKey: '-----BEGIN PUBLIC KEY-----\nkeykeykeykey\n-----END PUBLIC KEY-----\n', consumerKey: 'elastic_enterprise_search_123', + apiKey: 'asdf1234', + url: 'https://www.elastic.co', }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/page_template.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/page_template.test.tsx index 622fddc449ca7d..57611e1bacdc16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/page_template.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/page_template.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { SetWorkplaceSearchChrome } from '../../../shared/kibana_chrome'; -import { EnterpriseSearchPageTemplate } from '../../../shared/layout'; +import { EnterpriseSearchPageTemplateWrapper } from '../../../shared/layout'; import { SendWorkplaceSearchTelemetry } from '../../../shared/telemetry'; import { WorkplaceSearchPageTemplate } from './page_template'; @@ -27,7 +27,7 @@ describe('WorkplaceSearchPageTemplate', () => { ); - expect(wrapper.type()).toEqual(EnterpriseSearchPageTemplate); + expect(wrapper.type()).toEqual(EnterpriseSearchPageTemplateWrapper); expect(wrapper.prop('solutionNav')).toEqual({ name: 'Workplace Search', items: [] }); expect(wrapper.find('.hello').text()).toEqual('world'); }); @@ -35,7 +35,9 @@ describe('WorkplaceSearchPageTemplate', () => { describe('page chrome', () => { it('takes a breadcrumb array & renders a product-specific page chrome', () => { const wrapper = shallow(); - const setPageChrome = wrapper.find(EnterpriseSearchPageTemplate).prop('setPageChrome') as any; + const setPageChrome = wrapper + .find(EnterpriseSearchPageTemplateWrapper) + .prop('setPageChrome') as any; expect(setPageChrome.type).toEqual(SetWorkplaceSearchChrome); expect(setPageChrome.props.trail).toEqual(['Some page']); @@ -54,13 +56,15 @@ describe('WorkplaceSearchPageTemplate', () => { describe('props', () => { it('allows overriding the restrictWidth default', () => { const wrapper = shallow(); - expect(wrapper.find(EnterpriseSearchPageTemplate).prop('restrictWidth')).toEqual(true); + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('restrictWidth')).toEqual(true); wrapper.setProps({ restrictWidth: false }); - expect(wrapper.find(EnterpriseSearchPageTemplate).prop('restrictWidth')).toEqual(false); + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('restrictWidth')).toEqual( + false + ); }); - it('passes down any ...pageTemplateProps that EnterpriseSearchPageTemplate accepts', () => { + it('passes down any ...pageTemplateProps that EnterpriseSearchPageTemplateWrapper accepts', () => { const wrapper = shallow( { /> ); - expect(wrapper.find(EnterpriseSearchPageTemplate).prop('pageHeader')!.pageTitle).toEqual( - 'hello world' - ); - expect(wrapper.find(EnterpriseSearchPageTemplate).prop('isLoading')).toEqual(false); - expect(wrapper.find(EnterpriseSearchPageTemplate).prop('emptyState')).toEqual(
); + expect( + wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('pageHeader')!.pageTitle + ).toEqual('hello world'); + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('isLoading')).toEqual(false); + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('emptyState')).toEqual(
); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/page_template.tsx index 4a6e0d9c6e2ddc..f2a522b1b1d67c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/page_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/page_template.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetWorkplaceSearchChrome } from '../../../shared/kibana_chrome'; -import { EnterpriseSearchPageTemplate, PageTemplateProps } from '../../../shared/layout'; +import { EnterpriseSearchPageTemplateWrapper, PageTemplateProps } from '../../../shared/layout'; import { SendWorkplaceSearchTelemetry } from '../../../shared/telemetry'; import { useWorkplaceSearchNav } from './nav'; @@ -21,7 +21,7 @@ export const WorkplaceSearchPageTemplate: React.FC = ({ ...pageTemplateProps }) => { return ( - = ({ )} {children} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts index fdccd536c3c6d6..5b893250235f79 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts @@ -19,6 +19,7 @@ import oneDrive from './onedrive.svg'; import salesforce from './salesforce.svg'; import serviceNow from './servicenow.svg'; import sharePoint from './sharepoint.svg'; +import sharePointServer from './sharepoint_server.svg'; import slack from './slack.svg'; import zendesk from './zendesk.svg'; @@ -29,6 +30,8 @@ export const images = { confluenceServer: confluence, custom, dropbox, + // TODO: For now external sources are all SharePoint. When this is no longer the case, this needs to be dynamic. + external: sharePoint, github, githubEnterpriseServer: github, githubViaApp: github, @@ -44,6 +47,7 @@ export const images = { salesforceSandbox: salesforce, serviceNow, sharePoint, + sharePointServer, slack, zendesk, } as { [key: string]: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/sharepoint_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/sharepoint_server.svg new file mode 100644 index 00000000000000..aebfd7a8e49c0f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/sharepoint_server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 45104984657938..e83430504b3897 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -192,6 +192,10 @@ export const SOURCE_NAMES = { 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePoint', { defaultMessage: 'SharePoint Online' } ), + SHAREPOINT_SERVER: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePointServer', + { defaultMessage: 'SharePoint Server' } + ), SLACK: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.slack', { defaultMessage: 'Slack', }), @@ -357,6 +361,7 @@ export const GITHUB_VIA_APP_SERVICE_TYPE = 'github_via_app'; export const GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE = 'github_enterprise_server_via_app'; export const CUSTOM_SERVICE_TYPE = 'custom'; +export const EXTERNAL_SERVICE_TYPE = 'external'; export const WORKPLACE_SEARCH_URL_PREFIX = '/app/enterprise_search/workplace_search'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 4857fa2a158a0b..cbcd1d885b120e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -7,11 +7,6 @@ import { generatePath } from 'react-router-dom'; -import { - GITHUB_VIA_APP_SERVICE_TYPE, - GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE, -} from './constants'; - export const SETUP_GUIDE_PATH = '/setup_guide'; export const NOT_FOUND_PATH = '/404'; @@ -40,25 +35,7 @@ export const PRIVATE_SOURCES_PATH = `${PERSONAL_PATH}${SOURCES_PATH}`; export const SOURCE_ADDED_PATH = `${SOURCES_PATH}/added`; export const ADD_SOURCE_PATH = `${SOURCES_PATH}/add`; -export const ADD_BOX_PATH = `${SOURCES_PATH}/add/box`; -export const ADD_CONFLUENCE_PATH = `${SOURCES_PATH}/add/confluence_cloud`; -export const ADD_CONFLUENCE_SERVER_PATH = `${SOURCES_PATH}/add/confluence_server`; -export const ADD_DROPBOX_PATH = `${SOURCES_PATH}/add/dropbox`; -export const ADD_GITHUB_ENTERPRISE_PATH = `${SOURCES_PATH}/add/github_enterprise_server`; -export const ADD_GITHUB_PATH = `${SOURCES_PATH}/add/github`; -export const ADD_GITHUB_VIA_APP_PATH = `${SOURCES_PATH}/add/${GITHUB_VIA_APP_SERVICE_TYPE}`; -export const ADD_GITHUB_ENTERPRISE_SERVER_VIA_APP_PATH = `${SOURCES_PATH}/add/${GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE}`; -export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; -export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google_drive`; -export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira_cloud`; -export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira_server`; -export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/one_drive`; -export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`; -export const ADD_SALESFORCE_SANDBOX_PATH = `${SOURCES_PATH}/add/salesforce_sandbox`; -export const ADD_SERVICENOW_PATH = `${SOURCES_PATH}/add/servicenow`; -export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/share_point`; -export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`; -export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`; +export const ADD_EXTERNAL_PATH = `${SOURCES_PATH}/add/external`; export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`; export const PERSONAL_SETTINGS_PATH = `${PERSONAL_PATH}/settings`; @@ -83,24 +60,6 @@ export const ORG_SETTINGS_PATH = '/settings'; export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`; export const ORG_SETTINGS_CONNECTORS_PATH = `${ORG_SETTINGS_PATH}/connectors`; export const ORG_SETTINGS_OAUTH_APPLICATION_PATH = `${ORG_SETTINGS_PATH}/oauth`; -export const EDIT_BOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/box/edit`; -export const EDIT_CONFLUENCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence_cloud/edit`; -export const EDIT_CONFLUENCE_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence_server/edit`; -export const EDIT_DROPBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/dropbox/edit`; -export const EDIT_GITHUB_ENTERPRISE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github_enterprise_server/edit`; -export const EDIT_GITHUB_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github/edit`; -export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`; -export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google_drive/edit`; -export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_cloud/edit`; -export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_server/edit`; -export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/one_drive/edit`; -export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`; -export const EDIT_SALESFORCE_SANDBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce_sandbox/edit`; -export const EDIT_SERVICENOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/servicenow/edit`; -export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/share_point/edit`; -export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; -export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; -export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; export const getContentSourcePath = ( path: string, @@ -118,3 +77,6 @@ export const getReindexJobRoute = ( isOrganization: boolean ) => getSourcesPath(generatePath(REINDEX_JOB_PATH, { sourceId, activeReindexJobId }), isOrganization); +export const getAddPath = (serviceType: string): string => `${SOURCES_PATH}/add/${serviceType}`; +export const getEditPath = (serviceType: string): string => + `${ORG_SETTINGS_CONNECTORS_PATH}/${serviceType}/edit`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index b01700b8bce340..b3bfebcd6b2950 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -66,23 +66,27 @@ export interface Configuration { needsConfiguration?: boolean; hasOauthRedirect: boolean; baseUrlTitle?: string; - helpText: string; + helpText?: string; documentationUrl: string; applicationPortalUrl?: string; applicationLinkTitle?: string; + githubRepository?: string; } export interface SourceDataItem { name: string; + iconName: string; + categories?: string[]; serviceType: string; configuration: Configuration; configured?: boolean; connected?: boolean; features?: Features; objTypes?: string[]; - addPath: string; - editPath?: string; // undefined for GitHub apps, as they are configured on a source level, and don't use a connector where you can edit the configuration accountContextOnly: boolean; + internalConnectorAvailable?: boolean; + externalConnectorAvailable?: boolean; + customConnectorAvailable?: boolean; } export interface ContentSource { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/has_multiple_connector_options.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/has_multiple_connector_options.ts new file mode 100644 index 00000000000000..fbfda1ddf8d5ef --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/has_multiple_connector_options.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 { SourceDataItem } from '../types'; + +export const hasMultipleConnectorOptions = ({ + internalConnectorAvailable, + externalConnectorAvailable, + customConnectorAvailable, +}: SourceDataItem) => + [externalConnectorAvailable, internalConnectorAvailable, customConnectorAvailable].filter( + (available) => !!available + ).length > 1; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts index 92f27500d7262a..86d3e4f844bbd3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts @@ -11,3 +11,5 @@ export { mimeType } from './mime_types'; export { readUploadedFileAsBase64 } from './read_uploaded_file_as_base64'; export { readUploadedFileAsText } from './read_uploaded_file_as_text'; export { handlePrivateKeyUpload } from './handle_private_key_upload'; +export { hasMultipleConnectorOptions } from './has_multiple_connector_options'; +export { isNotNullish } from './is_not_nullish'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/is_not_nullish.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/is_not_nullish.ts new file mode 100644 index 00000000000000..d492dad5d52c2c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/is_not_nullish.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function isNotNullish(value: T | null | undefined): value is T { + return value !== null && value !== undefined; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.test.tsx new file mode 100644 index 00000000000000..b13cc6583cf2ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues } from '../../../../../__mocks__/kea_logic'; +import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; +import { staticSourceData } from '../../source_data'; + +import { AddCustomSource } from './add_custom_source'; +import { AddCustomSourceSteps } from './add_custom_source_logic'; +import { ConfigureCustom } from './configure_custom'; +import { SaveCustom } from './save_custom'; + +describe('AddCustomSource', () => { + const props = { + sourceData: staticSourceData[0], + initialValues: undefined, + }; + + const values = { + sourceConfigData, + isOrganization: true, + }; + + beforeEach(() => { + setMockValues({ ...values }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(WorkplaceSearchPageTemplate)).toHaveLength(1); + }); + + it('should show correct layout for personal dashboard', () => { + setMockValues({ isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.find(WorkplaceSearchPageTemplate)).toHaveLength(0); + expect(wrapper.find(PersonalDashboardLayout)).toHaveLength(1); + }); + + it('should show Configure Custom for custom configuration step', () => { + setMockValues({ currentStep: AddCustomSourceSteps.ConfigureCustomStep }); + const wrapper = shallow(); + + expect(wrapper.find(ConfigureCustom)).toHaveLength(1); + }); + + it('should show Save Custom for save custom step', () => { + setMockValues({ currentStep: AddCustomSourceSteps.SaveCustomStep }); + const wrapper = shallow(); + + expect(wrapper.find(SaveCustom)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.tsx new file mode 100644 index 00000000000000..6f7dc2bcdb342e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { AppLogic } from '../../../../app_logic'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; +import { NAV } from '../../../../constants'; + +import { SourceDataItem } from '../../../../types'; + +import { AddCustomSourceLogic, AddCustomSourceSteps } from './add_custom_source_logic'; +import { ConfigureCustom } from './configure_custom'; +import { SaveCustom } from './save_custom'; + +import './add_source.scss'; + +interface Props { + sourceData: SourceDataItem; + initialValue?: string; +} +export const AddCustomSource: React.FC = ({ sourceData, initialValue = '' }) => { + const addCustomSourceLogic = AddCustomSourceLogic({ sourceData, initialValue }); + const { currentStep } = useValues(addCustomSourceLogic); + const { isOrganization } = useValues(AppLogic); + + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + return ( + + {currentStep === AddCustomSourceSteps.ConfigureCustomStep && } + {currentStep === AddCustomSourceSteps.SaveCustomStep && } + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts new file mode 100644 index 00000000000000..93609679858765 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockFlashMessageHelpers, + mockHttpValues, +} from '../../../../../__mocks__/kea_logic'; +import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; + +import { i18n } from '@kbn/i18n'; +import { nextTick } from '@kbn/test-jest-helpers'; + +import { docLinks } from '../../../../../shared/doc_links'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; + +jest.mock('../../../../app_logic', () => ({ + AppLogic: { values: { isOrganization: true } }, +})); +import { AppLogic } from '../../../../app_logic'; + +import { SOURCE_NAMES } from '../../../../constants'; +import { CustomSource, SourceDataItem } from '../../../../types'; + +import { AddCustomSourceLogic, AddCustomSourceSteps } from './add_custom_source_logic'; + +const CUSTOM_SOURCE_DATA_ITEM: SourceDataItem = { + name: SOURCE_NAMES.CUSTOM, + iconName: SOURCE_NAMES.CUSTOM, + serviceType: 'custom', + configuration: { + isPublicKey: false, + hasOauthRedirect: false, + needsBaseUrl: false, + helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', { + defaultMessage: + 'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.', + }), + documentationUrl: docLinks.workplaceSearchCustomSources, + applicationPortalUrl: '', + }, + accountContextOnly: false, +}; + +const DEFAULT_VALUES = { + currentStep: AddCustomSourceSteps.ConfigureCustomStep, + buttonLoading: false, + customSourceNameValue: '', + newCustomSource: {} as CustomSource, + sourceData: CUSTOM_SOURCE_DATA_ITEM, +}; + +const MOCK_PROPS = { initialValue: '', sourceData: CUSTOM_SOURCE_DATA_ITEM }; + +const MOCK_NAME = 'name'; + +describe('AddCustomSourceLogic', () => { + const { mount } = new LogicMounter(AddCustomSourceLogic); + const { http } = mockHttpValues; + const { clearFlashMessages } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + mount({}, MOCK_PROPS); + }); + + it('has expected default values', () => { + expect(AddCustomSourceLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setButtonNotLoading', () => { + it('turns off the button loading flag', () => { + AddCustomSourceLogic.actions.setButtonNotLoading(); + + expect(AddCustomSourceLogic.values).toEqual({ + ...DEFAULT_VALUES, + buttonLoading: false, + }); + }); + }); + + describe('setCustomSourceNameValue', () => { + it('saves the name', () => { + AddCustomSourceLogic.actions.setCustomSourceNameValue('name'); + + expect(AddCustomSourceLogic.values).toEqual({ + ...DEFAULT_VALUES, + customSourceNameValue: 'name', + }); + }); + }); + + describe('setNewCustomSource', () => { + it('saves the custom source', () => { + const newCustomSource = { + accessToken: 'foo', + key: 'bar', + name: 'source', + id: '123key', + }; + + AddCustomSourceLogic.actions.setNewCustomSource(newCustomSource); + + expect(AddCustomSourceLogic.values).toEqual({ + ...DEFAULT_VALUES, + newCustomSource, + currentStep: AddCustomSourceSteps.SaveCustomStep, + }); + }); + }); + }); + + describe('listeners', () => { + beforeEach(() => { + mount( + { + customSourceNameValue: MOCK_NAME, + }, + MOCK_PROPS + ); + }); + + describe('organization context', () => { + describe('createContentSource', () => { + it('calls API and sets values', async () => { + const setButtonNotLoadingSpy = jest.spyOn( + AddCustomSourceLogic.actions, + 'setButtonNotLoading' + ); + const setNewCustomSourceSpy = jest.spyOn( + AddCustomSourceLogic.actions, + 'setNewCustomSource' + ); + http.post.mockReturnValue(Promise.resolve({ sourceConfigData })); + + AddCustomSourceLogic.actions.createContentSource(); + + expect(clearFlashMessages).toHaveBeenCalled(); + expect(AddCustomSourceLogic.values.buttonLoading).toEqual(true); + expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/org/create_source', { + body: JSON.stringify({ service_type: 'custom', name: MOCK_NAME }), + }); + await nextTick(); + expect(setNewCustomSourceSpy).toHaveBeenCalledWith({ sourceConfigData }); + expect(setButtonNotLoadingSpy).toHaveBeenCalled(); + }); + + itShowsServerErrorAsFlashMessage(http.post, () => { + AddCustomSourceLogic.actions.createContentSource(); + }); + }); + }); + + describe('account context routes', () => { + beforeEach(() => { + AppLogic.values.isOrganization = false; + }); + + describe('createContentSource', () => { + it('sends relevant fields to the API', () => { + AddCustomSourceLogic.actions.createContentSource(); + + expect(http.post).toHaveBeenCalledWith( + '/internal/workplace_search/account/create_source', + { + body: JSON.stringify({ service_type: 'custom', name: MOCK_NAME }), + } + ); + }); + + itShowsServerErrorAsFlashMessage(http.post, () => { + AddCustomSourceLogic.actions.createContentSource(); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts new file mode 100644 index 00000000000000..5bf86f6df41c7a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { flashAPIErrors, clearFlashMessages } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; +import { AppLogic } from '../../../../app_logic'; +import { CustomSource, SourceDataItem } from '../../../../types'; + +export interface AddCustomSourceProps { + sourceData: SourceDataItem; + initialValue: string; +} + +export enum AddCustomSourceSteps { + ConfigureCustomStep = 'Configure Custom', + SaveCustomStep = 'Save Custom', +} + +export interface AddCustomSourceActions { + createContentSource(): void; + setButtonNotLoading(): void; + setCustomSourceNameValue(customSourceNameValue: string): string; + setNewCustomSource(data: CustomSource): CustomSource; +} + +interface AddCustomSourceValues { + buttonLoading: boolean; + currentStep: AddCustomSourceSteps; + customSourceNameValue: string; + newCustomSource: CustomSource; + sourceData: SourceDataItem; +} + +/** + * Workplace Search needs to know the host for the redirect. As of yet, we do not + * have access to this in Kibana. We parse it from the browser and pass it as a param. + */ + +export const AddCustomSourceLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'workplace_search', 'add_custom_source_logic'], + actions: { + createContentSource: true, + setButtonNotLoading: true, + setCustomSourceNameValue: (customSourceNameValue) => customSourceNameValue, + setNewCustomSource: (data) => data, + }, + reducers: ({ props }) => ({ + buttonLoading: [ + false, + { + setButtonNotLoading: () => false, + createContentSource: () => true, + }, + ], + currentStep: [ + AddCustomSourceSteps.ConfigureCustomStep, + { + setNewCustomSource: () => AddCustomSourceSteps.SaveCustomStep, + }, + ], + customSourceNameValue: [ + props.initialValue, + { + setCustomSourceNameValue: (_, customSourceNameValue) => customSourceNameValue, + }, + ], + newCustomSource: [ + {} as CustomSource, + { + setNewCustomSource: (_, newCustomSource) => newCustomSource, + }, + ], + sourceData: [props.sourceData], + }), + listeners: ({ actions, values }) => ({ + createContentSource: async () => { + clearFlashMessages(); + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? '/internal/workplace_search/org/create_source' + : '/internal/workplace_search/account/create_source'; + + const { customSourceNameValue } = values; + + const params = { + service_type: 'custom', + name: customSourceNameValue, + }; + + try { + const response = await HttpLogic.values.http.post(route, { + body: JSON.stringify({ ...params }), + }); + actions.setNewCustomSource(response); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.setButtonNotLoading(); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index 0501509b3a8ef7..4598ca337f4e2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -22,16 +22,16 @@ import { PersonalDashboardLayout, } from '../../../../components/layout'; +import { staticSourceData } from '../../source_data'; + import { AddSource } from './add_source'; import { AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; import { ConfigurationIntro } from './configuration_intro'; -import { ConfigureCustom } from './configure_custom'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; import { Reauthenticate } from './reauthenticate'; import { SaveConfig } from './save_config'; -import { SaveCustom } from './save_custom'; describe('AddSourceList', () => { const { navigateToUrl } = mockKibanaValues; @@ -65,7 +65,7 @@ describe('AddSourceList', () => { }); it('renders default state', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.find(ConfigurationIntro).prop('advanceStep')(); expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); @@ -74,14 +74,14 @@ describe('AddSourceList', () => { describe('layout', () => { it('renders the default workplace search layout when on an organization view', () => { setMockValues({ ...mockValues, isOrganization: true }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); }); it('renders the personal dashboard layout when not in an organization', () => { setMockValues({ ...mockValues, isOrganization: false }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.type()).toEqual(PersonalDashboardLayout); }); @@ -89,7 +89,7 @@ describe('AddSourceList', () => { it('renders a breadcrumb fallback while data is loading', () => { setMockValues({ ...mockValues, dataLoading: true, sourceConfigData: {} }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.prop('pageChrome')).toEqual(['Sources', 'Add Source', '...']); }); @@ -99,7 +99,7 @@ describe('AddSourceList', () => { ...mockValues, addSourceCurrentStep: AddSourceSteps.ConfigCompletedStep, }); - const wrapper = shallow(); + const wrapper = shallow(); wrapper.find(ConfigCompleted).prop('advanceStep')(); expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect'); @@ -111,7 +111,7 @@ describe('AddSourceList', () => { ...mockValues, addSourceCurrentStep: AddSourceSteps.SaveConfigStep, }); - const wrapper = shallow(); + const wrapper = shallow(); const saveConfig = wrapper.find(SaveConfig); saveConfig.prop('advanceStep')(); saveConfig.prop('goBackStep')!(); @@ -126,51 +126,30 @@ describe('AddSourceList', () => { sourceConfigData, addSourceCurrentStep: AddSourceSteps.ConnectInstanceStep, }); - const wrapper = shallow(); + const wrapper = shallow(); wrapper.find(ConnectInstance).prop('onFormCreated')('foo'); expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect'); }); - it('renders Configure Custom step', () => { - setMockValues({ - ...mockValues, - addSourceCurrentStep: AddSourceSteps.ConfigureCustomStep, - }); - const wrapper = shallow(); - wrapper.find(ConfigureCustom).prop('advanceStep')(); - - expect(createContentSource).toHaveBeenCalled(); - }); - it('renders Configure Oauth step', () => { setMockValues({ ...mockValues, addSourceCurrentStep: AddSourceSteps.ConfigureOauthStep, }); - const wrapper = shallow(); + const wrapper = shallow(); wrapper.find(ConfigureOauth).prop('onFormCreated')('foo'); expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect'); }); - it('renders Save Custom step', () => { - setMockValues({ - ...mockValues, - addSourceCurrentStep: AddSourceSteps.SaveCustomStep, - }); - const wrapper = shallow(); - - expect(wrapper.find(SaveCustom)).toHaveLength(1); - }); - it('renders Reauthenticate step', () => { setMockValues({ ...mockValues, addSourceCurrentStep: AddSourceSteps.ReauthenticateStep, }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(Reauthenticate)).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index f575ddb19ebdc8..1e9be74224c5ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -18,49 +18,28 @@ import { WorkplaceSearchPageTemplate, PersonalDashboardLayout, } from '../../../../components/layout'; -import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants'; -import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; -import { SourceDataItem } from '../../../../types'; -import { staticSourceData } from '../../source_data'; +import { NAV } from '../../../../constants'; +import { SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes'; import { AddSourceHeader } from './add_source_header'; import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; import { ConfigurationIntro } from './configuration_intro'; -import { ConfigureCustom } from './configure_custom'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; import { Reauthenticate } from './reauthenticate'; import { SaveConfig } from './save_config'; -import { SaveCustom } from './save_custom'; import './add_source.scss'; export const AddSource: React.FC = (props) => { - const { - initializeAddSource, - setAddSourceStep, - saveSourceConfig, - createContentSource, - resetSourceState, - } = useActions(AddSourceLogic); - const { - addSourceCurrentStep, - sourceConfigData: { - name, - categories, - needsPermissions, - accountContextOnly, - privateSourcesEnabled, - }, - dataLoading, - newCustomSource, - } = useValues(AddSourceLogic); - - const { serviceType, configuration, features, objTypes, addPath } = staticSourceData[ - props.sourceIndex - ] as SourceDataItem; - + const { initializeAddSource, setAddSourceStep, saveSourceConfig, resetSourceState } = + useActions(AddSourceLogic); + const { addSourceCurrentStep, sourceConfigData, dataLoading } = useValues(AddSourceLogic); + const { name, categories, needsPermissions, accountContextOnly, privateSourcesEnabled } = + sourceConfigData; + const { serviceType, configuration, features, objTypes } = props.sourceData; + const addPath = getAddPath(serviceType); const { isOrganization } = useValues(AppLogic); useEffect(() => { @@ -85,9 +64,6 @@ export const AddSource: React.FC = (props) => { KibanaLogic.values.navigateToUrl(`${getSourcesPath(addPath, isOrganization)}/connect`); }; - const saveCustomSuccess = () => setAddSourceStep(AddSourceSteps.SaveCustomStep); - const goToSaveCustom = () => createContentSource(CUSTOM_SERVICE_TYPE, saveCustomSuccess); - const goToFormSourceCreated = () => { KibanaLogic.values.navigateToUrl(`${getSourcesPath(SOURCES_PATH, isOrganization)}`); flashSuccessToast(FORM_SOURCE_ADDED_SUCCESS_MESSAGE); @@ -131,24 +107,9 @@ export const AddSource: React.FC = (props) => { header={header} /> )} - {addSourceCurrentStep === AddSourceSteps.ConfigureCustomStep && ( - - )} {addSourceCurrentStep === AddSourceSteps.ConfigureOauthStep && ( )} - {addSourceCurrentStep === AddSourceSteps.SaveCustomStep && ( - - )} {addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && ( )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index 08e002ee432a9d..15160abb428095 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -27,7 +27,7 @@ import { } from '../../../../components/layout'; import { ContentSection } from '../../../../components/shared/content_section'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants'; +import { NAV, CUSTOM_SERVICE_TYPE, EXTERNAL_SERVICE_TYPE } from '../../../../constants'; import { SourceDataItem } from '../../../../types'; import { SourcesLogic } from '../../sources_logic'; @@ -90,12 +90,12 @@ export const AddSourceList: React.FC = () => { const filterConfiguredSources = (source: SourceDataItem) => filterSources(source, configuredSources); - const visibleAvailableSources = availableSources.filter( - filterAvailableSources - ) as SourceDataItem[]; - const visibleConfiguredSources = configuredSources.filter( - filterConfiguredSources - ) as SourceDataItem[]; + const visibleAvailableSources = availableSources + .filter(filterAvailableSources) + .filter((source) => source.serviceType !== EXTERNAL_SERVICE_TYPE); + // The API returns available external sources as a separate entry, but we don't want to present them as options to add + + const visibleConfiguredSources = configuredSources.filter(filterConfiguredSources); const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index 65ccd8d95256e8..80f8a2fc18218d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -15,6 +15,7 @@ import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test-jest-helpers'; +import { docLinks } from '../../../../../shared/doc_links'; import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; jest.mock('../../../../app_logic', () => ({ @@ -22,13 +23,9 @@ jest.mock('../../../../app_logic', () => ({ })); import { AppLogic } from '../../../../app_logic'; -import { - ADD_GITHUB_PATH, - SOURCES_PATH, - PRIVATE_SOURCES_PATH, - getSourcesPath, -} from '../../../../routes'; -import { CustomSource } from '../../../../types'; +import { SOURCE_NAMES, SOURCE_OBJ_TYPES } from '../../../../constants'; +import { SOURCES_PATH, PRIVATE_SOURCES_PATH, getSourcesPath } from '../../../../routes'; +import { FeatureIds } from '../../../../types'; import { PERSONAL_DASHBOARD_SOURCE_ERROR } from '../../constants'; import { SourcesLogic } from '../../sources_logic'; @@ -38,6 +35,8 @@ import { SourceConfigData, SourceConnectData, OrganizationsMap, + AddSourceValues, + AddSourceProps, } from './add_source_logic'; describe('AddSourceLogic', () => { @@ -46,13 +45,12 @@ describe('AddSourceLogic', () => { const { navigateToUrl } = mockKibanaValues; const { clearFlashMessages, flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers; - const DEFAULT_VALUES = { + const DEFAULT_VALUES: AddSourceValues = { addSourceCurrentStep: AddSourceSteps.ConfigIntroStep, - addSourceProps: {}, + addSourceProps: {} as AddSourceProps, dataLoading: true, sectionLoading: true, buttonLoading: false, - customSourceNameValue: '', clientIdValue: '', clientSecretValue: '', baseUrlValue: '', @@ -62,7 +60,6 @@ describe('AddSourceLogic', () => { indexPermissionsValue: false, sourceConfigData: {} as SourceConfigData, sourceConnectData: {} as SourceConnectData, - newCustomSource: {} as CustomSource, oauthConfigCompleted: false, currentServiceType: '', githubOrganizations: [], @@ -81,8 +78,34 @@ describe('AddSourceLogic', () => { serviceType: 'github', githubOrganizations: ['foo', 'bar'], }; - - const CUSTOM_SERVICE_TYPE_INDEX = 17; + const DEFAULT_SERVICE_TYPE = { + name: SOURCE_NAMES.BOX, + iconName: SOURCE_NAMES.BOX, + serviceType: 'box', + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: docLinks.workplaceSearchBox, + applicationPortalUrl: 'https://app.box.com/developers/console', + }, + objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.ALL_FILES], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + basicOrgContextExcludedFeatures: [FeatureIds.DocumentLevelPermissions], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }; beforeEach(() => { jest.clearAllMocks(); @@ -145,15 +168,6 @@ describe('AddSourceLogic', () => { }); }); - it('setCustomSourceNameValue', () => { - AddSourceLogic.actions.setCustomSourceNameValue('name'); - - expect(AddSourceLogic.values).toEqual({ - ...DEFAULT_VALUES, - customSourceNameValue: 'name', - }); - }); - it('setSourceLoginValue', () => { AddSourceLogic.actions.setSourceLoginValue('login'); @@ -190,22 +204,6 @@ describe('AddSourceLogic', () => { }); }); - it('setCustomSourceData', () => { - const newCustomSource = { - accessToken: 'foo', - key: 'bar', - name: 'source', - id: '123key', - }; - - AddSourceLogic.actions.setCustomSourceData(newCustomSource); - - expect(AddSourceLogic.values).toEqual({ - ...DEFAULT_VALUES, - newCustomSource, - }); - }); - it('setPreContentSourceConfigData', () => { AddSourceLogic.actions.setPreContentSourceConfigData(config); @@ -260,13 +258,14 @@ describe('AddSourceLogic', () => { }); it('handles fallback states', () => { - const { publicKey, privateKey, consumerKey } = sourceConfigData.configuredFields; - const sourceConfigDataMock = { + const { publicKey, privateKey, consumerKey, apiKey } = sourceConfigData.configuredFields; + const sourceConfigDataMock: SourceConfigData = { ...sourceConfigData, configuredFields: { publicKey, privateKey, consumerKey, + apiKey, }, }; AddSourceLogic.actions.setSourceConfigData(sourceConfigDataMock); @@ -284,7 +283,7 @@ describe('AddSourceLogic', () => { describe('listeners', () => { it('initializeAddSource', () => { - const addSourceProps = { sourceIndex: 1 }; + const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE }; const getSourceConfigDataSpy = jest.spyOn(AddSourceLogic.actions, 'getSourceConfigData'); const setAddSourcePropsSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceProps'); const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep'); @@ -293,21 +292,13 @@ describe('AddSourceLogic', () => { expect(setAddSourcePropsSpy).toHaveBeenCalledWith({ addSourceProps }); expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConfigIntroStep); - expect(getSourceConfigDataSpy).toHaveBeenCalledWith('confluence_cloud'); + expect(getSourceConfigDataSpy).toHaveBeenCalledWith('box'); }); describe('getFirstStep', () => { - it('sets custom as first step', () => { - const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep'); - const addSourceProps = { sourceIndex: CUSTOM_SERVICE_TYPE_INDEX }; - AddSourceLogic.actions.initializeAddSource(addSourceProps); - - expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConfigureCustomStep); - }); - it('sets connect as first step', () => { const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep'); - const addSourceProps = { sourceIndex: 1, connect: true }; + const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, connect: true }; AddSourceLogic.actions.initializeAddSource(addSourceProps); expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep); @@ -315,7 +306,7 @@ describe('AddSourceLogic', () => { it('sets configure as first step', () => { const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep'); - const addSourceProps = { sourceIndex: 1, configure: true }; + const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, configure: true }; AddSourceLogic.actions.initializeAddSource(addSourceProps); expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConfigureOauthStep); @@ -323,7 +314,7 @@ describe('AddSourceLogic', () => { it('sets reAuthenticate as first step', () => { const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep'); - const addSourceProps = { sourceIndex: 1, reAuthenticate: true }; + const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, reAuthenticate: true }; AddSourceLogic.actions.initializeAddSource(addSourceProps); expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ReauthenticateStep); @@ -401,7 +392,7 @@ describe('AddSourceLogic', () => { await nextTick(); expect(setPreContentSourceIdSpy).toHaveBeenCalledWith(preContentSourceId); - expect(navigateToUrl).toHaveBeenCalledWith(`${ADD_GITHUB_PATH}/configure${queryString}`); + expect(navigateToUrl).toHaveBeenCalledWith(`/sources/add/github/configure${queryString}`); }); describe('Github error edge case', () => { @@ -635,7 +626,6 @@ describe('AddSourceLogic', () => { const errorCallback = jest.fn(); const serviceType = 'zendesk'; - const name = 'name'; const login = 'login'; const password = 'password'; const indexPermissions = false; @@ -643,7 +633,6 @@ describe('AddSourceLogic', () => { let params: any; beforeEach(() => { - AddSourceLogic.actions.setCustomSourceNameValue(name); AddSourceLogic.actions.setSourceLoginValue(login); AddSourceLogic.actions.setSourcePasswordValue(password); AddSourceLogic.actions.setPreContentSourceConfigData(config); @@ -652,7 +641,6 @@ describe('AddSourceLogic', () => { params = { service_type: serviceType, - name, login, password, organizations: ['foo'], @@ -661,8 +649,7 @@ describe('AddSourceLogic', () => { it('calls API and sets values', async () => { const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading'); - const setCustomSourceDataSpy = jest.spyOn(AddSourceLogic.actions, 'setCustomSourceData'); - http.post.mockReturnValue(Promise.resolve({ sourceConfigData })); + http.post.mockReturnValue(Promise.resolve()); AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback); @@ -672,7 +659,6 @@ describe('AddSourceLogic', () => { body: JSON.stringify({ ...params }), }); await nextTick(); - expect(setCustomSourceDataSpy).toHaveBeenCalledWith({ sourceConfigData }); expect(successCallback).toHaveBeenCalled(); expect(setButtonNotLoadingSpy).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index 6dbac2dcd14526..db0c5b97372636 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -21,20 +21,14 @@ import { import { HttpLogic } from '../../../../../shared/http'; import { KibanaLogic } from '../../../../../shared/kibana'; import { AppLogic } from '../../../../app_logic'; -import { CUSTOM_SERVICE_TYPE, WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants'; -import { - SOURCES_PATH, - ADD_GITHUB_PATH, - PRIVATE_SOURCES_PATH, - getSourcesPath, -} from '../../../../routes'; -import { CustomSource } from '../../../../types'; +import { WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants'; +import { SOURCES_PATH, PRIVATE_SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes'; +import { SourceDataItem } from '../../../../types'; import { PERSONAL_DASHBOARD_SOURCE_ERROR } from '../../constants'; -import { staticSourceData } from '../../source_data'; import { SourcesLogic } from '../../sources_logic'; export interface AddSourceProps { - sourceIndex: number; + sourceData: SourceDataItem; connect?: boolean; configure?: boolean; reAuthenticate?: boolean; @@ -45,9 +39,7 @@ export enum AddSourceSteps { SaveConfigStep = 'Save Config', ConfigCompletedStep = 'Config Completed', ConnectInstanceStep = 'Connect Instance', - ConfigureCustomStep = 'Configure Custom', ConfigureOauthStep = 'Configure Oauth', - SaveCustomStep = 'Save Custom', ReauthenticateStep = 'Reauthenticate', } @@ -71,12 +63,10 @@ export interface AddSourceActions { setClientIdValue(clientIdValue: string): string; setClientSecretValue(clientSecretValue: string): string; setBaseUrlValue(baseUrlValue: string): string; - setCustomSourceNameValue(customSourceNameValue: string): string; setSourceLoginValue(loginValue: string): string; setSourcePasswordValue(passwordValue: string): string; setSourceSubdomainValue(subdomainValue: string): string; setSourceIndexPermissionsValue(indexPermissionsValue: boolean): boolean; - setCustomSourceData(data: CustomSource): CustomSource; setPreContentSourceConfigData(data: PreContentSourceResponse): PreContentSourceResponse; setPreContentSourceId(preContentSourceId: string): string; setSelectedGithubOrganizations(option: string): string; @@ -119,6 +109,8 @@ export interface SourceConfigData { baseUrl?: string; clientId?: string; clientSecret?: string; + url?: string; + apiKey?: string; }; accountContextOnly?: boolean; } @@ -132,13 +124,12 @@ export interface OrganizationsMap { [key: string]: string | boolean; } -interface AddSourceValues { +export interface AddSourceValues { addSourceProps: AddSourceProps; addSourceCurrentStep: AddSourceSteps; dataLoading: boolean; sectionLoading: boolean; buttonLoading: boolean; - customSourceNameValue: string; clientIdValue: string; clientSecretValue: string; baseUrlValue: string; @@ -148,7 +139,6 @@ interface AddSourceValues { indexPermissionsValue: boolean; sourceConfigData: SourceConfigData; sourceConnectData: SourceConnectData; - newCustomSource: CustomSource; currentServiceType: string; githubOrganizations: string[]; selectedGithubOrganizationsMap: OrganizationsMap; @@ -185,12 +175,10 @@ export const AddSourceLogic = kea clientIdValue, setClientSecretValue: (clientSecretValue: string) => clientSecretValue, setBaseUrlValue: (baseUrlValue: string) => baseUrlValue, - setCustomSourceNameValue: (customSourceNameValue: string) => customSourceNameValue, setSourceLoginValue: (loginValue: string) => loginValue, setSourcePasswordValue: (passwordValue: string) => passwordValue, setSourceSubdomainValue: (subdomainValue: string) => subdomainValue, setSourceIndexPermissionsValue: (indexPermissionsValue: boolean) => indexPermissionsValue, - setCustomSourceData: (data: CustomSource) => data, setPreContentSourceConfigData: (data: PreContentSourceResponse) => data, setPreContentSourceId: (preContentSourceId: string) => preContentSourceId, setSelectedGithubOrganizations: (option: string) => option, @@ -322,20 +310,6 @@ export const AddSourceLogic = kea false, }, ], - customSourceNameValue: [ - '', - { - setCustomSourceNameValue: (_, customSourceNameValue) => customSourceNameValue, - resetSourceState: () => '', - }, - ], - newCustomSource: [ - {} as CustomSource, - { - setCustomSourceData: (_, newCustomSource) => newCustomSource, - resetSourceState: () => ({} as CustomSource), - }, - ], currentServiceType: [ '', { @@ -383,7 +357,7 @@ export const AddSourceLogic = kea ({ initializeAddSource: ({ addSourceProps }) => { - const { serviceType } = staticSourceData[addSourceProps.sourceIndex]; + const { serviceType } = addSourceProps.sourceData; actions.setAddSourceProps({ addSourceProps }); actions.setAddSourceStep(getFirstStep(addSourceProps)); actions.getSourceConfigData(serviceType); @@ -540,7 +514,9 @@ export const AddSourceLogic = kea 0 ? githubOrganizations : undefined, @@ -580,10 +554,9 @@ export const AddSourceLogic = kea params[key] === undefined && delete params[key]); try { - const response = await HttpLogic.values.http.post(route, { + await HttpLogic.values.http.post(route, { body: JSON.stringify({ ...params }), }); - actions.setCustomSourceData(response); successCallback(); } catch (e) { flashAPIErrors(e); @@ -596,11 +569,7 @@ export const AddSourceLogic = kea { - const { sourceIndex, connect, configure, reAuthenticate } = props; - const { serviceType } = staticSourceData[sourceIndex]; - const isCustom = serviceType === CUSTOM_SERVICE_TYPE; - - if (isCustom) return AddSourceSteps.ConfigureCustomStep; + const { connect, configure, reAuthenticate } = props; if (connect) return AddSourceSteps.ConnectInstanceStep; if (configure) return AddSourceSteps.ConfigureOauthStep; if (reAuthenticate) return AddSourceSteps.ReauthenticateStep; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx index f168dfbea91ce8..fbcb8685f7ff9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx @@ -26,7 +26,7 @@ describe('AvailableSourcesList', () => { const wrapper = shallow(); expect(wrapper.find(EuiTitle)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="AvailableSourceListItem"]')).toHaveLength(11); + expect(wrapper.find('[data-test-subj="AvailableSourceListItem"]')).toHaveLength(20); expect(wrapper.find('[data-test-subj="CustomAPISourceLink"]')).toHaveLength(1); }); @@ -34,7 +34,7 @@ describe('AvailableSourcesList', () => { setMockValues({ hasPlatinumLicense: false }); const wrapper = shallow(); - expect(wrapper.find(EuiToolTip)).toHaveLength(1); + expect(wrapper.find(EuiToolTip)).toHaveLength(2); }); it('handles empty state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx index 13f0f41643e169..7dc9ad9ca0f60e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx @@ -24,9 +24,11 @@ import { i18n } from '@kbn/i18n'; import { LicensingLogic } from '../../../../../shared/licensing'; import { EuiButtonEmptyTo, EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { SourceIcon } from '../../../../components/shared/source_icon'; -import { ADD_CUSTOM_PATH, getSourcesPath } from '../../../../routes'; +import { ADD_CUSTOM_PATH, getAddPath, getSourcesPath } from '../../../../routes'; import { SourceDataItem } from '../../../../types'; +import { staticCustomSourceData } from '../../source_data'; + import { AVAILABLE_SOURCE_EMPTY_STATE, AVAILABLE_SOURCE_TITLE, @@ -41,7 +43,8 @@ interface AvailableSourcesListProps { export const AvailableSourcesList: React.FC = ({ sources }) => { const { hasPlatinumLicense } = useValues(LicensingLogic); - const getSourceCard = ({ name, serviceType, addPath, accountContextOnly }: SourceDataItem) => { + const getSourceCard = ({ name, serviceType, accountContextOnly }: SourceDataItem) => { + const addPath = getAddPath(serviceType); const disabled = !hasPlatinumLicense && accountContextOnly; const connectButton = () => { @@ -105,6 +108,15 @@ export const AvailableSourcesList: React.FC = ({ sour ))} + + + {getSourceCard(staticCustomSourceData)} + + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx new file mode 100644 index 00000000000000..bfb916847d865e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockKibanaValues, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiText, EuiButton } from '@elastic/eui'; + +import { + PersonalDashboardLayout, + WorkplaceSearchPageTemplate, +} from '../../../../components/layout'; +import { staticSourceData } from '../../source_data'; + +import { ConfigurationChoice } from './configuration_choice'; + +describe('ConfigurationChoice', () => { + const { navigateToUrl } = mockKibanaValues; + const props = { + sourceData: staticSourceData[0], + }; + const mockValues = { + isOrganization: true, + }; + + beforeEach(() => { + setMockValues(mockValues); + jest.clearAllMocks(); + }); + + describe('layout', () => { + it('renders the default workplace search layout when on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + }); + + it('renders the personal dashboard layout when not in an organization', () => { + setMockValues({ ...mockValues, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + }); + }); + + it('renders internal connector if available', () => { + const wrapper = shallow(); + + expect(wrapper.find('EuiPanel')).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(3); + expect(wrapper.find(EuiButton)).toHaveLength(1); + }); + it('should navigate to internal connector on internal connector click', () => { + const wrapper = shallow(); + const button = wrapper.find(EuiButton); + button.simulate('click'); + expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/internal/'); + }); + + it('renders external connector if available', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('EuiPanel')).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(3); + expect(wrapper.find(EuiButton)).toHaveLength(1); + }); + it('should navigate to external connector on external connector click', () => { + const wrapper = shallow( + + ); + const button = wrapper.find(EuiButton); + button.simulate('click'); + expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/external/'); + }); + + it('renders custom connector if available', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('EuiPanel')).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(3); + expect(wrapper.find(EuiButton)).toHaveLength(1); + }); + it('should navigate to custom connector on internal connector click', () => { + const wrapper = shallow( + + ); + const button = wrapper.find(EuiButton); + button.simulate('click'); + expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/custom/'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx new file mode 100644 index 00000000000000..46a8998c9dd10a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx @@ -0,0 +1,236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { KibanaLogic } from '../../../../../shared/kibana'; +import { AppLogic } from '../../../../app_logic'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; +import { NAV } from '../../../../constants'; +import { getAddPath, getSourcesPath } from '../../../../routes'; +import { SourceDataItem } from '../../../../types'; + +import { AddSourceHeader } from './add_source_header'; + +interface ConfigurationIntroProps { + sourceData: SourceDataItem; +} + +export const ConfigurationChoice: React.FC = ({ + sourceData: { + name, + serviceType, + externalConnectorAvailable, + internalConnectorAvailable, + customConnectorAvailable, + }, +}) => { + const { isOrganization } = useValues(AppLogic); + const goToInternal = () => + KibanaLogic.values.navigateToUrl( + `${getSourcesPath( + `${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`, + isOrganization + )}/` + ); + const goToExternal = () => + KibanaLogic.values.navigateToUrl( + `${getSourcesPath( + `${getSourcesPath(getAddPath(serviceType), isOrganization)}/external`, + isOrganization + )}/` + ); + const goToCustom = () => + KibanaLogic.values.navigateToUrl( + `${getSourcesPath( + `${getSourcesPath(getAddPath(serviceType), isOrganization)}/custom`, + isOrganization + )}/` + ); + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + return ( + + + + + {internalConnectorAvailable && ( + + + + + +

{name}

+
+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.internal.title', + { + defaultMessage: 'Default connector', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.internal.description', + { + defaultMessage: 'Use our out-of-the-box connector to get started quickly.', + } + )} + + + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.internal.button', + { + defaultMessage: 'Connect', + } + )} + + +
+
+
+ )} + {externalConnectorAvailable && ( + + + + + +

{name}

+
+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.external.title', + { + defaultMessage: 'Custom connector', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.external.description', + { + defaultMessage: + 'Set up a custom connector for more configurability and control.', + } + )} + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.external.button', + { + defaultMessage: 'Instructions', + } + )} + + + +
+
+
+ )} + {customConnectorAvailable && ( + + + + + +

{name}

+
+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.custom.title', + { + defaultMessage: 'Custom connector', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.custom.description', + { + defaultMessage: + 'Set up a custom connector for more configurability and control.', + } + )} + + +
+ + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.custom.button', + { + defaultMessage: 'Instructions', + } + )} + + + +
+
+ )} +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx index 6c0d87b7696ecd..645226c546f102 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx @@ -14,45 +14,45 @@ import { shallow } from 'enzyme'; import { EuiForm, EuiFieldText } from '@elastic/eui'; +import { staticSourceData } from '../../source_data'; + import { ConfigureCustom } from './configure_custom'; describe('ConfigureCustom', () => { - const advanceStep = jest.fn(); const setCustomSourceNameValue = jest.fn(); - - const props = { - header:

Header

, - helpText: 'I bet you could use a hand.', - advanceStep, - }; + const createContentSource = jest.fn(); beforeEach(() => { - setMockActions({ setCustomSourceNameValue }); - setMockValues({ customSourceNameValue: 'name', buttonLoading: false }); + setMockActions({ setCustomSourceNameValue, createContentSource }); + setMockValues({ + customSourceNameValue: 'name', + buttonLoading: false, + sourceData: staticSourceData[1], + }); }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiForm)).toHaveLength(1); }); it('handles input change', () => { - const wrapper = shallow(); - const TEXT = 'changed for the better'; + const wrapper = shallow(); + const text = 'changed for the better'; const input = wrapper.find(EuiFieldText); - input.simulate('change', { target: { value: TEXT } }); + input.simulate('change', { target: { value: text } }); - expect(setCustomSourceNameValue).toHaveBeenCalledWith(TEXT); + expect(setCustomSourceNameValue).toHaveBeenCalledWith(text); }); it('handles form submission', () => { - const wrapper = shallow(); + const wrapper = shallow(); const preventDefault = jest.fn(); wrapper.find('form').simulate('submit', { preventDefault }); expect(preventDefault).toHaveBeenCalled(); - expect(advanceStep).toHaveBeenCalled(); + expect(createContentSource).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx index e794323dc169e0..bf5a7fea21333d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx @@ -24,51 +24,64 @@ import { docLinks } from '../../../../../shared/doc_links'; import { SOURCE_NAME_LABEL } from '../../constants'; -import { AddSourceLogic } from './add_source_logic'; +import { AddCustomSourceLogic } from './add_custom_source_logic'; +import { AddSourceHeader } from './add_source_header'; import { CONFIG_CUSTOM_BUTTON, CONFIG_CUSTOM_LINK_TEXT } from './constants'; -interface ConfigureCustomProps { - header: React.ReactNode; - helpText: string; - advanceStep(): void; -} - -export const ConfigureCustom: React.FC = ({ - helpText, - advanceStep, - header, -}) => { - const { setCustomSourceNameValue } = useActions(AddSourceLogic); - const { customSourceNameValue, buttonLoading } = useValues(AddSourceLogic); +export const ConfigureCustom: React.FC = () => { + const { setCustomSourceNameValue, createContentSource } = useActions(AddCustomSourceLogic); + const { customSourceNameValue, buttonLoading, sourceData } = useValues(AddCustomSourceLogic); const handleFormSubmit = (e: FormEvent) => { e.preventDefault(); - advanceStep(); + createContentSource(); }; const handleNameChange = (e: ChangeEvent) => setCustomSourceNameValue(e.target.value); + const { + serviceType, + configuration: { documentationUrl, helpText }, + name, + categories = [], + } = sourceData; + return ( <> - {header} +

{helpText}

- - {CONFIG_CUSTOM_LINK_TEXT} - - ), - }} - /> + {serviceType === 'custom' ? ( + + {CONFIG_CUSTOM_LINK_TEXT} + + ), + }} + /> + ) : ( + + {CONFIG_CUSTOM_LINK_TEXT} + + ), + name, + }} + /> + )}

@@ -90,7 +103,17 @@ export const ConfigureCustom: React.FC = ({ isLoading={buttonLoading} data-test-subj="CreateCustomButton" > - {CONFIG_CUSTOM_BUTTON} + {serviceType === 'custom' ? ( + CONFIG_CUSTOM_BUTTON + ) : ( + + )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx index a1169cd582cba7..a13558469cc085 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx @@ -22,9 +22,9 @@ describe('ConfiguredSourcesList', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(5); - expect(wrapper.find('[data-test-subj="AccountOnlyTooltip"]')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(6); + expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(16); + expect(wrapper.find('[data-test-subj="AccountOnlyTooltip"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(19); }); it('handles empty state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx index ac465c43643a44..d4bb62901cdb60 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx @@ -22,8 +22,9 @@ import { import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers'; import { SourceIcon } from '../../../../components/shared/source_icon'; -import { getSourcesPath } from '../../../../routes'; +import { getAddPath, getSourcesPath } from '../../../../routes'; import { SourceDataItem } from '../../../../types'; +import { hasMultipleConnectorOptions } from '../../../../utils'; import { CONFIGURED_SOURCES_LIST_UNCONNECTED_TOOLTIP, @@ -68,54 +69,62 @@ export const ConfiguredSourcesList: React.FC = ({ const visibleSources = ( - {sources.map(({ name, serviceType, addPath, connected, accountContextOnly }, i) => ( - - - - - - - - - - -

- {name} - {!connected && !accountContextOnly && isOrganization && unConnectedTooltip} - {accountContextOnly && isOrganization && accountOnlyTooltip} -

-
-
-
-
- - {((!isOrganization || (isOrganization && !accountContextOnly)) && ( - { + const { connected, accountContextOnly, name, serviceType } = sourceData; + return ( + + + + + - {CONFIGURED_SOURCES_CONNECT_BUTTON} - - )) || ( - - {ADD_SOURCE_ORG_SOURCES_TITLE} - - )} - -
-
-
- ))} + + + + + +

+ {name} + {!connected && + !accountContextOnly && + isOrganization && + unConnectedTooltip} + {accountContextOnly && isOrganization && accountOnlyTooltip} +

+
+
+ + + + {((!isOrganization || (isOrganization && !accountContextOnly)) && ( + + {CONFIGURED_SOURCES_CONNECT_BUTTON} + + )) || ( + + {ADD_SOURCE_ORG_SOURCES_TITLE} + + )} + + + + + ); + })}
); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx index c967b20e0450dc..0ee80019ea720e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx @@ -43,7 +43,7 @@ describe('ConnectInstance', () => { const credentialsSourceData = staticSourceData[13]; const oauthSourceData = staticSourceData[0]; - const subdomainSourceData = staticSourceData[16]; + const subdomainSourceData = staticSourceData[18]; const props = { ...credentialsSourceData, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_config.test.tsx new file mode 100644 index 00000000000000..6288a5fc791294 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_config.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; +import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiSteps } from '@elastic/eui'; + +import { staticSourceData } from '../../source_data'; + +import { ExternalConnectorConfig } from './external_connector_config'; + +describe('ExternalConnectorConfig', () => { + const goBack = jest.fn(); + const onDeleteConfig = jest.fn(); + const setExternalConnectorApiKey = jest.fn(); + const setExternalConnectorUrl = jest.fn(); + const saveExternalConnectorConfig = jest.fn(); + const fetchExternalSource = jest.fn(); + + const props = { + sourceData: staticSourceData[0], + goBack, + onDeleteConfig, + }; + + const values = { + sourceConfigData, + buttonLoading: false, + clientIdValue: 'foo', + clientSecretValue: 'bar', + baseUrlValue: 'http://foo.baz', + hasPlatinumLicense: true, + }; + + beforeEach(() => { + setMockActions({ + setExternalConnectorApiKey, + setExternalConnectorUrl, + saveExternalConnectorConfig, + fetchExternalSource, + }); + setMockValues({ ...values }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiSteps)).toHaveLength(1); + }); + + it('handles form submission', () => { + const wrapper = shallow(); + + const preventDefault = jest.fn(); + wrapper.find('form').simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(saveExternalConnectorConfig).toHaveBeenCalled(); + }); + + describe('external connector configuration', () => { + it('handles url change', () => { + const wrapper = shallow(); + const steps = wrapper.find(EuiSteps); + const input = steps.dive().find('[name="external-connector-url"]'); + input.simulate('change', { target: { value: 'url' } }); + + expect(setExternalConnectorUrl).toHaveBeenCalledWith('url'); + }); + + it('handles Client secret change', () => { + const wrapper = shallow(); + const steps = wrapper.find(EuiSteps); + const input = steps.dive().find('[name="external-connector-api-key"]'); + input.simulate('change', { target: { value: 'api-key' } }); + + expect(setExternalConnectorApiKey).toHaveBeenCalledWith('api-key'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_config.tsx new file mode 100644 index 00000000000000..1f0528f492b9d0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_config.tsx @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { FormEvent, useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiSteps, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { AppLogic } from '../../../../app_logic'; +import { + PersonalDashboardLayout, + WorkplaceSearchPageTemplate, +} from '../../../../components/layout'; +import { NAV, REMOVE_BUTTON } from '../../../../constants'; +import { SourceDataItem } from '../../../../types'; + +import { AddSourceHeader } from './add_source_header'; +import { OAUTH_SAVE_CONFIG_BUTTON, OAUTH_BACK_BUTTON } from './constants'; +import { ExternalConnectorLogic } from './external_connector_logic'; + +interface SaveConfigProps { + sourceData: SourceDataItem; + goBack?: () => void; + onDeleteConfig?: () => void; +} + +export const ExternalConnectorConfig: React.FC = ({ goBack, onDeleteConfig }) => { + const serviceType = 'external'; + const { + fetchExternalSource, + setExternalConnectorApiKey, + setExternalConnectorUrl, + saveExternalConnectorConfig, + } = useActions(ExternalConnectorLogic); + + const { buttonLoading, externalConnectorUrl, externalConnectorApiKey, sourceConfigData } = + useValues(ExternalConnectorLogic); + + useEffect(() => { + fetchExternalSource(); + }, []); + + const handleFormSubmission = (e: FormEvent) => { + e.preventDefault(); + saveExternalConnectorConfig({ url: externalConnectorUrl, apiKey: externalConnectorApiKey }); + }; + + const { name, categories } = sourceConfigData; + const { isOrganization } = useValues(AppLogic); + + const saveButton = ( + + {OAUTH_SAVE_CONFIG_BUTTON} + + ); + + const deleteButton = ( + + {REMOVE_BUTTON} + + ); + + const backButton = {OAUTH_BACK_BUTTON}; + + const formActions = ( + + + {saveButton} + + {goBack && backButton} + {onDeleteConfig && deleteButton} + + + + ); + + const connectorForm = ( + + {/* TODO: get a docs link in here for the external connector + */} + + + + setExternalConnectorUrl(e.target.value)} + name="external-connector-url" + /> + + + setExternalConnectorApiKey(e.target.value)} + name="external-connector-api-key" + /> + + + {formActions} + + + ); + + const configSteps = [ + { + title: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSource.externalConnectorConfig.stepTitle', + { + defaultMessage: 'Provide the appropriate configuration information', + } + ), + children: connectorForm, + }, + ]; + + const header = ; + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + return ( + + {header} + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_logic.test.ts new file mode 100644 index 00000000000000..22a36deeeccd5a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_logic.test.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, mockHttpValues, mockKibanaValues } from '../../../../../__mocks__/kea_logic'; +import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; + +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; + +jest.mock('../../../../app_logic', () => ({ + AppLogic: { values: { isOrganization: true } }, +})); + +import { ExternalConnectorLogic, ExternalConnectorValues } from './external_connector_logic'; + +describe('ExternalConnectorLogic', () => { + const { mount } = new LogicMounter(ExternalConnectorLogic); + const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; + + const DEFAULT_VALUES: ExternalConnectorValues = { + dataLoading: true, + buttonLoading: false, + externalConnectorUrl: '', + externalConnectorApiKey: '', + sourceConfigData: { + name: '', + categories: [], + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(ExternalConnectorLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('fetchExternalSourceSuccess', () => { + beforeEach(() => { + ExternalConnectorLogic.actions.fetchExternalSourceSuccess(sourceConfigData); + }); + + it('turns off the data loading flag', () => { + expect(ExternalConnectorLogic.values.dataLoading).toEqual(false); + }); + + it('saves the external url', () => { + expect(ExternalConnectorLogic.values.externalConnectorUrl).toEqual( + sourceConfigData.configuredFields.url + ); + }); + + it('saves the source config', () => { + expect(ExternalConnectorLogic.values.sourceConfigData).toEqual(sourceConfigData); + }); + + it('sets undefined url to empty string', () => { + ExternalConnectorLogic.actions.fetchExternalSourceSuccess({ + ...sourceConfigData, + configuredFields: { ...sourceConfigData.configuredFields, url: undefined }, + }); + expect(ExternalConnectorLogic.values.externalConnectorUrl).toEqual(''); + }); + it('sets undefined api key to empty string', () => { + ExternalConnectorLogic.actions.fetchExternalSourceSuccess({ + ...sourceConfigData, + configuredFields: { ...sourceConfigData.configuredFields, apiKey: undefined }, + }); + expect(ExternalConnectorLogic.values.externalConnectorApiKey).toEqual(''); + }); + }); + + describe('saveExternalConnectorConfigSuccess', () => { + it('turns off the button loading flag', () => { + mount({ + buttonLoading: true, + }); + + ExternalConnectorLogic.actions.saveExternalConnectorConfigSuccess('external'); + + expect(ExternalConnectorLogic.values.buttonLoading).toEqual(false); + }); + }); + + describe('setExternalConnectorApiKey', () => { + it('updates the api key', () => { + ExternalConnectorLogic.actions.setExternalConnectorApiKey('abcd1234'); + + expect(ExternalConnectorLogic.values.externalConnectorApiKey).toEqual('abcd1234'); + }); + }); + + describe('setExternalConnectorUrl', () => { + it('updates the url', () => { + ExternalConnectorLogic.actions.setExternalConnectorUrl('https://www.elastic.co'); + + expect(ExternalConnectorLogic.values.externalConnectorUrl).toEqual( + 'https://www.elastic.co' + ); + }); + }); + }); + + describe('listeners', () => { + describe('fetchExternalSource', () => { + it('retrieves config info on the "external" connector', () => { + const promise = Promise.resolve(); + http.get.mockReturnValue(promise); + ExternalConnectorLogic.actions.fetchExternalSource(); + + expect(http.get).toHaveBeenCalledWith( + '/internal/workplace_search/org/settings/connectors/external' + ); + }); + + itShowsServerErrorAsFlashMessage(http.get, () => { + mount(); + ExternalConnectorLogic.actions.fetchExternalSource(); + }); + }); + + describe('saveExternalConnectorConfig', () => { + it('saves the external connector config', () => { + const saveExternalConnectorConfigSuccess = jest.spyOn( + ExternalConnectorLogic.actions, + 'saveExternalConnectorConfigSuccess' + ); + ExternalConnectorLogic.actions.saveExternalConnectorConfig({ + url: 'url', + apiKey: 'apiKey', + }); + expect(saveExternalConnectorConfigSuccess).toHaveBeenCalled(); + expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/external'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_logic.ts new file mode 100644 index 00000000000000..13c0b9167310b1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/external_connector_logic.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { + flashAPIErrors, + flashSuccessToast, + clearFlashMessages, +} from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { AppLogic } from '../../../../app_logic'; + +import { getAddPath, getSourcesPath } from '../../../../routes'; + +import { SourceConfigData } from './add_source_logic'; + +export interface ExternalConnectorActions { + fetchExternalSource: () => true; + fetchExternalSourceSuccess(sourceConfigData: SourceConfigData): SourceConfigData; + saveExternalConnectorConfigSuccess(externalConnectorId: string): string; + setExternalConnectorApiKey(externalConnectorApiKey: string): string; + saveExternalConnectorConfig(config: ExternalConnectorConfig): ExternalConnectorConfig; + setExternalConnectorUrl(externalConnectorUrl: string): string; + resetSourceState: () => true; +} + +export interface ExternalConnectorConfig { + url: string; + apiKey: string; +} + +export interface ExternalConnectorValues { + buttonLoading: boolean; + dataLoading: boolean; + externalConnectorApiKey: string; + externalConnectorUrl: string; + sourceConfigData: SourceConfigData | Pick; +} + +export const ExternalConnectorLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'workplace_search', 'external_connector_logic'], + actions: { + fetchExternalSource: true, + fetchExternalSourceSuccess: (sourceConfigData) => sourceConfigData, + saveExternalConnectorConfigSuccess: (externalConnectorId) => externalConnectorId, + saveExternalConnectorConfig: (config) => config, + setExternalConnectorApiKey: (externalConnectorApiKey: string) => externalConnectorApiKey, + setExternalConnectorUrl: (externalConnectorUrl: string) => externalConnectorUrl, + }, + reducers: { + dataLoading: [ + true, + { + fetchExternalSourceSuccess: () => false, + }, + ], + buttonLoading: [ + false, + { + saveExternalConnectorConfigSuccess: () => false, + saveExternalConnectorConfig: () => true, + }, + ], + externalConnectorUrl: [ + '', + { + fetchExternalSourceSuccess: (_, { configuredFields: { url } }) => url || '', + setExternalConnectorUrl: (_, url) => url, + }, + ], + externalConnectorApiKey: [ + '', + { + fetchExternalSourceSuccess: (_, { configuredFields: { apiKey } }) => apiKey || '', + setExternalConnectorApiKey: (_, apiKey) => apiKey, + }, + ], + sourceConfigData: [ + { name: '', categories: [] }, + { + fetchExternalSourceSuccess: (_, sourceConfigData) => sourceConfigData, + }, + ], + }, + listeners: ({ actions }) => ({ + fetchExternalSource: async () => { + const route = '/internal/workplace_search/org/settings/connectors/external'; + + try { + const response = await HttpLogic.values.http.get(route); + actions.fetchExternalSourceSuccess(response); + } catch (e) { + flashAPIErrors(e); + } + }, + saveExternalConnectorConfig: async () => { + clearFlashMessages(); + // const route = '/internal/workplace_search/org/settings/connectors'; + // const http = HttpLogic.values.http.post; + // const params = { + // url, + // api_key: apiKey, + // service_type: 'external', + // }; + try { + // const response = await http(route, { + // body: JSON.stringify(params), + // }); + + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.externalConnectorCreated', + { + defaultMessage: 'Successfully updated configuration.', + } + ) + ); + // TODO: use response data instead + actions.saveExternalConnectorConfigSuccess('external'); + KibanaLogic.values.navigateToUrl( + getSourcesPath(`${getAddPath('external')}`, AppLogic.values.isOrganization) + ); + } catch (e) { + // flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx index b62648348ed805..c0e72d3b7a5a05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx @@ -61,7 +61,8 @@ export const GitHubViaApp: React.FC = ({ isGithubEnterpriseSe const { hasPlatinumLicense } = useValues(LicensingLogic); const name = isGithubEnterpriseServer ? SOURCE_NAMES.GITHUB_ENTERPRISE : SOURCE_NAMES.GITHUB; - const data = staticSourceData.find((source) => source.name === name); + const serviceType = isGithubEnterpriseServer ? 'github_enterprise_server' : 'github'; + const data = staticSourceData.find((source) => source.serviceType === serviceType); const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; const handleSubmit = (e: FormEvent) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx index 4715c50e4233c8..c05110bd4e6acf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx @@ -11,40 +11,45 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiLink, EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiTitle } from '@elastic/eui'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { LicenseBadge } from '../../../../components/shared/license_badge'; +import { staticCustomSourceData } from '../../source_data'; import { SaveCustom } from './save_custom'; describe('SaveCustom', () => { - const props = { - documentationUrl: 'http://string.boolean', + const mockValues = { newCustomSource: { - accessToken: 'dsgfsd', - key: 'sdfs', - name: 'source', - id: '12e1', + id: 'id', + accessToken: 'token', + name: 'name', }, + sourceData: staticCustomSourceData, isOrganization: true, - header:

Header

, + hasPlatinumLicense: true, }; + + beforeEach(() => { + setMockValues(mockValues); + }); + it('renders', () => { - setMockValues({ hasPlatinumLicense: true }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiPanel)).toHaveLength(1); expect(wrapper.find(EuiTitle)).toHaveLength(4); expect(wrapper.find(EuiLinkTo)).toHaveLength(1); + expect(wrapper.find(LicenseBadge)).toHaveLength(0); }); - - it('renders platinum LicenseBadge and link', () => { - setMockValues({ hasPlatinumLicense: false }); - const wrapper = shallow(); + it('renders platinum license badge if license is not present', () => { + setMockValues({ ...mockValues, hasPlatinumLicense: false }); + const wrapper = shallow(); expect(wrapper.find(LicenseBadge)).toHaveLength(1); - expect(wrapper.find(EuiLink)).toHaveLength(1); + expect(wrapper.find(EuiTitle)).toHaveLength(4); + expect(wrapper.find(EuiLinkTo)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index c136f22d91d3d8..14d088f377f5ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -20,6 +20,7 @@ import { EuiTitle, EuiLink, EuiPanel, + EuiCode, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -27,6 +28,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { docLinks } from '../../../../../shared/doc_links'; import { LicensingLogic } from '../../../../../shared/licensing'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../../app_logic'; import { LicenseBadge } from '../../../../components/shared/license_badge'; import { SOURCES_PATH, @@ -34,11 +36,12 @@ import { getContentSourcePath, getSourcesPath, } from '../../../../routes'; -import { CustomSource } from '../../../../types'; import { LEARN_CUSTOM_FEATURES_BUTTON } from '../../constants'; import { SourceIdentifier } from '../source_identifier'; +import { AddCustomSourceLogic } from './add_custom_source_logic'; +import { AddSourceHeader } from './add_source_header'; import { SAVE_CUSTOM_BODY1, SAVE_CUSTOM_BODY2, @@ -51,23 +54,20 @@ import { SAVE_CUSTOM_DOC_PERMISSIONS_LINK, } from './constants'; -interface SaveCustomProps { - documentationUrl: string; - newCustomSource: CustomSource; - isOrganization: boolean; - header: React.ReactNode; -} - -export const SaveCustom: React.FC = ({ - documentationUrl, - newCustomSource: { id, name }, - isOrganization, - header, -}) => { +export const SaveCustom: React.FC = () => { + const { newCustomSource, sourceData } = useValues(AddCustomSourceLogic); + const { isOrganization } = useValues(AppLogic); const { hasPlatinumLicense } = useValues(LicensingLogic); + const { + serviceType, + configuration: { githubRepository, documentationUrl }, + name, + categories = [], + } = sourceData; + return ( <> - {header} + @@ -84,7 +84,7 @@ export const SaveCustom: React.FC = ({ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.heading', { defaultMessage: '{name} Created', - values: { name }, + values: { name: newCustomSource.name }, } )} @@ -93,7 +93,22 @@ export const SaveCustom: React.FC = ({ {SAVE_CUSTOM_BODY1} -
+ + {serviceType !== 'custom' && githubRepository && ( + <> + +
+ + + {githubRepository} + + + + + )} {SAVE_CUSTOM_BODY2}
@@ -105,7 +120,7 @@ export const SaveCustom: React.FC = ({
- + @@ -119,17 +134,32 @@ export const SaveCustom: React.FC = ({

- - {SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK} - - ), - }} - /> + {serviceType === 'custom' ? ( + + {SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK} + + ), + }} + /> + ) : ( + + {SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK} + + ), + name, + }} + /> + )}

@@ -149,7 +179,7 @@ export const SaveCustom: React.FC = ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 484a9ca14b4e1d..d57dc496832751 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -41,7 +41,7 @@ import { SAVE_CHANGES_BUTTON, REMOVE_BUTTON, } from '../../../constants'; -import { SourceDataItem } from '../../../types'; +import { getEditPath } from '../../../routes'; import { handlePrivateKeyUpload } from '../../../utils'; import { AddSourceLogic } from '../components/add_source/add_source_logic'; import { @@ -57,7 +57,6 @@ import { SYNC_DIAGNOSTICS_DESCRIPTION, SYNC_DIAGNOSTICS_BUTTON, } from '../constants'; -import { staticSourceData } from '../source_data'; import { SourceLogic } from '../source_logic'; import { DownloadDiagnosticsButton } from './download_diagnostics_button'; @@ -96,8 +95,7 @@ export const SourceSettings: React.FC = () => { const editPath = isGithubApp ? undefined // undefined for GitHub apps, as they are configured source-wide, and don't use a connector where you can edit the configuration - : (staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem) - .editPath; + : getEditPath(serviceType); const [inputValue, setValue] = useState(name); const [confirmModalVisible, setModalVisibility] = useState(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index 20a0673709b5ac..f99af418364193 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -10,52 +10,13 @@ import { i18n } from '@kbn/i18n'; import { docLinks } from '../../../shared/doc_links'; import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; -import { - ADD_BOX_PATH, - ADD_CONFLUENCE_PATH, - ADD_CONFLUENCE_SERVER_PATH, - ADD_DROPBOX_PATH, - ADD_GITHUB_ENTERPRISE_PATH, - ADD_GITHUB_PATH, - ADD_GMAIL_PATH, - ADD_GOOGLE_DRIVE_PATH, - ADD_JIRA_PATH, - ADD_JIRA_SERVER_PATH, - ADD_ONEDRIVE_PATH, - ADD_SALESFORCE_PATH, - ADD_SALESFORCE_SANDBOX_PATH, - ADD_SERVICENOW_PATH, - ADD_SHAREPOINT_PATH, - ADD_SLACK_PATH, - ADD_ZENDESK_PATH, - ADD_CUSTOM_PATH, - EDIT_BOX_PATH, - EDIT_CONFLUENCE_PATH, - EDIT_CONFLUENCE_SERVER_PATH, - EDIT_DROPBOX_PATH, - EDIT_GITHUB_ENTERPRISE_PATH, - EDIT_GITHUB_PATH, - EDIT_GMAIL_PATH, - EDIT_GOOGLE_DRIVE_PATH, - EDIT_JIRA_PATH, - EDIT_JIRA_SERVER_PATH, - EDIT_ONEDRIVE_PATH, - EDIT_SALESFORCE_PATH, - EDIT_SALESFORCE_SANDBOX_PATH, - EDIT_SERVICENOW_PATH, - EDIT_SHAREPOINT_PATH, - EDIT_SLACK_PATH, - EDIT_ZENDESK_PATH, - EDIT_CUSTOM_PATH, -} from '../../routes'; import { FeatureIds, SourceDataItem } from '../../types'; -export const staticSourceData = [ +export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.BOX, + iconName: SOURCE_NAMES.BOX, serviceType: 'box', - addPath: ADD_BOX_PATH, - editPath: EDIT_BOX_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -79,12 +40,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.CONFLUENCE, + iconName: SOURCE_NAMES.CONFLUENCE, serviceType: 'confluence_cloud', - addPath: ADD_CONFLUENCE_PATH, - editPath: EDIT_CONFLUENCE_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -113,12 +74,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.CONFLUENCE_SERVER, + iconName: SOURCE_NAMES.CONFLUENCE_SERVER, serviceType: 'confluence_server', - addPath: ADD_CONFLUENCE_SERVER_PATH, - editPath: EDIT_CONFLUENCE_SERVER_PATH, configuration: { isPublicKey: true, hasOauthRedirect: true, @@ -145,12 +106,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.DROPBOX, + iconName: SOURCE_NAMES.DROPBOX, serviceType: 'dropbox', - addPath: ADD_DROPBOX_PATH, - editPath: EDIT_DROPBOX_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -174,12 +135,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.GITHUB, + iconName: SOURCE_NAMES.GITHUB, serviceType: 'github', - addPath: ADD_GITHUB_PATH, - editPath: EDIT_GITHUB_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -210,12 +171,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.GITHUB_ENTERPRISE, + iconName: SOURCE_NAMES.GITHUB_ENTERPRISE, serviceType: 'github_enterprise_server', - addPath: ADD_GITHUB_ENTERPRISE_PATH, - editPath: EDIT_GITHUB_ENTERPRISE_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -252,12 +213,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.GMAIL, + iconName: SOURCE_NAMES.GMAIL, serviceType: 'gmail', - addPath: ADD_GMAIL_PATH, - editPath: EDIT_GMAIL_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -273,9 +234,8 @@ export const staticSourceData = [ }, { name: SOURCE_NAMES.GOOGLE_DRIVE, + iconName: SOURCE_NAMES.GOOGLE_DRIVE, serviceType: 'google_drive', - addPath: ADD_GOOGLE_DRIVE_PATH, - editPath: EDIT_GOOGLE_DRIVE_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -303,12 +263,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.JIRA, + iconName: SOURCE_NAMES.JIRA, serviceType: 'jira_cloud', - addPath: ADD_JIRA_PATH, - editPath: EDIT_JIRA_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -339,12 +299,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.JIRA_SERVER, + iconName: SOURCE_NAMES.JIRA_SERVER, serviceType: 'jira_server', - addPath: ADD_JIRA_SERVER_PATH, - editPath: EDIT_JIRA_SERVER_PATH, configuration: { isPublicKey: true, hasOauthRedirect: true, @@ -374,12 +334,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.ONEDRIVE, + iconName: SOURCE_NAMES.ONEDRIVE, serviceType: 'one_drive', - addPath: ADD_ONEDRIVE_PATH, - editPath: EDIT_ONEDRIVE_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -403,12 +363,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.SALESFORCE, + iconName: SOURCE_NAMES.SALESFORCE, serviceType: 'salesforce', - addPath: ADD_SALESFORCE_PATH, - editPath: EDIT_SALESFORCE_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -439,12 +399,13 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, + { name: SOURCE_NAMES.SALESFORCE_SANDBOX, + iconName: SOURCE_NAMES.SALESFORCE_SANDBOX, serviceType: 'salesforce_sandbox', - addPath: ADD_SALESFORCE_SANDBOX_PATH, - editPath: EDIT_SALESFORCE_SANDBOX_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -475,12 +436,12 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.SERVICENOW, + iconName: SOURCE_NAMES.SERVICENOW, serviceType: 'service_now', - addPath: ADD_SERVICENOW_PATH, - editPath: EDIT_SERVICENOW_PATH, configuration: { isPublicKey: false, hasOauthRedirect: false, @@ -508,12 +469,44 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, { name: SOURCE_NAMES.SHAREPOINT, + iconName: SOURCE_NAMES.SHAREPOINT, serviceType: 'share_point', - addPath: ADD_SHAREPOINT_PATH, - editPath: EDIT_SHAREPOINT_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: docLinks.workplaceSearchSharePoint, + applicationPortalUrl: 'https://portal.azure.com/', + }, + objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.SITES, SOURCE_OBJ_TYPES.ALL_FILES], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + basicOrgContextExcludedFeatures: [FeatureIds.DocumentLevelPermissions], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + + accountContextOnly: false, + internalConnectorAvailable: true, + externalConnectorAvailable: true, + }, + // TODO: temporary hack until backend sends us stuff + { + name: SOURCE_NAMES.SHAREPOINT, + iconName: SOURCE_NAMES.SHAREPOINT, + serviceType: 'external', configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -537,12 +530,54 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, + externalConnectorAvailable: false, + customConnectorAvailable: false, + }, + { + name: SOURCE_NAMES.SHAREPOINT_SERVER, + iconName: SOURCE_NAMES.SHAREPOINT_SERVER, + categories: [ + i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.fileSharing', { + defaultMessage: 'File Sharing', + }), + i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.storage', { + defaultMessage: 'Storage', + }), + i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.cloud', { + defaultMessage: 'Cloud', + }), + i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.microsoft', { + defaultMessage: 'Microsoft', + }), + i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.office', { + defaultMessage: 'Office 365', + }), + ], + serviceType: 'share_point_server', // this doesn't exist on the BE + configuration: { + isPublicKey: false, + hasOauthRedirect: false, + needsBaseUrl: false, + // helpText: i18n.translate( // TODO updatae this + // 'xpack.enterpriseSearch.workplaceSearch.sources.helpText.sharepointServer', + // { + // defaultMessage: + // "Here is some help text. It should probably give the user a heads up that they're going to have to deploy some code.", + // } + // ), + documentationUrl: docLinks.workplaceSearchCustomSources, // TODO update this + applicationPortalUrl: '', + githubRepository: 'elastic/enterprise-search-sharepoint-server-connector', + }, + accountContextOnly: false, + internalConnectorAvailable: false, + customConnectorAvailable: true, }, { name: SOURCE_NAMES.SLACK, + iconName: SOURCE_NAMES.SLACK, serviceType: 'slack', - addPath: ADD_SLACK_PATH, - editPath: EDIT_SLACK_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -559,12 +594,13 @@ export const staticSourceData = [ platinumPrivateContext: [FeatureIds.Remote, FeatureIds.Private, FeatureIds.SearchableContent], }, accountContextOnly: true, + internalConnectorAvailable: true, }, + { name: SOURCE_NAMES.ZENDESK, + iconName: SOURCE_NAMES.ZENDESK, serviceType: 'zendesk', - addPath: ADD_ZENDESK_PATH, - editPath: EDIT_ZENDESK_PATH, configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -588,23 +624,26 @@ export const staticSourceData = [ ], }, accountContextOnly: false, + internalConnectorAvailable: true, }, - { - name: SOURCE_NAMES.CUSTOM, - serviceType: 'custom', - addPath: ADD_CUSTOM_PATH, - editPath: EDIT_CUSTOM_PATH, - configuration: { - isPublicKey: false, - hasOauthRedirect: false, - needsBaseUrl: false, - helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', { - defaultMessage: - 'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.', - }), - documentationUrl: docLinks.workplaceSearchCustomSources, - applicationPortalUrl: '', - }, - accountContextOnly: false, +]; + +export const staticCustomSourceData: SourceDataItem = { + name: SOURCE_NAMES.CUSTOM, + iconName: SOURCE_NAMES.CUSTOM, + categories: ['API', 'Custom'], + serviceType: 'custom', + configuration: { + isPublicKey: false, + hasOauthRedirect: false, + needsBaseUrl: false, + helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', { + defaultMessage: + 'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.', + }), + documentationUrl: docLinks.workplaceSearchCustomSources, + applicationPortalUrl: '', }, -] as SourceDataItem[]; + accountContextOnly: false, + customConnectorAvailable: true, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts index f7e41f65120174..a007d31ff67cb5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts @@ -18,6 +18,7 @@ jest.mock('../../app_logic', () => ({ import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { AppLogic } from '../../app_logic'; +import { staticSourceData } from './source_data'; import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_logic'; describe('SourcesLogic', () => { @@ -32,8 +33,8 @@ describe('SourcesLogic', () => { const defaultValues = { contentSources: [], privateContentSources: [], - sourceData: [], - availableSources: [], + sourceData: staticSourceData.map((data) => ({ ...data, connected: false })), + availableSources: staticSourceData.map((data) => ({ ...data, connected: false })), configuredSources: [], serviceTypes: [], permissionsModal: null, @@ -316,7 +317,7 @@ describe('SourcesLogic', () => { it('availableSources & configuredSources have correct length', () => { SourcesLogic.actions.onInitializeSources(serverResponse); - expect(SourcesLogic.values.availableSources).toHaveLength(1); + expect(SourcesLogic.values.availableSources).toHaveLength(14); expect(SourcesLogic.values.configuredSources).toHaveLength(5); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index 90b1f83281e942..b7bdef52fceb00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -178,7 +178,7 @@ export const SourcesLogic = kea>( if (isOrganization && !values.serverStatuses) { // We want to get the initial statuses from the server to compare our polling results to. const sourceStatuses = await fetchSourceStatuses(isOrganization, breakpoint); - actions.setServerSourceStatuses(sourceStatuses); + actions.setServerSourceStatuses(sourceStatuses ?? []); } }, // We poll the server and if the status update, we trigger a new fetch of the sources. @@ -190,7 +190,7 @@ export const SourcesLogic = kea>( pollingInterval = window.setInterval(async () => { const sourceStatuses = await fetchSourceStatuses(isOrganization, breakpoint); - sourceStatuses.some((source: ContentSourceStatus) => { + (sourceStatuses ?? []).some((source: ContentSourceStatus) => { if (serverStatuses && serverStatuses[source.id] !== source.status.status) { return actions.initializeSources(); } @@ -249,7 +249,7 @@ export const SourcesLogic = kea>( export const fetchSourceStatuses = async ( isOrganization: boolean, breakpoint: BreakPointFunction -) => { +): Promise => { const route = isOrganization ? '/internal/workplace_search/org/sources/status' : '/internal/workplace_search/account/sources/status'; @@ -267,8 +267,7 @@ export const fetchSourceStatuses = async ( } } - // TODO: remove casting. return type should be ContentSourceStatus[] | undefined - return response as ContentSourceStatus[]; + return response; }; const updateSourcesOnToggle = ( @@ -293,7 +292,7 @@ const updateSourcesOnToggle = ( * The second is the base list of available sources that the server sends back in the collection, * `availableTypes` that is the source of truth for the name and whether the source has been configured. * - * Fnally, also in the collection response is the current set of connected sources. We check for the + * Finally, also in the collection response is the current set of connected sources. We check for the * existence of a `connectedSource` of the type in the loop and set `connected` to true so that the UI * can diplay "Add New" instead of "Connect", the latter of which is displated only when a connector * has been configured but there are no connected sources yet. @@ -304,13 +303,13 @@ export const mergeServerAndStaticData = ( contentSources: ContentSourceDetails[] ) => { const combined = [] as CombinedDataItem[]; - serverData.forEach((serverItem) => { - const type = serverItem.serviceType; - const staticItem = staticData.find(({ serviceType }) => serviceType === type); + staticData.forEach((staticItem) => { + const type = staticItem.serviceType; + const serverItem = serverData.find(({ serviceType }) => serviceType === type); const connectedSource = contentSources.find(({ serviceType }) => serviceType === type); combined.push({ - ...serverItem, ...staticItem, + ...serverItem, connected: !!connectedSource, } as CombinedDataItem); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx index cf5dc48682ae83..49c8ebbbebc08c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx @@ -34,7 +34,7 @@ describe('SourcesRouter', () => { }); it('renders sources routes', () => { - const TOTAL_ROUTES = 63; + const TOTAL_ROUTES = 86; const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); @@ -45,8 +45,8 @@ describe('SourcesRouter', () => { setMockValues({ ...mockValues, hasPlatinumLicense: false }); const wrapper = shallow(); - expect(wrapper.find(Redirect).first().prop('from')).toEqual(ADD_SOURCE_PATH); - expect(wrapper.find(Redirect).first().prop('to')).toEqual(SOURCES_PATH); + expect(wrapper.find(Redirect).last().prop('from')).toEqual(ADD_SOURCE_PATH); + expect(wrapper.find(Redirect).last().prop('to')).toEqual(SOURCES_PATH); }); it('redirects when cannot create sources', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index 23109506b364ee..c2cd58a90f209d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -14,19 +14,27 @@ import { useActions, useValues } from 'kea'; import { LicensingLogic } from '../../../shared/licensing'; import { AppLogic } from '../../app_logic'; import { - ADD_GITHUB_VIA_APP_PATH, - ADD_GITHUB_ENTERPRISE_SERVER_VIA_APP_PATH, + GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE, + GITHUB_VIA_APP_SERVICE_TYPE, +} from '../../constants'; +import { ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, PRIVATE_SOURCES_PATH, SOURCES_PATH, getSourcesPath, + getAddPath, + ADD_CUSTOM_PATH, } from '../../routes'; +import { hasMultipleConnectorOptions } from '../../utils'; import { AddSource, AddSourceList, GitHubViaApp } from './components/add_source'; +import { AddCustomSource } from './components/add_source/add_custom_source'; +import { ConfigurationChoice } from './components/add_source/configuration_choice'; +import { ExternalConnectorConfig } from './components/add_source/external_connector_config'; import { OrganizationSources } from './organization_sources'; import { PrivateSources } from './private_sources'; -import { staticSourceData } from './source_data'; +import { staticCustomSourceData, staticSourceData as sources } from './source_data'; import { SourceRouter } from './source_router'; import { SourcesLogic } from './sources_logic'; @@ -68,36 +76,121 @@ export const SourcesRouter: React.FC = () => { - + - + - {staticSourceData.map(({ addPath, accountContextOnly }, i) => ( - - {!hasPlatinumLicense && accountContextOnly ? ( - - ) : ( - - )} - - ))} - {staticSourceData.map(({ addPath }, i) => ( - - + {sources.map((sourceData, i) => { + const { serviceType, externalConnectorAvailable, internalConnectorAvailable } = sourceData; + const path = `${getSourcesPath(getAddPath(serviceType), isOrganization)}`; + const defaultOption = internalConnectorAvailable + ? 'internal' + : externalConnectorAvailable + ? 'external' + : 'custom'; + return ( + + {hasMultipleConnectorOptions(sourceData) ? ( + + ) : ( + + )} + + ); + })} + + + + {sources + .filter((sourceData) => sourceData.internalConnectorAvailable) + .map((sourceData, i) => { + const { serviceType, accountContextOnly } = sourceData; + return ( + + {!hasPlatinumLicense && accountContextOnly ? ( + + ) : ( + + )} + + ); + })} + {sources + .filter((sourceData) => sourceData.externalConnectorAvailable) + .map((sourceData, i) => { + const { serviceType, accountContextOnly } = sourceData; + + return ( + + {!hasPlatinumLicense && accountContextOnly ? ( + + ) : ( + + )} + + ); + })} + {sources + .filter((sourceData) => sourceData.customConnectorAvailable) + .map((sourceData, i) => { + const { serviceType, accountContextOnly } = sourceData; + return ( + + {!hasPlatinumLicense && accountContextOnly ? ( + + ) : ( + + )} + + ); + })} + {sources.map((sourceData, i) => ( + + ))} - {staticSourceData.map(({ addPath }, i) => ( - - + {sources.map((sourceData, i) => ( + + ))} - {staticSourceData.map(({ addPath, configuration: { needsConfiguration } }, i) => { - if (needsConfiguration) + {sources.map((sourceData, i) => { + if (sourceData.configuration.needsConfiguration) return ( - - + + ); })} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx index 85f91f769cc77f..be139fd6b38eaf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx @@ -33,9 +33,7 @@ import { PRIVATE_SOURCE, UPDATE_BUTTON, } from '../../../constants'; -import { getSourcesPath } from '../../../routes'; -import { SourceDataItem } from '../../../types'; -import { staticSourceData } from '../../content_sources/source_data'; +import { getAddPath, getEditPath, getSourcesPath } from '../../../routes'; import { SettingsLogic } from '../settings_logic'; export const Connectors: React.FC = () => { @@ -52,9 +50,9 @@ export const Connectors: React.FC = () => { ); const getRowActions = (configured: boolean, serviceType: string, supportedByLicense: boolean) => { - const { addPath, editPath } = staticSourceData.find( - (s) => s.serviceType === serviceType - ) as SourceDataItem; + const addPath = getAddPath(serviceType); + const editPath = getEditPath(serviceType); + const configurePath = getSourcesPath(addPath, true); const updateButtons = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx index 35619d2b2d560d..af8b8fe461f162 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx @@ -18,6 +18,8 @@ import { EuiConfirmModal } from '@elastic/eui'; import { SaveConfig } from '../../content_sources/components/add_source/save_config'; +import { staticSourceData } from '../../content_sources/source_data'; + import { SourceConfig } from './source_config'; describe('SourceConfig', () => { @@ -31,7 +33,7 @@ describe('SourceConfig', () => { }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); const saveConfig = wrapper.find(SaveConfig); // Trigger modal visibility @@ -42,13 +44,13 @@ describe('SourceConfig', () => { it('renders a breadcrumb fallback while data is loading', () => { setMockValues({ dataLoading: true, sourceConfigData: {} }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.prop('pageChrome')).toEqual(['Settings', 'Content source connectors', '...']); }); it('handles delete click', () => { - const wrapper = shallow(); + const wrapper = shallow(); const saveConfig = wrapper.find(SaveConfig); // Trigger modal visibility @@ -60,7 +62,7 @@ describe('SourceConfig', () => { }); it('saves source config', () => { - const wrapper = shallow(); + const wrapper = shallow(); const saveConfig = wrapper.find(SaveConfig); // Trigger modal visibility @@ -72,7 +74,7 @@ describe('SourceConfig', () => { }); it('cancels and closes modal', () => { - const wrapper = shallow(); + const wrapper = shallow(); const saveConfig = wrapper.find(SaveConfig); // Trigger modal visibility diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index c2a0b60e1eca3d..ea63f3bab77d98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -18,16 +18,15 @@ import { SourceDataItem } from '../../../types'; import { AddSourceHeader } from '../../content_sources/components/add_source/add_source_header'; import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic'; import { SaveConfig } from '../../content_sources/components/add_source/save_config'; -import { staticSourceData } from '../../content_sources/source_data'; import { SettingsLogic } from '../settings_logic'; interface SourceConfigProps { - sourceIndex: number; + sourceData: SourceDataItem; } -export const SourceConfig: React.FC = ({ sourceIndex }) => { +export const SourceConfig: React.FC = ({ sourceData }) => { const [confirmModalVisible, setConfirmModalVisibility] = useState(false); - const { configuration, serviceType } = staticSourceData[sourceIndex] as SourceDataItem; + const { configuration, serviceType } = sourceData; const { deleteSourceConfig } = useActions(SettingsLogic); const { saveSourceConfig, getSourceConfigData } = useActions(AddSourceLogic); const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx index d9aeba361d2400..7c5e501d6a2a12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx @@ -14,6 +14,7 @@ import { ORG_SETTINGS_CUSTOMIZE_PATH, ORG_SETTINGS_CONNECTORS_PATH, ORG_SETTINGS_OAUTH_APPLICATION_PATH, + getEditPath, } from '../../routes'; import { staticSourceData } from '../content_sources/source_data'; @@ -41,9 +42,9 @@ export const SettingsRouter: React.FC = () => { - {staticSourceData.map(({ editPath }, i) => ( - - + {staticSourceData.map((sourceData, i) => ( + + ))} diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/sharepoint_server.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/sharepoint_server.svg new file mode 100644 index 00000000000000..aebfd7a8e49c0f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/sharepoint_server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 1cc96be1b40f87..5b193d3e809640 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -26,7 +26,7 @@ import { SecurityPluginSetup, SecurityPluginStart } from '../../security/public' import { APP_SEARCH_PLUGIN, - ENTERPRISE_SEARCH_PLUGIN, + ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; import { InitialAppData } from '../common/types'; @@ -67,30 +67,32 @@ export class EnterpriseSearchPlugin implements Plugin { const { cloud } = plugins; core.application.register({ - id: ENTERPRISE_SEARCH_PLUGIN.ID, - title: ENTERPRISE_SEARCH_PLUGIN.NAV_TITLE, - euiIconType: ENTERPRISE_SEARCH_PLUGIN.LOGO, - appRoute: ENTERPRISE_SEARCH_PLUGIN.URL, + id: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, + title: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAV_TITLE, + euiIconType: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.LOGO, + appRoute: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { const kibanaDeps = await this.getKibanaDeps(core, params, cloud); const { chrome, http } = kibanaDeps.core; - chrome.docTitle.change(ENTERPRISE_SEARCH_PLUGIN.NAME); + chrome.docTitle.change(ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAME); await this.getInitialData(http); const pluginData = this.getPluginData(); const { renderApp } = await import('./applications'); - const { EnterpriseSearch } = await import('./applications/enterprise_search'); + const { EnterpriseSearchOverview } = await import( + './applications/enterprise_search_overview' + ); - return renderApp(EnterpriseSearch, kibanaDeps, pluginData); + return renderApp(EnterpriseSearchOverview, kibanaDeps, pluginData); }, }); core.application.register({ id: APP_SEARCH_PLUGIN.ID, title: APP_SEARCH_PLUGIN.NAME, - euiIconType: ENTERPRISE_SEARCH_PLUGIN.LOGO, + euiIconType: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.LOGO, appRoute: APP_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { @@ -111,7 +113,7 @@ export class EnterpriseSearchPlugin implements Plugin { core.application.register({ id: WORKPLACE_SEARCH_PLUGIN.ID, title: WORKPLACE_SEARCH_PLUGIN.NAME, - euiIconType: ENTERPRISE_SEARCH_PLUGIN.LOGO, + euiIconType: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.LOGO, appRoute: WORKPLACE_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { @@ -134,11 +136,11 @@ export class EnterpriseSearchPlugin implements Plugin { if (plugins.home) { plugins.home.featureCatalogue.registerSolution({ - id: ENTERPRISE_SEARCH_PLUGIN.ID, - title: ENTERPRISE_SEARCH_PLUGIN.NAME, + id: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, + title: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAME, icon: 'logoEnterpriseSearch', - description: ENTERPRISE_SEARCH_PLUGIN.DESCRIPTION, - path: ENTERPRISE_SEARCH_PLUGIN.URL, + description: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.DESCRIPTION, + path: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, order: 100, }); diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index fe8e584b65bedc..ebe98d4e805ac1 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -235,6 +235,24 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ categories: ['file_storage'], uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/share_point', }, + { + id: 'sharepoint_server', + title: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerName', + { + defaultMessage: 'SharePoint Server', + } + ), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription', + { + defaultMessage: + 'Search over your files stored on Microsoft SharePoint Server with Workplace Search.', + } + ), + categories: ['enterprise_search', 'file_storage', 'microsoft_365'], + uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/sharepoint_server', + }, { id: 'slack', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.slackName', { diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 29a744e487f404..ef9a0cea9da60f 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -23,7 +23,7 @@ import { SecurityPluginSetup } from '../../security/server'; import { SpacesPluginStart } from '../../spaces/server'; import { - ENTERPRISE_SEARCH_PLUGIN, + ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, LOGS_SOURCE_ID, @@ -101,17 +101,21 @@ export class EnterpriseSearchPlugin implements Plugin { * Register space/feature control */ features.registerKibanaFeature({ - id: ENTERPRISE_SEARCH_PLUGIN.ID, - name: ENTERPRISE_SEARCH_PLUGIN.NAME, + id: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, + name: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAME, order: 0, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, app: [ 'kibana', - ENTERPRISE_SEARCH_PLUGIN.ID, + ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, + APP_SEARCH_PLUGIN.ID, + WORKPLACE_SEARCH_PLUGIN.ID, + ], + catalogue: [ + ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID, ], - catalogue: [ENTERPRISE_SEARCH_PLUGIN.ID, APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID], privileges: null, }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 222288d369fdb6..d1063021282045 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -38,6 +38,14 @@ const oauthConfigSchema = schema.object({ consumer_key: schema.maybe(schema.string()), }); +const externalConnectorSchema = schema.object({ + url: schema.string(), + api_key: schema.string(), + service_type: schema.string(), +}); + +const postConnectorSchema = schema.oneOf([externalConnectorSchema, oauthConfigSchema]); + const displayFieldSchema = schema.object({ fieldName: schema.string(), label: schema.string(), @@ -872,7 +880,7 @@ export function registerOrgSourceOauthConfigurationsRoute({ { path: '/internal/workplace_search/org/settings/connectors', validate: { - body: oauthConfigSchema, + body: postConnectorSchema, }, }, enterpriseSearchRequestHandler.createRequest({ diff --git a/x-pack/plugins/fleet/.storybook/context/execution_context.ts b/x-pack/plugins/fleet/.storybook/context/execution_context.ts new file mode 100644 index 00000000000000..d3a15e200129bb --- /dev/null +++ b/x-pack/plugins/fleet/.storybook/context/execution_context.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { ExecutionContextSetup } from 'kibana/public'; +import { of } from 'rxjs'; + +export const getExecutionContext = () => { + const exec: ExecutionContextSetup = { + context$: of({}), + get: () => { + return {}; + }, + clear: () => {}, + set: (context: Record) => {}, + getAsLabels: () => { + return {}; + }, + withGlobalContext: () => { + return {}; + }, + }; + + return exec; +}; diff --git a/x-pack/plugins/fleet/.storybook/context/index.tsx b/x-pack/plugins/fleet/.storybook/context/index.tsx index eb19a1145ba751..fbcbd4fd3a0811 100644 --- a/x-pack/plugins/fleet/.storybook/context/index.tsx +++ b/x-pack/plugins/fleet/.storybook/context/index.tsx @@ -31,6 +31,7 @@ import { stubbedStartServices } from './stubs'; import { getDocLinks } from './doc_links'; import { getCloud } from './cloud'; import { getShare } from './share'; +import { getExecutionContext } from './execution_context'; // TODO: clintandrewhall - this is not ideal, or complete. The root context of Fleet applications // requires full start contracts of its dependencies. As a result, we have to mock all of those contracts @@ -52,6 +53,7 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({ () => ({ ...stubbedStartServices, application: getApplication(), + executionContext: getExecutionContext(), chrome: getChrome(), cloud: { ...getCloud({ isCloudEnabled }), diff --git a/x-pack/plugins/fleet/common/constants/output.ts b/x-pack/plugins/fleet/common/constants/output.ts index e41e3c526951e8..318712d228859c 100644 --- a/x-pack/plugins/fleet/common/constants/output.ts +++ b/x-pack/plugins/fleet/common/constants/output.ts @@ -23,3 +23,5 @@ export const DEFAULT_OUTPUT: NewOutput = { type: outputType.Elasticsearch, hosts: [''], }; + +export const LICENCE_FOR_PER_POLICY_OUTPUT = 'platinum'; diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 46cd3e998ea7f4..6db1459d90c64c 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -621,6 +621,19 @@ "type" ] } + }, + "_meta": { + "type": "object", + "properties": { + "install_source": { + "type": "string", + "enum": [ + "registry", + "upload", + "bundled" + ] + } + } } }, "required": [ @@ -3825,6 +3838,14 @@ "logs" ] } + }, + "data_output_id": { + "type": "string", + "nullable": true + }, + "monitoring_output_id": { + "type": "string", + "nullable": true } }, "required": [ @@ -3981,6 +4002,12 @@ "updated_by": { "type": "string" }, + "data_output_id": { + "type": "string" + }, + "monitoring_output_id": { + "type": "string" + }, "revision": { "type": "number" }, diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index ae8fdb3b87d4d8..6aaeaeaf160817 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -382,6 +382,15 @@ paths: required: - id - type + _meta: + type: object + properties: + install_source: + type: string + enum: + - registry + - upload + - bundled required: - items operationId: install-package @@ -2402,6 +2411,12 @@ components: enum: - metrics - logs + data_output_id: + type: string + nullable: true + monitoring_output_id: + type: string + nullable: true required: - name - namespace @@ -2501,6 +2516,10 @@ components: format: date-time updated_by: type: string + data_output_id: + type: string + monitoring_output_id: + type: string revision: type: number agents: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml index 7eed85eb2e3bce..c2cebb183ed869 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml @@ -22,6 +22,10 @@ allOf: format: date-time updated_by: type: string + data_output_id: + type: string + monitoring_output_id: + type: string revision: type: number agents: diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/new_agent_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/new_agent_policy.yaml index 7b9e7f43c8ab00..7ad8988f1b0e42 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/new_agent_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/new_agent_policy.yaml @@ -16,6 +16,12 @@ properties: enum: - metrics - logs + data_output_id: + type: string + nullable: true + monitoring_output_id: + type: string + nullable: true required: - name - namespace \ No newline at end of file diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml index ef0964b66e045d..6ef61788acd62d 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml @@ -64,6 +64,15 @@ post: required: - id - type + _meta: + type: object + properties: + install_source: + type: string + enum: + - registry + - upload + - bundled required: - items operationId: install-package diff --git a/x-pack/plugins/fleet/common/services/license.ts b/x-pack/plugins/fleet/common/services/license.ts index d7e64f484474a7..a5fdfb1e741493 100644 --- a/x-pack/plugins/fleet/common/services/license.ts +++ b/x-pack/plugins/fleet/common/services/license.ts @@ -40,18 +40,10 @@ export class LicenseService { } public isGoldPlus() { - return ( - this.licenseInformation?.isAvailable && - this.licenseInformation?.isActive && - this.licenseInformation?.hasAtLeast('gold') - ); + return this.hasAtLeast('gold'); } public isEnterprise() { - return ( - this.licenseInformation?.isAvailable && - this.licenseInformation?.isActive && - this.licenseInformation?.hasAtLeast('enterprise') - ); + return this.hasAtLeast('enterprise'); } public hasAtLeast(licenseType: LicenseType) { return ( diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 6fbb423507c3b0..4d87d103856175 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -25,8 +25,9 @@ export interface NewAgentPolicy { monitoring_enabled?: MonitoringType; unenroll_timeout?: number; is_preconfigured?: boolean; - data_output_id?: string; - monitoring_output_id?: string; + // Nullable to allow user to reset to default outputs + data_output_id?: string | null; + monitoring_output_id?: string | null; } export interface AgentPolicy extends Omit { diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 64ea5665241e13..1c7e09a51c5da7 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -43,7 +43,7 @@ export interface DefaultPackagesInstallationError { } export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install' | 'unknown'; -export type InstallSource = 'registry' | 'upload'; +export type InstallSource = 'registry' | 'upload' | 'bundled'; export type EpmPackageInstallStatus = | 'installed' diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index 6a72792e780ef5..f1ccaae05487b0 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -12,6 +12,7 @@ import type { PackageInfo, PackageUsageStats, InstallType, + InstallSource, } from '../models/epm'; export interface GetCategoriesRequest { @@ -108,6 +109,9 @@ export interface InstallPackageRequest { export interface InstallPackageResponse { items: AssetReference[]; + _meta: { + install_source: InstallSource; + }; // deprecated in 8.0 response?: AssetReference[]; } @@ -123,6 +127,7 @@ export interface InstallResult { status?: 'installed' | 'already_installed'; error?: Error; installType: InstallType; + installSource: InstallSource; } export interface BulkInstallPackageInfo { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx new file mode 100644 index 00000000000000..88072b327d9f29 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createFleetTestRendererMock } from '../../../../../../mock'; +import type { MockedFleetStartServices } from '../../../../../../mock'; +import { useLicense } from '../../../../../../hooks/use_license'; +import type { LicenseService } from '../../../../services'; + +import { useOutputOptions } from './hooks'; + +jest.mock('../../../../../../hooks/use_license'); + +const mockedUseLicence = useLicense as jest.MockedFunction; + +function defaultHttpClientGetImplementation(path: any) { + if (typeof path !== 'string') { + throw new Error('Invalid request'); + } + const err = new Error(`API [GET ${path}] is not MOCKED!`); + // eslint-disable-next-line no-console + console.log(err); + throw err; +} + +const mockApiCallsWithOutputs = (http: MockedFleetStartServices['http']) => { + http.get.mockImplementation(async (path) => { + if (typeof path !== 'string') { + throw new Error('Invalid request'); + } + if (path === '/api/fleet/outputs') { + return { + data: { + items: [ + { + id: 'output1', + name: 'Output 1', + is_default: true, + is_default_monitoring: true, + }, + { + id: 'output2', + name: 'Output 2', + is_default: true, + is_default_monitoring: true, + }, + { + id: 'output3', + name: 'Output 3', + is_default: true, + is_default_monitoring: true, + }, + ], + }, + }; + } + + return defaultHttpClientGetImplementation(path); + }); +}; + +describe('useOutputOptions', () => { + it('should generate enabled options if the licence is platinium', async () => { + const testRenderer = createFleetTestRendererMock(); + mockedUseLicence.mockReturnValue({ + hasAtLeast: () => true, + } as unknown as LicenseService); + mockApiCallsWithOutputs(testRenderer.startServices.http); + const { result, waitForNextUpdate } = testRenderer.renderHook(() => useOutputOptions()); + expect(result.current.isLoading).toBeTruthy(); + + await waitForNextUpdate(); + expect(result.current.dataOutputOptions).toMatchInlineSnapshot(` + Array [ + Object { + "inputDisplay": "Default (currently Output 1)", + "value": "@@##DEFAULT_OUTPUT_VALUE##@@", + }, + Object { + "disabled": false, + "inputDisplay": "Output 1", + "value": "output1", + }, + Object { + "disabled": false, + "inputDisplay": "Output 2", + "value": "output2", + }, + Object { + "disabled": false, + "inputDisplay": "Output 3", + "value": "output3", + }, + ] + `); + expect(result.current.monitoringOutputOptions).toMatchInlineSnapshot(` + Array [ + Object { + "inputDisplay": "Default (currently Output 1)", + "value": "@@##DEFAULT_OUTPUT_VALUE##@@", + }, + Object { + "disabled": false, + "inputDisplay": "Output 1", + "value": "output1", + }, + Object { + "disabled": false, + "inputDisplay": "Output 2", + "value": "output2", + }, + Object { + "disabled": false, + "inputDisplay": "Output 3", + "value": "output3", + }, + ] + `); + }); + + it('should only enable the default options if the licence is not platinium', async () => { + const testRenderer = createFleetTestRendererMock(); + mockedUseLicence.mockReturnValue({ + hasAtLeast: () => false, + } as unknown as LicenseService); + mockApiCallsWithOutputs(testRenderer.startServices.http); + const { result, waitForNextUpdate } = testRenderer.renderHook(() => useOutputOptions()); + expect(result.current.isLoading).toBeTruthy(); + + await waitForNextUpdate(); + expect(result.current.dataOutputOptions).toMatchInlineSnapshot(` + Array [ + Object { + "inputDisplay": "Default (currently Output 1)", + "value": "@@##DEFAULT_OUTPUT_VALUE##@@", + }, + Object { + "disabled": true, + "inputDisplay": "Output 1", + "value": "output1", + }, + Object { + "disabled": true, + "inputDisplay": "Output 2", + "value": "output2", + }, + Object { + "disabled": true, + "inputDisplay": "Output 3", + "value": "output3", + }, + ] + `); + expect(result.current.monitoringOutputOptions).toMatchInlineSnapshot(` + Array [ + Object { + "inputDisplay": "Default (currently Output 1)", + "value": "@@##DEFAULT_OUTPUT_VALUE##@@", + }, + Object { + "disabled": true, + "inputDisplay": "Output 1", + "value": "output1", + }, + Object { + "disabled": true, + "inputDisplay": "Output 2", + "value": "output2", + }, + Object { + "disabled": true, + "inputDisplay": "Output 3", + "value": "output3", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx new file mode 100644 index 00000000000000..b0922238799940 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { EuiSuperSelectOption } from '@elastic/eui'; + +import { useGetOutputs, useLicense } from '../../../../hooks'; +import { LICENCE_FOR_PER_POLICY_OUTPUT } from '../../../../../../../common'; + +// The super select component do not support null or '' as a value +export const DEFAULT_OUTPUT_VALUE = '@@##DEFAULT_OUTPUT_VALUE##@@'; + +function getDefaultOutput(defaultOutputName?: string) { + return { + inputDisplay: i18n.translate('xpack.fleet.agentPolicy.outputOptions.defaultOutputText', { + defaultMessage: 'Default (currently {defaultOutputName})', + values: { defaultOutputName }, + }), + value: DEFAULT_OUTPUT_VALUE, + }; +} + +export function useOutputOptions() { + const outputsRequest = useGetOutputs(); + const licenseService = useLicense(); + + const isLicenceAllowingPolicyPerOutput = licenseService.hasAtLeast(LICENCE_FOR_PER_POLICY_OUTPUT); + + const outputOptions: Array> = useMemo(() => { + if (outputsRequest.isLoading || !outputsRequest.data) { + return []; + } + + return outputsRequest.data.items.map((item) => ({ + value: item.id, + inputDisplay: item.name, + disabled: !isLicenceAllowingPolicyPerOutput, + })); + }, [outputsRequest, isLicenceAllowingPolicyPerOutput]); + + const dataOutputOptions = useMemo(() => { + if (outputsRequest.isLoading || !outputsRequest.data) { + return []; + } + + const defaultOutputName = outputsRequest.data.items.find((item) => item.is_default)?.name; + return [getDefaultOutput(defaultOutputName), ...outputOptions]; + }, [outputsRequest, outputOptions]); + + const monitoringOutputOptions = useMemo(() => { + if (outputsRequest.isLoading || !outputsRequest.data) { + return []; + } + + const defaultOutputName = outputsRequest.data.items.find( + (item) => item.is_default_monitoring + )?.name; + return [getDefaultOutput(defaultOutputName), ...outputOptions]; + }, [outputsRequest, outputOptions]); + + return useMemo( + () => ({ + dataOutputOptions, + monitoringOutputOptions, + isLoading: outputsRequest.isLoading, + }), + [dataOutputOptions, monitoringOutputOptions, outputsRequest.isLoading] + ); +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx similarity index 77% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index d26dc83084a20f..305008513d0199 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -17,20 +17,23 @@ import { EuiLink, EuiFieldNumber, EuiFieldText, + EuiSuperSelect, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { dataTypes } from '../../../../../../common'; -import type { NewAgentPolicy, AgentPolicy } from '../../../types'; -import { useStartServices } from '../../../hooks'; +import { dataTypes } from '../../../../../../../common'; +import type { NewAgentPolicy, AgentPolicy } from '../../../../types'; +import { useStartServices } from '../../../../hooks'; -import { AgentPolicyPackageBadge } from '../../../components'; +import { AgentPolicyPackageBadge } from '../../../../components'; -import { policyHasFleetServer } from '../../agents/services/has_fleet_server'; +import { policyHasFleetServer } from '../../../agents/services/has_fleet_server'; -import { AgentPolicyDeleteProvider } from './agent_policy_delete_provider'; -import type { ValidationResults } from './agent_policy_validation'; +import { AgentPolicyDeleteProvider } from '../agent_policy_delete_provider'; +import type { ValidationResults } from '../agent_policy_validation'; + +import { useOutputOptions, DEFAULT_OUTPUT_VALUE } from './hooks'; interface Props { agentPolicy: Partial; @@ -49,6 +52,11 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = }) => { const { docLinks } = useStartServices(); const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); + const { + dataOutputOptions, + monitoringOutputOptions, + isLoading: isLoadingOptions, + } = useOutputOptions(); // agent monitoring checkbox group can appear multiple times in the DOM, ids have to be unique to work correctly const monitoringCheckboxIdSuffix = Date.now(); @@ -275,6 +283,82 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = /> + + + + } + description={ + + } + > + + { + updateAgentPolicy({ + data_output_id: e !== DEFAULT_OUTPUT_VALUE ? e : null, + }); + }} + options={dataOutputOptions} + /> + + + + + + } + description={ + + } + > + + { + updateAgentPolicy({ + monitoring_output_id: e !== DEFAULT_OUTPUT_VALUE ? e : null, + }); + }} + options={monitoringOutputOptions} + /> + + {isEditing && 'id' in agentPolicy && !agentPolicy.is_managed ? ( ( const submitUpdateAgentPolicy = async () => { setIsLoading(true); try { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { name, description, namespace, monitoring_enabled, unenroll_timeout } = agentPolicy; + const { + name, + description, + namespace, + // eslint-disable-next-line @typescript-eslint/naming-convention + monitoring_enabled, + // eslint-disable-next-line @typescript-eslint/naming-convention + unenroll_timeout, + // eslint-disable-next-line @typescript-eslint/naming-convention + data_output_id, + // eslint-disable-next-line @typescript-eslint/naming-convention + monitoring_output_id, + } = agentPolicy; const { data, error } = await sendUpdateAgentPolicy(agentPolicy.id, { name, description, namespace, monitoring_enabled, unenroll_timeout, + data_output_id, + monitoring_output_id, }); if (data) { notifications.toasts.addSuccess( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/output_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/output_section.tsx index 835a3576da77b8..1da2bacf9068db 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/output_section.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/output_section.tsx @@ -12,7 +12,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useLink } from '../../../../hooks'; import type { Output } from '../../../../types'; import { OutputsTable } from '../outputs_table'; -import { FEATURE_ADD_OUTPUT_ENABLED } from '../../constants'; export interface OutputSectionProps { outputs: Output[]; @@ -42,14 +41,12 @@ export const OutputSection: React.FunctionComponent = ({ - {FEATURE_ADD_OUTPUT_ENABLED && ( - - - - )} + + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/constants/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/constants/index.tsx index b609c4c25308f8..8d29433e7232b1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/constants/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/constants/index.tsx @@ -6,5 +6,3 @@ */ export const FLYOUT_MAX_WIDTH = 670; - -export const FEATURE_ADD_OUTPUT_ENABLED = false; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx index 5a393ee74ea7b6..c586e882619403 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx @@ -19,7 +19,6 @@ import { withConfirmModalProvider } from './hooks/use_confirm_modal'; import { FleetServerHostsFlyout } from './components/fleet_server_hosts_flyout'; import { EditOutputFlyout } from './components/edit_output_flyout'; import { useDeleteOutput } from './hooks/use_delete_output'; -import { FEATURE_ADD_OUTPUT_ENABLED } from './constants'; export const SettingsApp = withConfirmModalProvider(() => { useBreadcrumbs('settings'); @@ -64,13 +63,11 @@ export const SettingsApp = withConfirmModalProvider(() => { /> - {FEATURE_ADD_OUTPUT_ENABLED && ( - - - - - - )} + + + + + {(route: { match: { params: { outputId: string } } }) => { const output = outputs.data?.items.find((o) => route.match.params.outputId === o.id); diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 9bfcffa04bf35d..7ba2d3f194eeb5 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -267,9 +267,13 @@ export const installPackageFromRegistryHandler: FleetRequestHandler< force: request.body?.force, ignoreConstraints: request.body?.ignore_constraints, }); + if (!res.error) { const body: InstallPackageResponse = { items: res.assets || [], + _meta: { + install_source: res.installSource, + }, }; return response.ok({ body }); } else { @@ -342,6 +346,9 @@ export const installPackageByUploadHandler: FleetRequestHandler< const body: InstallPackageResponse = { items: res.assets || [], response: res.assets || [], + _meta: { + install_source: res.installSource, + }, }; return response.ok({ body }); } else { diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index 95aadf1b8555ad..544ab8b288cb40 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -242,7 +242,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => { response ); if (resp.payload?.items) { - return response.ok({ body: { response: resp.payload.items } }); + return response.ok({ body: { ...resp.payload, response: resp.payload.items } }); } return resp; } diff --git a/x-pack/plugins/fleet/server/services/agent_policies/index.ts b/x-pack/plugins/fleet/server/services/agent_policies/index.ts index b793ed26a08b5b..2e1fffdec1147d 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/index.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/index.ts @@ -6,3 +6,4 @@ */ export { getFullAgentPolicy } from './full_agent_policy'; +export { validateOutputForPolicy } from './validate_outputs_for_policy'; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/validate_outputs_for_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/validate_outputs_for_policy.test.ts new file mode 100644 index 00000000000000..ba5bc4a3aeeb2b --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/validate_outputs_for_policy.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { appContextService } from '..'; + +import { validateOutputForPolicy } from '.'; + +jest.mock('../app_context'); + +const mockedAppContextService = appContextService as jest.Mocked; + +function mockHasLicence(res: boolean) { + mockedAppContextService.getSecurityLicense.mockReturnValue({ + hasAtLeast: () => res, + } as any); +} + +describe('validateOutputForPolicy', () => { + describe('Without oldData (create)', () => { + it('should allow default outputs without platinum licence', async () => { + mockHasLicence(false); + await validateOutputForPolicy({ + data_output_id: null, + monitoring_output_id: null, + }); + }); + + it('should allow default outputs with platinum licence', async () => { + mockHasLicence(false); + await validateOutputForPolicy({ + data_output_id: null, + monitoring_output_id: null, + }); + }); + + it('should not allow custom data outputs without platinum licence', async () => { + mockHasLicence(false); + const res = validateOutputForPolicy({ + data_output_id: 'test1', + monitoring_output_id: null, + }); + await expect(res).rejects.toThrow( + 'Invalid licence to set per policy output, you need platinum licence' + ); + }); + + it('should not allow custom monitoring outputs without platinum licence', async () => { + mockHasLicence(false); + const res = validateOutputForPolicy({ + data_output_id: null, + monitoring_output_id: 'test1', + }); + await expect(res).rejects.toThrow( + 'Invalid licence to set per policy output, you need platinum licence' + ); + }); + + it('should allow custom data output with platinum licence', async () => { + mockHasLicence(true); + await validateOutputForPolicy({ + data_output_id: 'test1', + monitoring_output_id: null, + }); + }); + + it('should allow custom monitoring output with platinum licence', async () => { + mockHasLicence(true); + await validateOutputForPolicy({ + data_output_id: null, + monitoring_output_id: 'test1', + }); + }); + + it('should allow custom outputs for managed preconfigured policy without licence', async () => { + mockHasLicence(false); + await validateOutputForPolicy({ + is_managed: true, + is_preconfigured: true, + data_output_id: 'test1', + monitoring_output_id: 'test1', + }); + }); + }); + + describe('With oldData (update)', () => { + it('should allow default outputs without platinum licence', async () => { + mockHasLicence(false); + await validateOutputForPolicy( + { + data_output_id: null, + monitoring_output_id: null, + }, + { + data_output_id: 'test1', + monitoring_output_id: 'test1', + } + ); + }); + + it('should not allow custom data outputs without platinum licence', async () => { + mockHasLicence(false); + const res = validateOutputForPolicy( + { + data_output_id: 'test1', + monitoring_output_id: null, + }, + { + data_output_id: null, + monitoring_output_id: null, + } + ); + await expect(res).rejects.toThrow( + 'Invalid licence to set per policy output, you need platinum licence' + ); + }); + + it('should not allow custom monitoring outputs without platinum licence', async () => { + mockHasLicence(false); + const res = validateOutputForPolicy( + { + data_output_id: null, + monitoring_output_id: 'test1', + }, + { + data_output_id: null, + monitoring_output_id: null, + } + ); + await expect(res).rejects.toThrow( + 'Invalid licence to set per policy output, you need platinum licence' + ); + }); + + it('should allow custom data output with platinum licence', async () => { + mockHasLicence(true); + await validateOutputForPolicy( + { + data_output_id: 'test1', + monitoring_output_id: null, + }, + { + data_output_id: 'test1', + monitoring_output_id: null, + } + ); + }); + + it('should allow custom monitoring output with platinum licence', async () => { + mockHasLicence(true); + await validateOutputForPolicy({ + data_output_id: null, + monitoring_output_id: 'test1', + }); + }); + + it('should allow custom outputs for managed preconfigured policy without licence', async () => { + mockHasLicence(false); + await validateOutputForPolicy( + { + data_output_id: 'test1', + monitoring_output_id: 'test1', + }, + { is_managed: true, is_preconfigured: true } + ); + }); + + it('should allow custom outputs if they did not change without licence', async () => { + mockHasLicence(false); + await validateOutputForPolicy( + { + data_output_id: 'test1', + monitoring_output_id: 'test1', + }, + { data_output_id: 'test1', monitoring_output_id: 'test1' } + ); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/validate_outputs_for_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/validate_outputs_for_policy.ts new file mode 100644 index 00000000000000..272e1cd6c5b527 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/validate_outputs_for_policy.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AgentPolicySOAttributes } from '../../types'; +import { LICENCE_FOR_PER_POLICY_OUTPUT } from '../../../common'; +import { appContextService } from '..'; + +/** + * Validate outputs are valid for a policy using the current kibana licence or throw. + * @param data + * @returns + */ +export async function validateOutputForPolicy( + newData: Partial, + oldData: Partial = {} +) { + if ( + newData.data_output_id === oldData.data_output_id && + newData.monitoring_output_id === oldData.monitoring_output_id + ) { + return; + } + + const data = { ...oldData, ...newData }; + + if (!data.data_output_id && !data.monitoring_output_id) { + return; + } + + // Do not validate licence output for managed and preconfigured policy + if (data.is_managed && data.is_preconfigured) { + return; + } + + const hasLicence = appContextService + .getSecurityLicense() + .hasAtLeast(LICENCE_FOR_PER_POLICY_OUTPUT); + + if (!hasLicence) { + throw new Error( + `Invalid licence to set per policy output, you need ${LICENCE_FOR_PER_POLICY_OUTPUT} licence` + ); + } +} diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 50586badbe0c80..1784ff190385d7 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -63,6 +63,7 @@ import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object'; import { appContextService } from './app_context'; import { getFullAgentPolicy } from './agent_policies'; +import { validateOutputForPolicy } from './agent_policies'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; @@ -99,6 +100,8 @@ class AgentPolicyService { ); } + await validateOutputForPolicy(agentPolicy); + await soClient.update(SAVED_OBJECT_TYPE, id, { ...agentPolicy, ...(options.bumpRevision ? { revision: oldAgentPolicy.revision + 1 } : {}), @@ -169,6 +172,8 @@ class AgentPolicyService { ): Promise { await this.requireUniqueName(soClient, agentPolicy); + await validateOutputForPolicy(agentPolicy); + const newSo = await soClient.create( SAVED_OBJECT_TYPE, { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bundled_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bundled_packages.ts index 8ccd2006ad846d..77ece9e1d77877 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/bundled_packages.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/bundled_packages.ts @@ -12,7 +12,7 @@ import type { BundledPackage } from '../../../types'; import { appContextService } from '../../app_context'; import { splitPkgKey } from '../registry'; -const BUNDLED_PACKAGE_DIRECTORY = path.join(__dirname, '../../../bundled_packages'); +const BUNDLED_PACKAGE_DIRECTORY = path.join(__dirname, '../../../../target/bundled_packages'); export async function getBundledPackages(): Promise { try { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts index 1a1f1aa617f540..c803b0ff18a44b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts @@ -83,6 +83,7 @@ describe('install', () => { .mockImplementation(() => Promise.resolve({ packageInfo: { license: 'basic' } } as any)); mockGetBundledPackages.mockReset(); + (install._installPackage as jest.Mock).mockClear(); }); describe('registry', () => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 107b906a969c82..23883f90d42480 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -276,6 +276,7 @@ async function installPackageFromRegistry({ ], status: 'already_installed', installType, + installSource: 'registry', }; } } @@ -307,7 +308,7 @@ async function installPackageFromRegistry({ ...telemetryEvent, errorMessage: err.message, }); - return { error: err, installType }; + return { error: err, installType, installSource: 'registry' }; } const savedObjectsImporter = appContextService @@ -338,7 +339,7 @@ async function installPackageFromRegistry({ ...telemetryEvent, status: 'success', }); - return { assets, status: 'installed', installType }; + return { assets, status: 'installed', installType, installSource: 'registry' }; }) .catch(async (err: Error) => { logger.warn(`Failure to install package [${pkgName}]: [${err.toString()}]`); @@ -355,7 +356,7 @@ async function installPackageFromRegistry({ ...telemetryEvent, errorMessage: err.message, }); - return { error: err, installType }; + return { error: err, installType, installSource: 'registry' }; }); } catch (e) { sendEvent({ @@ -365,6 +366,7 @@ async function installPackageFromRegistry({ return { error: e, installType, + installSource: 'registry', }; } } @@ -454,7 +456,7 @@ async function installPackageByUpload({ ...telemetryEvent, errorMessage: e.message, }); - return { error: e, installType }; + return { error: e, installType, installSource: 'upload' }; } } @@ -463,9 +465,10 @@ export type InstallPackageParams = { } & ( | ({ installSource: Extract } & InstallRegistryPackageParams) | ({ installSource: Extract } & InstallUploadedArchiveParams) + | ({ installSource: Extract } & InstallUploadedArchiveParams) ); -export async function installPackage(args: InstallPackageParams) { +export async function installPackage(args: InstallPackageParams): Promise { if (!('installSource' in args)) { throw new Error('installSource is required'); } @@ -487,7 +490,7 @@ export async function installPackage(args: InstallPackageParams) { `found bundled package for requested install of ${pkgkey} - installing from bundled package archive` ); - const response = installPackageByUpload({ + const response = await installPackageByUpload({ savedObjectsClient, esClient, archiveBuffer: matchingBundledPackage.buffer, @@ -495,11 +498,11 @@ export async function installPackage(args: InstallPackageParams) { spaceId, }); - return response; + return { ...response, installSource: 'bundled' }; } logger.debug(`kicking off install of ${pkgkey} from registry`); - const response = installPackageFromRegistry({ + const response = await installPackageFromRegistry({ savedObjectsClient, pkgkey, esClient, @@ -510,7 +513,7 @@ export async function installPackage(args: InstallPackageParams) { return response; } else if (args.installSource === 'upload') { const { archiveBuffer, contentType, spaceId } = args; - const response = installPackageByUpload({ + const response = await installPackageByUpload({ savedObjectsClient, esClient, archiveBuffer, @@ -519,7 +522,6 @@ export async function installPackage(args: InstallPackageParams) { }); return response; } - // @ts-expect-error s/b impossibe b/c `never` by this point, but just in case throw new Error(`Unknown installSource: ${args.installSource}`); } diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 518b79b9e8547e..2a6e2355808117 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -143,6 +143,7 @@ jest.mock('./epm/packages/install', () => ({ return { error: new Error(installError), installType: 'install', + installSource: 'registry', }; } @@ -157,6 +158,7 @@ jest.mock('./epm/packages/install', () => ({ return { status: 'installed', installType: 'install', + installSource: 'registry', }; } else if (args.installSource === 'upload') { const { archiveBuffer } = args; @@ -168,7 +170,7 @@ jest.mock('./epm/packages/install', () => ({ const packageInstallation = { name: pkgName, version: '1.0.0', title: pkgName }; mockInstalledPackages.set(pkgName, packageInstallation); - return { status: 'installed', installType: 'install' }; + return { status: 'installed', installType: 'install', installSource: 'upload' }; } }, ensurePackagesCompletedInstall() { diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index d15d73fca73320..38d4b887227437 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -32,6 +32,8 @@ export const AgentPolicyBaseSchema = { schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)]) ) ), + data_output_id: schema.maybe(schema.nullable(schema.string())), + monitoring_output_id: schema.maybe(schema.nullable(schema.string())), }; export const NewAgentPolicySchema = schema.object({ diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx index 3a1c2dd36812f6..89f7d4795429a5 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx @@ -25,11 +25,13 @@ import { init as initHttp } from '../public/application/services/http'; import { init as initUiMetric } from '../public/application/services/ui_metric'; import { KibanaContextProvider } from '../public/shared_imports'; import { PolicyListContextProvider } from '../public/application/sections/policy_list/policy_list_context'; +import { executionContextServiceMock } from 'src/core/public/execution_context/execution_context_service.mock'; initHttp( new HttpService().setup({ injectedMetadata: injectedMetadataServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), + executionContext: executionContextServiceMock.createSetupContract(), }) ); initUiMetric(usageCollectionPluginMock.createSetupContract()); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts index 11811c7b7d26cd..b14dd17d3c11c9 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts @@ -74,6 +74,7 @@ async function fetchTemplates( const response = isLegacy ? await client.indices.getTemplate({}, options) : await client.indices.getIndexTemplate({}, options); + // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns return response; } diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/register_get_route.ts index 039eb24f4d9d6b..9e54e80c714849 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/register_get_route.ts @@ -36,6 +36,7 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep const body = componentTemplates.map((componentTemplate: ComponentTemplateFromEs) => { const deserializedComponentTemplateListItem = deserializeComponentTemplateList( componentTemplate, + // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns indexTemplates ); return deserializedComponentTemplateListItem; @@ -70,6 +71,7 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep await client.asCurrentUser.indices.getIndexTemplate(); return response.ok({ + // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns body: deserializeComponentTemplate(componentTemplates[0], indexTemplates), }); } catch (error) { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 8eedcee590fd58..93d65e162da710 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -34,6 +34,7 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep legacyTemplatesEs, cloudManagedTemplatePrefix ); + // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); const body = { @@ -92,6 +93,7 @@ export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDep if (indexTemplates.length > 0) { return response.ok({ body: deserializeTemplate( + // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns { ...indexTemplates[0].index_template, name }, cloudManagedTemplatePrefix ), diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 2a87c9cbca994f..3e1be0196231a4 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -83,7 +83,9 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { series: panel.series.map((series) => { return { id: series.id, - label: series.label, + // In case of grouping by multiple fields, "series.label" is array. + // If infra will perform this type of grouping, the following code needs to be updated + label: [series.label].flat()[0], data: series.data.map((point) => ({ timestamp: point[0] as number, value: point[1] as number | null, diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 3660c3d3db0cbf..5ef9e05cf590b0 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -13,7 +13,7 @@ import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../../../../src/plugins/kibana_utils/public'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useExecutionContext, useKibana } from '../../../../../src/plugins/kibana_react/public'; import { OnSaveProps } from '../../../../../src/plugins/saved_objects/public'; import { syncQueryStateWithUrl } from '../../../../../src/plugins/data/public'; import { LensAppProps, LensAppServices } from './types'; @@ -71,6 +71,7 @@ export function App({ getOriginatingAppName, spaces, http, + executionContext, // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag, } = lensAppServices; @@ -111,6 +112,7 @@ export function App({ undefined ); const [isGoBackToVizEditorModalVisible, setIsGoBackToVizEditorModalVisible] = useState(false); + const savedObjectId = (initialInput as LensByReferenceInput)?.savedObjectId; useEffect(() => { if (currentDoc) { @@ -122,6 +124,12 @@ export function App({ setIndicateNoData(true); }, [setIndicateNoData]); + useExecutionContext(executionContext, { + type: 'application', + id: savedObjectId || 'new', + page: 'editor', + }); + useEffect(() => { if (indicateNoData) { setIndicateNoData(false); @@ -132,11 +140,9 @@ export function App({ () => Boolean( // Temporarily required until the 'by value' paradigm is default. - dashboardFeatureFlag.allowByValueEmbeddables && - isLinkedToOriginatingApp && - !(initialInput as LensByReferenceInput)?.savedObjectId + dashboardFeatureFlag.allowByValueEmbeddables && isLinkedToOriginatingApp && !savedObjectId ), - [dashboardFeatureFlag.allowByValueEmbeddables, isLinkedToOriginatingApp, initialInput] + [dashboardFeatureFlag.allowByValueEmbeddables, isLinkedToOriginatingApp, savedObjectId] ); useEffect(() => { diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 28db5e9f4c43a3..6f2fd4e8026ad9 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -77,6 +77,7 @@ export async function getLensServices( usageCollection, savedObjectsTagging, attributeService, + executionContext: coreStart.executionContext, http: coreStart.http, chrome: coreStart.chrome, overlays: coreStart.overlays, diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index bdd7bebd991e78..25fff038c4814e 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -12,6 +12,7 @@ import type { ApplicationStart, AppMountParameters, ChromeStart, + ExecutionContextStart, HttpStart, IUiSettingsClient, NotificationsStart, @@ -114,6 +115,7 @@ export interface HistoryLocationState { export interface LensAppServices { http: HttpStart; + executionContext: ExecutionContextStart; chrome: ChromeStart; overlays: OverlayStart; storage: IStorageWrapper; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index c25b8b72640776..72639f3582583a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1604,7 +1604,7 @@ describe('IndexPattern Data Source suggestions', () => { const updatedContext = [ { ...context[0], - splitField: 'source', + splitFields: ['source'], splitMode: 'terms', termsParams: { size: 10, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 0e6fbf02a491ef..8e4017a583a910 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -181,10 +181,16 @@ function createNewTimeseriesLayerWithMetricAggregationFromVizEditor( ): IndexPatternLayer | undefined { const { timeFieldName, splitMode, splitFilters, metrics, timeInterval } = layer; const dateField = indexPattern.getFieldByName(timeFieldName!); - const splitField = layer.splitField ? indexPattern.getFieldByName(layer.splitField) : null; + + const splitFields = layer.splitFields + ? (layer.splitFields + .map((item) => indexPattern.getFieldByName(item)) + .filter(Boolean) as IndexPatternField[]) + : null; + // generate the layer for split by terms - if (splitMode === 'terms' && splitField) { - return getSplitByTermsLayer(indexPattern, splitField, dateField, layer); + if (splitMode === 'terms' && splitFields?.length) { + return getSplitByTermsLayer(indexPattern, splitFields, dateField, layer); // generate the layer for split by filters } else if (splitMode?.includes('filter') && splitFilters && splitFilters.length) { return getSplitByFiltersLayer(indexPattern, dateField, layer); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 78129cc8c12330..6f2a2acf3edf0c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -322,11 +322,13 @@ export const termsOperation: OperationDefinition { }) ); }); + + it('should preserve custom label when set by the user', () => { + const updateLayerSpy = jest.fn(); + const existingFields = getExistingFields(); + const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields); + + layer.columns.col1 = { + label: 'MyCustomLabel', + customLabel: true, + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + secondaryFields: ['geo.src'], + }, + sourceField: 'source', + } as TermsIndexPatternColumn; + let instance = mount( + + ); + // add a new field + act(() => { + instance.find('[data-test-subj="indexPattern-terms-add-field"]').first().simulate('click'); + }); + instance = instance.update(); + + act(() => { + instance.find(EuiComboBox).last().prop('onChange')!([ + { value: { type: 'field', field: 'bytes' }, label: 'bytes' }, + ]); + }); + + expect(updateLayerSpy).toHaveBeenCalledWith( + expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + label: 'MyCustomLabel', + }), + }), + }) + ); + }); }); describe('param editor', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index ab7ee8992f2fe8..5f51b531231704 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -1672,12 +1672,13 @@ export function computeLayerFromContext( export function getSplitByTermsLayer( indexPattern: IndexPattern, - splitField: IndexPatternField, + splitFields: IndexPatternField[], dateField: IndexPatternField | undefined, layer: VisualizeEditorLayersContext ): IndexPatternLayer { const { termsParams, metrics, timeInterval, splitWithDateHistogram } = layer; const copyMetricsArray = [...metrics]; + const computedLayer = computeLayerFromContext( metrics.length === 1, copyMetricsArray, @@ -1686,7 +1687,9 @@ export function getSplitByTermsLayer( layer.label ); + const [baseField, ...secondaryFields] = splitFields; const columnId = generateId(); + let termsLayer = insertNewColumn({ op: splitWithDateHistogram ? 'date_histogram' : 'terms', layer: insertNewColumn({ @@ -1701,10 +1704,22 @@ export function getSplitByTermsLayer( }, }), columnId, - field: splitField, + field: baseField, indexPattern, visualizationGroups: [], }); + + if (secondaryFields.length) { + termsLayer = updateColumnParam({ + layer: termsLayer, + columnId, + paramName: 'secondaryFields', + value: secondaryFields.map((i) => i.name), + }); + + termsLayer = updateDefaultLabels(termsLayer, indexPattern); + } + const termsColumnParams = termsParams as TermsIndexPatternColumn['params']; if (termsColumnParams) { for (const [param, value] of Object.entries(termsColumnParams)) { diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index 9fa6d61370a17c..6681811744da42 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -112,6 +112,7 @@ export function makeDefaultServices( chrome: core.chrome, overlays: core.overlays, uiSettings: core.uiSettings, + executionContext: core.executionContext, navigation: navigationStartMock, notifications: core.notifications, attributeService: makeAttributeService(), diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss b/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss index a11e3373df4679..c06f13dfc2eb11 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss @@ -1,3 +1,3 @@ .lnsVisToolbar__popover { - width: 365px; + width: 404px; } diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index e1e2ba75b50c40..2c567254384219 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -59,6 +59,7 @@ export function getColorAssignments( } const splitAccessor = layer.splitAccessor; const column = data.tables[layer.layerId]?.columns.find(({ id }) => id === splitAccessor); + const columnFormatter = column && formatFactory(column.meta.params); const splits = !column || !data.tables[layer.layerId] ? [] @@ -66,7 +67,7 @@ export function getColorAssignments( data.tables[layer.layerId].rows.map((row) => { let value = row[splitAccessor]; if (value && !isPrimitive(value)) { - value = formatFactory(column.meta.params).convert(value); + value = columnFormatter?.convert(value) ?? value; } else { value = String(value); } diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index ea0e336ff2f08a..fb8ad127a394ef 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -42,11 +42,13 @@ import type { ExpressionRenderDefinition, Datatable, DatatableRow, + DatatableColumn, } from 'src/plugins/expressions/public'; import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RenderMode } from 'src/plugins/expressions'; import { ThemeServiceStart } from 'kibana/public'; +import { FieldFormat } from 'src/plugins/field_formats/common'; import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { ILensInterpreterRenderHandlers, LensFilterEvent, LensBrushEvent } from '../types'; @@ -599,6 +601,7 @@ export function XYChart({ : undefined, }, }; + return ( (); + for (const column of table.columns) { + formatterPerColumn.set(column, formatFactory(column.meta.params)); + } + // what if row values are not primitive? That is the case of, for instance, Ranges // remaps them to their serialized version with the formatHint metadata // In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on @@ -744,7 +752,7 @@ export function XYChart({ // pre-format values for ordinal x axes because there can only be a single x axis formatter on chart level (!isPrimitive(record) || (column.id === xAccessor && xScaleType === 'ordinal')) ) { - newRow[column.id] = formatFactory(column.meta.params).convert(record); + newRow[column.id] = formatterPerColumn.get(column)!.convert(record); } } return newRow; @@ -798,6 +806,8 @@ export function XYChart({ ); const formatter = table?.columns.find((column) => column.id === accessor)?.meta?.params; + const splitHint = table.columns.find((col) => col.id === splitAccessor)?.meta?.params; + const splitFormatter = formatFactory(splitHint); const seriesProps: SeriesSpec = { splitSeriesAccessors: splitAccessor ? [splitAccessor] : [], @@ -857,8 +867,6 @@ export function XYChart({ }, }, name(d) { - const splitHint = table.columns.find((col) => col.id === splitAccessor)?.meta?.params; - // For multiple y series, the name of the operation is used on each, either: // * Key - Y name // * Formatted value - Y name @@ -871,7 +879,7 @@ export function XYChart({ splitAccessor && !layersAlreadyFormatted[splitAccessor] ) { - return formatFactory(splitHint).convert(key); + return splitFormatter.convert(key); } return splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? ''; }) @@ -885,7 +893,7 @@ export function XYChart({ if (splitAccessor && layersAlreadyFormatted[splitAccessor]) { return d.seriesKeys[0]; } - return formatFactory(splitHint).convert(d.seriesKeys[0]); + return splitFormatter.convert(d.seriesKeys[0]); } // This handles both split and single-y cases: // * If split series without formatting, show the value literally diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx index f0c59cc613c76e..0f842bb53b99e7 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx @@ -236,6 +236,7 @@ Default.args = { errorExists: false, exceptionItems: [], exceptionsToDelete: [], + warningExists: false, }), ruleName: 'My awesome rule', }; @@ -288,6 +289,7 @@ SingleExceptionItem.args = { errorExists: false, exceptionItems: [sampleExceptionItem], exceptionsToDelete: [], + warningExists: false, }), ruleName: 'My awesome rule', }; @@ -313,6 +315,7 @@ MultiExceptionItems.args = { errorExists: false, exceptionItems: [sampleExceptionItem, sampleExceptionItem], exceptionsToDelete: [], + warningExists: false, }), ruleName: 'My awesome rule', }; @@ -338,6 +341,7 @@ WithNestedExceptionItem.args = { errorExists: false, exceptionItems: [sampleNestedExceptionItem, sampleExceptionItem], exceptionsToDelete: [], + warningExists: false, }), ruleName: 'My awesome rule', }; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx index 1ac35608f884a9..aa7071a9074a96 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx @@ -18,7 +18,9 @@ import { isNotOperator, isOneOfOperator, isOperator, + matchesOperator, } from '@kbn/securitysolution-list-utils'; +import { validateFilePathInput } from '@kbn/securitysolution-utils'; import { useFindLists } from '@kbn/securitysolution-list-hooks'; import type { FieldSpec } from 'src/plugins/data/common'; @@ -30,6 +32,7 @@ import { getFoundListSchemaMock } from '../../../../common/schemas/response/foun import { BuilderEntryItem } from './entry_renderer'; jest.mock('@kbn/securitysolution-list-hooks'); +jest.mock('@kbn/securitysolution-utils'); const mockKibanaHttpService = coreMock.createStart().http; const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); @@ -74,6 +77,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={true} /> ); @@ -104,6 +108,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -138,6 +143,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -174,6 +180,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -210,6 +217,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -247,6 +255,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={true} /> ); @@ -284,6 +293,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={true} /> ); @@ -320,6 +330,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -357,6 +368,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -414,6 +426,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -456,6 +469,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={mockOnChange} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -496,6 +510,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={mockOnChange} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -536,6 +551,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={mockOnChange} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -576,6 +592,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={mockOnChange} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -616,6 +633,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={mockOnChange} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -662,6 +680,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={mockSetErrorExists} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -701,6 +720,7 @@ describe('BuilderEntryItem', () => { listType="detection" onChange={jest.fn()} setErrorsExist={mockSetErrorExists} + setWarningsExist={jest.fn()} showLabel={false} /> ); @@ -723,6 +743,104 @@ describe('BuilderEntryItem', () => { expect(mockSetErrorExists).toHaveBeenCalledWith(true); }); + test('it invokes "setWarningsExist" when invalid value in field value input', async () => { + const mockSetWarningsExists = jest.fn(); + + (validateFilePathInput as jest.Mock).mockReturnValue('some warning message'); + wrapper = mount( + + ); + + await waitFor(() => { + ( + wrapper.find(EuiComboBox).at(2).props() as unknown as { + onBlur: () => void; + } + ).onBlur(); + + // Invalid input because field is just a string and not a path + ( + wrapper.find(EuiComboBox).at(2).props() as unknown as { + onSearchChange: (arg: string) => void; + } + ).onSearchChange('i243kjhfew'); + }); + + expect(mockSetWarningsExists).toHaveBeenCalledWith(true); + }); + + test('it does not invoke "setWarningsExist" when valid value in field value input', async () => { + const mockSetWarningsExists = jest.fn(); + + (validateFilePathInput as jest.Mock).mockReturnValue(undefined); + wrapper = mount( + + ); + + await waitFor(() => { + ( + wrapper.find(EuiComboBox).at(2).props() as unknown as { + onBlur: () => void; + } + ).onBlur(); + + // valid input as it is a path + ( + wrapper.find(EuiComboBox).at(2).props() as unknown as { + onSearchChange: (arg: string) => void; + } + ).onSearchChange('c:\\path.exe'); + }); + + expect(mockSetWarningsExists).toHaveBeenCalledWith(false); + }); + test('it disabled field inputs correctly when passed "isDisabled=true"', () => { wrapper = mount( { listType="detection" onChange={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} osTypes={['windows']} showLabel={false} isDisabled={true} diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 206b1a5dd6f85f..aa24ec6611b975 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -24,6 +24,7 @@ import { getEntryOnMatchAnyChange, getEntryOnMatchChange, getEntryOnOperatorChange, + getEntryOnWildcardChange, getFilteredIndexPatterns, getOperatorOptions, } from '@kbn/securitysolution-list-utils'; @@ -32,9 +33,11 @@ import { AutocompleteFieldListsComponent, AutocompleteFieldMatchAnyComponent, AutocompleteFieldMatchComponent, + AutocompleteFieldWildcardComponent, FieldComponent, OperatorComponent, } from '@kbn/securitysolution-autocomplete'; +import { OperatingSystem, validateFilePathInput } from '@kbn/securitysolution-utils'; import { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; import type { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; @@ -64,6 +67,7 @@ export interface EntryItemProps { onChange: (arg: BuilderEntry, i: number) => void; onlyShowListOperators?: boolean; setErrorsExist: (arg: boolean) => void; + setWarningsExist: (arg: boolean) => void; isDisabled?: boolean; operatorsList?: OperatorOption[]; } @@ -80,6 +84,7 @@ export const BuilderEntryItem: React.FC = ({ onChange, onlyShowListOperators = false, setErrorsExist, + setWarningsExist, showLabel, isDisabled = false, operatorsList, @@ -90,6 +95,12 @@ export const BuilderEntryItem: React.FC = ({ }, [setErrorsExist] ); + const handleWarning = useCallback( + (warn: boolean): void => { + setWarningsExist(warn); + }, + [setWarningsExist] + ); const handleFieldChange = useCallback( ([newField]: DataViewFieldBase[]): void => { @@ -126,6 +137,15 @@ export const BuilderEntryItem: React.FC = ({ [onChange, entry] ); + const handleFieldWildcardValueChange = useCallback( + (newField: string): void => { + const { updatedEntry, index } = getEntryOnWildcardChange(entry, newField); + + onChange(updatedEntry, index); + }, + [onChange, entry] + ); + const handleFieldListValueChange = useCallback( (newField: ListSchema): void => { const { updatedEntry, index } = getEntryOnListChange(entry, newField); @@ -199,8 +219,17 @@ export const BuilderEntryItem: React.FC = ({ ); const renderOperatorInput = (isFirst: boolean): JSX.Element => { - const operatorOptions = operatorsList - ? operatorsList + // for event filters forms + // show extra operators for wildcards when field is `file.path.text` + const isFilePathTextField = entry.field !== undefined && entry.field.name === 'file.path.text'; + const isEventFilterList = listType === 'endpoint_events'; + const augmentedOperatorsList = + operatorsList && isFilePathTextField && isEventFilterList + ? operatorsList + : operatorsList?.filter((operator) => operator.type !== OperatorTypeEnum.WILDCARD); + + const operatorOptions = augmentedOperatorsList + ? augmentedOperatorsList : onlyShowListOperators ? EXCEPTION_OPERATORS_ONLY_LISTS : getOperatorOptions( @@ -209,6 +238,7 @@ export const BuilderEntryItem: React.FC = ({ entry.field != null && entry.field.type === 'boolean', isFirst && allowLargeValueLists ); + const comboBox = ( = ({ data-test-subj="exceptionBuilderEntryFieldMatchAny" /> ); + case OperatorTypeEnum.WILDCARD: + const wildcardValue = typeof entry.value === 'string' ? entry.value : undefined; + let os: OperatingSystem = OperatingSystem.WINDOWS; + if (osTypes) { + [os] = osTypes as OperatingSystem[]; + } + const warning = validateFilePathInput({ os, value: wildcardValue }); + return ( + + ); case OperatorTypeEnum.LIST: const id = typeof entry.value === 'string' ? entry.value : undefined; return ( diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx index ccda52e2805861..fed24ba428e6c3 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx @@ -53,6 +53,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); @@ -84,6 +85,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); @@ -113,6 +115,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); @@ -144,6 +147,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); @@ -182,6 +186,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); @@ -212,6 +217,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); @@ -243,6 +249,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); @@ -272,6 +279,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); @@ -303,6 +311,7 @@ describe('BuilderExceptionListItemComponent', () => { onChangeExceptionItem={jest.fn()} onDeleteExceptionItem={mockOnDeleteExceptionItem} setErrorsExist={jest.fn()} + setWarningsExist={jest.fn()} /> ); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 931a8356e93be3..febfa54a482b2a 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -59,6 +59,7 @@ interface BuilderExceptionListItemProps { onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; setErrorsExist: (arg: boolean) => void; + setWarningsExist: (arg: boolean) => void; onlyShowListOperators?: boolean; isDisabled?: boolean; operatorsList?: OperatorOption[]; @@ -80,6 +81,7 @@ export const BuilderExceptionListItemComponent = React.memo - indexPattern != null && exceptionItem.entries.length > 0 - ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) - : [], - [exceptionItem.entries, indexPattern] - ); + const entries = useMemo((): FormattedBuilderEntry[] => { + const hasIndexPatternAndEntries = indexPattern != null && exceptionItem.entries.length > 0; + return hasIndexPatternAndEntries + ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) + : []; + }, [exceptionItem.entries, indexPattern]); return ( @@ -150,6 +150,7 @@ export const BuilderExceptionListItemComponent = React.memo; exceptionsToDelete: ExceptionListItemSchema[]; + warningExists: boolean; } export interface ExceptionBuilderProps { @@ -123,6 +125,7 @@ export const ExceptionBuilderComponent = ({ disableNested, disableOr, errorExists, + warningExists, exceptions, exceptionsToDelete, }, @@ -144,6 +147,16 @@ export const ExceptionBuilderComponent = ({ [dispatch] ); + const setWarningsExist = useCallback( + (hasWarnings: boolean): void => { + dispatch({ + type: 'setWarningsExist', + warningExists: hasWarnings, + }); + }, + [dispatch] + ); + const setUpdateExceptions = useCallback( (items: ExceptionsBuilderExceptionItem[]): void => { dispatch({ @@ -350,8 +363,9 @@ export const ExceptionBuilderComponent = ({ errorExists: errorExists > 0, exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete, + warningExists: warningExists > 0, }); - }, [onChange, exceptionsToDelete, exceptions, errorExists]); + }, [onChange, exceptionsToDelete, exceptions, errorExists, warningExists]); useEffect(() => { setUpdateExceptions([]); @@ -416,6 +430,7 @@ export const ExceptionBuilderComponent = ({ onDeleteExceptionItem={handleDeleteExceptionItem} onlyShowListOperators={containsValueListEntry(exceptions)} setErrorsExist={setErrorsExist} + setWarningsExist={setWarningsExist} osTypes={osTypes} isDisabled={isDisabled} operatorsList={operatorsList} diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts index 4ace0c7d31ef8d..ba3b77fb24ed14 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts @@ -25,6 +25,7 @@ export interface State { exceptions: ExceptionsBuilderExceptionItem[]; exceptionsToDelete: ExceptionListItemSchema[]; errorExists: number; + warningExists: number; } export type Action = @@ -56,6 +57,10 @@ export type Action = | { type: 'setErrorsExist'; errorExists: boolean; + } + | { + type: 'setWarningsExist'; + warningExists: boolean; }; export const exceptionsBuilderReducer = @@ -128,6 +133,15 @@ export const exceptionsBuilderReducer = errorExists: errTotal < 0 ? 0 : errTotal, }; } + case 'setWarningsExist': { + const { warningExists } = state; + const warnTotal = action.warningExists ? warningExists + 1 : warningExists - 1; + + return { + ...state, + warningExists: warnTotal < 0 ? 0 : warnTotal, + }; + } default: return state; } diff --git a/x-pack/plugins/maps/common/execution_context.test.ts b/x-pack/plugins/maps/common/execution_context.test.ts new file mode 100644 index 00000000000000..25e3813a7e8f01 --- /dev/null +++ b/x-pack/plugins/maps/common/execution_context.test.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 { makeExecutionContext } from './execution_context'; + +describe('makeExecutionContext', () => { + test('returns basic fields if nothing is provided', () => { + const context = makeExecutionContext({}); + expect(context).toStrictEqual({ + name: 'maps', + type: 'application', + }); + }); + + test('merges in context', () => { + const context = makeExecutionContext({ id: '123' }); + expect(context).toStrictEqual({ + name: 'maps', + type: 'application', + id: '123', + }); + }); + + test('omits undefined values', () => { + const context = makeExecutionContext({ id: '123', description: undefined }); + expect(context).toStrictEqual({ + name: 'maps', + type: 'application', + id: '123', + }); + }); +}); diff --git a/x-pack/plugins/maps/common/execution_context.ts b/x-pack/plugins/maps/common/execution_context.ts index 23de29cfa8cd7d..4a11eb5d890295 100644 --- a/x-pack/plugins/maps/common/execution_context.ts +++ b/x-pack/plugins/maps/common/execution_context.ts @@ -5,14 +5,16 @@ * 2.0. */ +import { isUndefined, omitBy } from 'lodash'; import { APP_ID } from './constants'; -export function makeExecutionContext(id: string, url: string, description?: string) { - return { - name: APP_ID, - type: 'application', - id, - description: description || '', - url, - }; +export function makeExecutionContext(context: { id?: string; url?: string; description?: string }) { + return omitBy( + { + name: APP_ID, + type: 'application', + ...context, + }, + isUndefined + ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index e2f9959b25d31d..a26bd341613b2d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -5,8 +5,14 @@ * 2.0. */ +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { MapExtent, VectorSourceRequestMeta } from '../../../../common/descriptor_types'; -import { getHttp, getIndexPatternService, getSearchService } from '../../../kibana_services'; +import { + getExecutionContext, + getHttp, + getIndexPatternService, + getSearchService, +} from '../../../kibana_services'; import { ESGeoGridSource } from './es_geo_grid_source'; import { ES_GEO_FIELD_TYPE, @@ -129,6 +135,13 @@ describe('ESGeoGridSource', () => { }, }, }); + + const coreStartMock = coreMock.createStart(); + coreStartMock.executionContext.get.mockReturnValue({ + name: 'some-app', + }); + // @ts-expect-error + getExecutionContext.mockReturnValue(coreStartMock.executionContext); }); afterEach(() => { diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 54e4964b35f03f..d8197902c73ace 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -33,6 +33,7 @@ export const getUiSettings = () => coreStart.uiSettings; export const getIsDarkMode = () => getUiSettings().get('theme:darkMode', false); export const getIndexPatternSelectComponent = () => pluginsStart.data.ui.IndexPatternSelect; export const getHttp = () => coreStart.http; +export const getExecutionContext = () => coreStart.executionContext; export const getTimeFilter = () => pluginsStart.data.query.timefilter.timefilter; export const getToasts = () => coreStart.notifications.toasts; export const getSavedObjectsClient = () => coreStart.savedObjects.client; diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index 571cba64a06c45..dab284b0b71e46 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -17,6 +17,7 @@ import { getMapsCapabilities, getToasts, getCoreChrome, + getExecutionContext, getNavigateToApp, getSavedObjectsClient, getSavedObjectsTagging, @@ -121,6 +122,12 @@ async function deleteMaps(items: object[]) { } export function MapsListView() { + getExecutionContext().set({ + type: 'application', + page: 'list', + id: '', + }); + const isReadOnly = !getMapsCapabilities().save; getCoreChrome().docTitle.change(getAppTitle()); diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 9aede248e1877d..a341246f748f3a 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -16,6 +16,7 @@ import { type Filter, FilterStateStore } from '@kbn/es-query'; import type { Query, TimeRange, DataView } from 'src/plugins/data/common'; import { getData, + getExecutionContext, getCoreChrome, getMapsCapabilities, getNavigation, @@ -115,6 +116,12 @@ export class MapApp extends React.Component { componentDidMount() { this._isMounted = true; + getExecutionContext().set({ + type: 'application', + page: 'editor', + id: this.props.savedMap.getSavedObjectId() || 'new', + }); + this._autoRefreshSubscription = getTimeFilter() .getAutoRefreshFetch$() .pipe( diff --git a/x-pack/plugins/maps/public/util.test.js b/x-pack/plugins/maps/public/util.test.js index d8861063fc637c..7fc88578b378ae 100644 --- a/x-pack/plugins/maps/public/util.test.js +++ b/x-pack/plugins/maps/public/util.test.js @@ -5,7 +5,7 @@ * 2.0. */ -import { getGlyphUrl } from './util'; +import { getGlyphUrl, makePublicExecutionContext } from './util'; const MOCK_EMS_SETTINGS = { isEMSEnabled: () => true, @@ -62,3 +62,55 @@ describe('getGlyphUrl', () => { }); }); }); + +describe('makePublicExecutionContext', () => { + let injectedContext = {}; + beforeAll(() => { + require('./kibana_services').getExecutionContext = () => ({ + get: () => injectedContext, + }); + }); + + test('creates basic context when no top level context is provided', () => { + const context = makePublicExecutionContext('test'); + expect(context).toStrictEqual({ + description: 'test', + name: 'maps', + type: 'application', + url: '/', + }); + }); + + test('merges with top level context if its from the same app', () => { + injectedContext = { + name: 'maps', + id: '1234', + }; + const context = makePublicExecutionContext('test'); + expect(context).toStrictEqual({ + description: 'test', + name: 'maps', + type: 'application', + url: '/', + id: '1234', + }); + }); + + test('nests inside top level context if its from a different app', () => { + injectedContext = { + name: 'other-app', + id: '1234', + }; + const context = makePublicExecutionContext('test'); + expect(context).toStrictEqual({ + name: 'other-app', + id: '1234', + child: { + description: 'test', + type: 'application', + name: 'maps', + url: '/', + }, + }); + }); +}); diff --git a/x-pack/plugins/maps/public/util.ts b/x-pack/plugins/maps/public/util.ts index 4adb8b35bfcea2..66244ea5f67685 100644 --- a/x-pack/plugins/maps/public/util.ts +++ b/x-pack/plugins/maps/public/util.ts @@ -8,7 +8,13 @@ import { EMSClient, FileLayer, TMSService } from '@elastic/ems-client'; import type { KibanaExecutionContext } from 'kibana/public'; import { FONTS_API_PATH } from '../common/constants'; -import { getHttp, getTilemap, getEMSSettings, getMapsEmsStart } from './kibana_services'; +import { + getHttp, + getTilemap, + getEMSSettings, + getMapsEmsStart, + getExecutionContext, +} from './kibana_services'; import { getLicenseId } from './licensed_features'; import { makeExecutionContext } from '../common/execution_context'; @@ -67,9 +73,21 @@ export function isRetina(): boolean { return window.devicePixelRatio === 2; } -export function makePublicExecutionContext( - id: string, - description?: string -): KibanaExecutionContext { - return makeExecutionContext(id, window.location.pathname, description); +export function makePublicExecutionContext(description: string): KibanaExecutionContext { + const topLevelContext = getExecutionContext().get(); + const context = makeExecutionContext({ + url: window.location.pathname, + description, + }); + + // Distinguish between running in maps app vs. embedded + return topLevelContext.name !== undefined && topLevelContext.name !== context.name + ? { + ...topLevelContext, + child: context, + } + : { + ...topLevelContext, + ...context, + }; } diff --git a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts index 193a3d74e2dca2..28effa5eabfba8 100644 --- a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts @@ -56,7 +56,10 @@ export async function getEsGridTile({ }; const tile = await core.executionContext.withContext( - makeExecutionContext('mvt:get_grid_tile', url), + makeExecutionContext({ + description: 'mvt:get_grid_tile', + url, + }), async () => { return await context.core.elasticsearch.client.asCurrentUser.transport.request( { diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 2c8b6dd4b113d6..7e9bc01c5c317c 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -57,7 +57,10 @@ export async function getEsTile({ }; const tile = await core.executionContext.withContext( - makeExecutionContext('mvt:get_tile', url), + makeExecutionContext({ + description: 'mvt:get_tile', + url, + }), async () => { return await context.core.elasticsearch.client.asCurrentUser.transport.request( { diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 498a27834d050f..9305927618872f 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -230,7 +230,7 @@ export const RevertModelSnapshotFlyout: FC = ({ fadeChart={true} overlayRanges={[ { - start: currentSnapshot.latest_record_time_stamp, + start: currentSnapshot.latest_record_time_stamp!, end: job.data_counts.latest_record_timestamp!, color: '#ff0000', }, @@ -253,7 +253,7 @@ export const RevertModelSnapshotFlyout: FC = ({ @@ -333,7 +333,7 @@ export const RevertModelSnapshotFlyout: FC = ({ handleClusterStats(response)) @@ -42,7 +42,7 @@ export function getClustersStats(req: LegacyRequest, clusterUuid: string, ccs?: * @param {String} clusterUuid (optional) - if not undefined, getClusters filters for a single clusterUuid * @return {Promise} Object representing each cluster. */ -function fetchClusterStats(req: LegacyRequest, clusterUuid: string, ccs?: string) { +function fetchClusterStats(req: LegacyRequest, clusterUuid?: string, ccs?: string) { const dataset = 'cluster_stats'; const moduleType = 'elasticsearch'; const indexPattern = getNewIndexPatterns({ diff --git a/x-pack/plugins/monitoring/server/lib/create_query.ts b/x-pack/plugins/monitoring/server/lib/create_query.ts index 051b0ed6b4f9ce..55b96a7d369064 100644 --- a/x-pack/plugins/monitoring/server/lib/create_query.ts +++ b/x-pack/plugins/monitoring/server/lib/create_query.ts @@ -72,7 +72,7 @@ interface CreateQueryOptions { dsDataset?: string; metricset?: string; filters?: any[]; - clusterUuid: string; + clusterUuid?: string; uuid?: string; start?: number; end?: number; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts index 7673f1b7ff0521..3bd9f6d2265dc1 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts @@ -17,6 +17,8 @@ import { LegacyRequest } from '../../types'; * * @param req {Object} the server route handler request object */ + +// TODO: replace LegacyRequest with current request object + plugin retrieval export async function verifyMonitoringAuth(req: LegacyRequest) { const xpackInfo = get(req.server.plugins.monitoring, 'info'); @@ -38,6 +40,8 @@ export async function verifyMonitoringAuth(req: LegacyRequest) { * @param req {Object} the server route handler request object * @return {Promise} That either resolves with no response (void) or an exception. */ + +// TODO: replace LegacyRequest with current request object + plugin retrieval async function verifyHasPrivileges(req: LegacyRequest) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts index 7654ed551b63b4..91983186218c9a 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts @@ -15,7 +15,7 @@ import { Globals } from '../../static_globals'; interface GetLogstashPipelineIdsParams { req: LegacyRequest; - clusterUuid: string; + clusterUuid?: string; size: number; logstashUuid?: string; ccs?: string; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js rename to x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.js b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts similarity index 72% rename from x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.js rename to x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts index 84bea7ba2e8c4b..450872049a3deb 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts @@ -7,17 +7,20 @@ import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; import { handleError } from '../../../../lib/errors'; +import { LegacyRequest, LegacyServer } from '../../../../types'; /* * API for checking read privilege on Monitoring Data * Used for the "Access Denied" page as something to auto-retry with. */ -export function checkAccessRoute(server) { + +// TODO: Replace this LegacyServer call with the "new platform" core Kibana route method +export function checkAccessRoute(server: LegacyServer) { server.route({ method: 'GET', path: '/api/monitoring/v1/check_access', - handler: async (req) => { - const response = {}; + handler: async (req: LegacyRequest) => { + const response: { has_access?: boolean } = {}; try { await verifyMonitoringAuth(req); response.has_access = true; // response data is ignored diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.js rename to x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.js b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts similarity index 81% rename from x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.js rename to x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts index 2a1ec03f93db6e..81acd0e53f319f 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts @@ -6,16 +6,19 @@ */ import { schema } from '@kbn/config-schema'; +import { LegacyRequest, LegacyServer } from '../../../../types'; import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_from_request'; import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; import { handleError } from '../../../../lib/errors'; import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; -export function clustersRoute(server) { +export function clustersRoute(server: LegacyServer) { /* * Monitoring Home * Route Init (for checking license and compatibility for multi-cluster monitoring */ + + // TODO switch from the LegacyServer route() method to the "new platform" route methods server.route({ method: 'POST', path: '/api/monitoring/v1/clusters', @@ -30,7 +33,7 @@ export function clustersRoute(server) { }), }, }, - handler: async (req) => { + handler: async (req: LegacyRequest) => { let clusters = []; const config = server.config; @@ -43,7 +46,7 @@ export function clustersRoute(server) { filebeatIndexPattern: config.ui.logs.index, }); clusters = await getClustersFromRequest(req, indexPatterns, { - codePaths: req.payload.codePaths, + codePaths: req.payload.codePaths as string[], // TODO remove this cast when we can properly type req by using the right route handler }); } catch (err) { throw handleError(err, req); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.js rename to x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx index 3bac9a445f3fb9..03eaef8f6c8a68 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx @@ -65,6 +65,9 @@ import { parseAlert } from '../../components/parse_alert'; import { CoreStart } from '../../../../../../../../src/core/public'; import { translations, paths } from '../../../../config'; import { addDisplayNames } from './add_display_names'; +import { CaseAttachments, CasesUiStart } from '../../../../../../cases/public'; +import { CommentType } from '../../../../../../cases/common'; +import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from './translations'; const ALERT_TABLE_STATE_STORAGE_KEY = 'xpack.observability.alert.tableState'; @@ -146,9 +149,9 @@ function ObservabilityActions({ const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const [openActionsPopoverId, setActionsPopover] = useState(null); const { - timelines, + cases, application: {}, - } = useKibana().services; + } = useKibana().services; const parseObservabilityAlert = useMemo( () => parseAlert(observabilityRuleTypeRegistry), @@ -158,10 +161,6 @@ function ObservabilityActions({ const alert = parseObservabilityAlert(dataFieldEs); const { prepend } = core.http.basePath; - const afterCaseSelection = useCallback(() => { - setActionsPopover(null); - }, []); - const closeActionsPopover = useCallback(() => { setActionsPopover(null); }, []); @@ -171,35 +170,59 @@ function ObservabilityActions({ }, []); const casePermissions = useGetUserCasesPermissions(); - const event = useMemo(() => { - return { - data, - _id: eventId, - ecs: ecsData, - }; - }, [data, eventId, ecsData]); - const ruleId = alert.fields['kibana.alert.rule.uuid'] ?? null; const linkToRule = ruleId ? prepend(paths.management.ruleDetails(ruleId)) : null; + const caseAttachments: CaseAttachments = useMemo(() => { + return ecsData?._id + ? [ + { + alertId: ecsData?._id ?? '', + index: ecsData?._index ?? '', + owner: observabilityFeatureId, + type: CommentType.alert, + rule: cases.helpers.getRuleIdFromEvent({ ecs: ecsData, data: data ?? [] }), + }, + ] + : []; + }, [ecsData, cases.helpers, data]); + + const createCaseFlyout = cases.hooks.getUseCasesAddToNewCaseFlyout({ + attachments: caseAttachments, + }); + + const selectCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal({ + attachments: caseAttachments, + }); + + const handleAddToNewCaseClick = useCallback(() => { + createCaseFlyout.open(); + closeActionsPopover(); + }, [createCaseFlyout, closeActionsPopover]); + + const handleAddToExistingCaseClick = useCallback(() => { + selectCaseModal.open(); + closeActionsPopover(); + }, [closeActionsPopover, selectCaseModal]); + const actionsMenuItems = useMemo(() => { return [ ...(casePermissions?.crud ? [ - timelines.getAddToExistingCaseButton({ - event, - casePermissions, - appId: observabilityAppId, - owner: observabilityFeatureId, - onClose: afterCaseSelection, - }), - timelines.getAddToNewCaseButton({ - event, - casePermissions, - appId: observabilityAppId, - owner: observabilityFeatureId, - onClose: afterCaseSelection, - }), + + {ADD_TO_EXISTING_CASE} + , + + {ADD_TO_NEW_CASE} + , ] : []), @@ -215,7 +238,7 @@ function ObservabilityActions({ ] : []), ]; - }, [afterCaseSelection, casePermissions, timelines, event, linkToRule]); + }, [casePermissions?.crud, handleAddToExistingCaseClick, handleAddToNewCaseClick, linkToRule]); const actionsToolTip = actionsMenuItems.length <= 0 diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/translations.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/translations.ts new file mode 100644 index 00000000000000..c72dc4c9437592 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ADD_TO_EXISTING_CASE = i18n.translate( + 'xpack.observability.detectionEngine.alerts.actions.addToCase', + { + defaultMessage: 'Add to existing case', + } +); + +export const ADD_TO_NEW_CASE = i18n.translate( + 'xpack.observability.detectionEngine.alerts.actions.addToNewCase', + { + defaultMessage: 'Add to new case', + } +); diff --git a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx index 4d4ef5b8148438..ac5900ca3dc6ae 100644 --- a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx +++ b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx @@ -8,6 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo, useRef, useCallback } from 'react'; +import { observabilityFeatureId } from '../../../common'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../..'; import { EmptySections } from '../../components/app/empty_sections'; import { ObservabilityHeaderMenu } from '../../components/app/header'; @@ -28,6 +30,8 @@ import { DataSections } from './data_sections'; import { LoadingObservability } from './loading_observability'; import { AlertsTableTGrid } from '../alerts/containers/alerts_table_t_grid/alerts_table_t_grid'; import { SectionContainer } from '../../components/app/section'; +import { ObservabilityAppServices } from '../../application/types'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; interface Props { routeParams: RouteParams<'/overview'>; } @@ -85,6 +89,10 @@ export function OverviewPage({ routeParams }: Props) { return refetch.current && refetch.current(); }, []); + const kibana = useKibana(); + const CasesContext = kibana.services.cases.getCasesContext(); + const userPermissions = useGetUserCasesPermissions(); + if (hasAnyData === undefined) { return ; } @@ -130,12 +138,18 @@ export function OverviewPage({ routeParams }: Props) { })} hasError={false} > - + + + diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts new file mode 100644 index 00000000000000..0a700cab50ee61 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.test.ts @@ -0,0 +1,315 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, right } from 'fp-ts/lib/Either'; +import { RuleDataClient, RuleDataClientConstructorOptions, WaitResult } from './rule_data_client'; +import { IndexInfo } from '../rule_data_plugin_service/index_info'; +import { Dataset, RuleDataWriterInitializationError } from '..'; +import { resourceInstallerMock } from '../rule_data_plugin_service/resource_installer.mock'; +import { loggingSystemMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { IndexPatternsFetcher } from '../../../../../src/plugins/data/server'; +import { createNoMatchingIndicesError } from '../../../../../src/plugins/data_views/server/fetcher/lib/errors'; + +const mockLogger = loggingSystemMock.create().get(); +const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient().asInternalUser; +const mockResourceInstaller = resourceInstallerMock.create(); + +// Be careful setting this delay too high. Jest tests can time out +const delay = (ms: number = 3000) => new Promise((resolve) => setTimeout(resolve, ms)); + +interface GetRuleDataClientOptionsOpts { + isWriteEnabled?: boolean; + isWriterCacheEnabled?: boolean; + waitUntilReadyForReading?: Promise; + waitUntilReadyForWriting?: Promise; +} +function getRuleDataClientOptions({ + isWriteEnabled, + isWriterCacheEnabled, + waitUntilReadyForReading, + waitUntilReadyForWriting, +}: GetRuleDataClientOptionsOpts): RuleDataClientConstructorOptions { + return { + indexInfo: new IndexInfo({ + indexOptions: { + feature: 'apm', + registrationContext: 'observability.apm', + dataset: 'alerts' as Dataset, + componentTemplateRefs: [], + componentTemplates: [], + }, + kibanaVersion: '8.2.0', + }), + resourceInstaller: mockResourceInstaller, + isWriteEnabled: isWriteEnabled ?? true, + isWriterCacheEnabled: isWriterCacheEnabled ?? true, + waitUntilReadyForReading: + waitUntilReadyForReading ?? Promise.resolve(right(scopedClusterClient) as WaitResult), + waitUntilReadyForWriting: + waitUntilReadyForWriting ?? Promise.resolve(right(scopedClusterClient) as WaitResult), + logger: mockLogger, + }; +} + +describe('RuleDataClient', () => { + const getFieldsForWildcardMock = jest.fn(); + + test('options are set correctly in constructor', () => { + const namespace = 'test'; + const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); + expect(ruleDataClient.indexName).toEqual(`.alerts-observability.apm.alerts`); + expect(ruleDataClient.kibanaVersion).toEqual('8.2.0'); + expect(ruleDataClient.indexNameWithNamespace(namespace)).toEqual( + `.alerts-observability.apm.alerts-${namespace}` + ); + expect(ruleDataClient.isWriteEnabled()).toEqual(true); + }); + + describe('getReader()', () => { + beforeAll(() => { + getFieldsForWildcardMock.mockResolvedValue(['foo']); + IndexPatternsFetcher.prototype.getFieldsForWildcard = getFieldsForWildcardMock; + }); + + beforeEach(() => { + getFieldsForWildcardMock.mockClear(); + }); + + afterAll(() => { + getFieldsForWildcardMock.mockRestore(); + }); + + test('waits until cluster client is ready before searching', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + waitUntilReadyForReading: new Promise((resolve) => + setTimeout(resolve, 3000, right(scopedClusterClient)) + ), + }) + ); + + const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; + const reader = ruleDataClient.getReader(); + await reader.search({ + body: query, + }); + + expect(scopedClusterClient.search).toHaveBeenCalledWith({ + body: query, + index: `.alerts-observability.apm.alerts*`, + }); + }); + + test('re-throws error when search throws error', async () => { + scopedClusterClient.search.mockRejectedValueOnce(new Error('something went wrong!')); + const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); + const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; + const reader = ruleDataClient.getReader(); + + await expect( + reader.search({ + body: query, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong!"`); + }); + + test('waits until cluster client is ready before getDynamicIndexPattern', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + waitUntilReadyForReading: new Promise((resolve) => + setTimeout(resolve, 3000, right(scopedClusterClient)) + ), + }) + ); + + const reader = ruleDataClient.getReader(); + expect(await reader.getDynamicIndexPattern()).toEqual({ + fields: ['foo'], + timeFieldName: '@timestamp', + title: '.alerts-observability.apm.alerts*', + }); + }); + + test('re-throws generic errors from getFieldsForWildcard', async () => { + getFieldsForWildcardMock.mockRejectedValueOnce(new Error('something went wrong!')); + const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); + const reader = ruleDataClient.getReader(); + + await expect(reader.getDynamicIndexPattern()).rejects.toThrowErrorMatchingInlineSnapshot( + `"something went wrong!"` + ); + }); + + test('correct handles no_matching_indices errors from getFieldsForWildcard', async () => { + getFieldsForWildcardMock.mockRejectedValueOnce(createNoMatchingIndicesError([])); + const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); + const reader = ruleDataClient.getReader(); + + expect(await reader.getDynamicIndexPattern()).toEqual({ + fields: [], + timeFieldName: '@timestamp', + title: '.alerts-observability.apm.alerts*', + }); + }); + + test('handles errors getting cluster client', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + waitUntilReadyForReading: Promise.resolve( + left(new Error('could not get cluster client')) + ), + }) + ); + + const query = { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }; + const reader = ruleDataClient.getReader(); + await expect( + reader.search({ + body: query, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"could not get cluster client"`); + + await expect(reader.getDynamicIndexPattern()).rejects.toThrowErrorMatchingInlineSnapshot( + `"could not get cluster client"` + ); + }); + }); + + describe('getWriter()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('bulk()', () => { + test('logs debug and returns undefined if writing is disabled', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ isWriteEnabled: false }) + ); + const writer = ruleDataClient.getWriter(); + + // Previously, a delay between calling getWriter() and using a writer function + // would cause an Unhandled promise rejection if there were any errors getting a writer + // Adding this delay in the tests to ensure this does not pop up again. + await delay(); + + expect(await writer.bulk({})).toEqual(undefined); + expect(mockLogger.debug).toHaveBeenCalledWith( + `Writing is disabled, bulk() will not write any data.` + ); + }); + + test('logs error, returns undefined and turns off writing if initialization error', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + waitUntilReadyForWriting: Promise.resolve( + left(new Error('could not get cluster client')) + ), + }) + ); + expect(ruleDataClient.isWriteEnabled()).toBe(true); + const writer = ruleDataClient.getWriter(); + + // Previously, a delay between calling getWriter() and using a writer function + // would cause an Unhandled promise rejection if there were any errors getting a writer + // Adding this delay in the tests to ensure this does not pop up again. + await delay(); + + expect(await writer.bulk({})).toEqual(undefined); + expect(mockLogger.error).toHaveBeenNthCalledWith( + 1, + new RuleDataWriterInitializationError( + 'index', + 'observability.apm', + new Error('could not get cluster client') + ) + ); + expect(mockLogger.error).toHaveBeenNthCalledWith( + 2, + `The writer for the Rule Data Client for the observability.apm registration context was not initialized properly, bulk() cannot continue, and writing will be disabled.` + ); + expect(ruleDataClient.isWriteEnabled()).toBe(false); + }); + + test('logs error, returns undefined and turns off writing if resource installation error', async () => { + const error = new Error('bad resource installation'); + mockResourceInstaller.installAndUpdateNamespaceLevelResources.mockRejectedValueOnce(error); + const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); + expect(ruleDataClient.isWriteEnabled()).toBe(true); + const writer = ruleDataClient.getWriter(); + + // Previously, a delay between calling getWriter() and using a writer function + // would cause an Unhandled promise rejection if there were any errors getting a writer + // Adding this delay in the tests to ensure this does not pop up again. + await delay(); + + expect(await writer.bulk({})).toEqual(undefined); + expect(mockLogger.error).toHaveBeenNthCalledWith( + 1, + new RuleDataWriterInitializationError('namespace', 'observability.apm', error) + ); + expect(mockLogger.error).toHaveBeenNthCalledWith( + 2, + `The writer for the Rule Data Client for the observability.apm registration context was not initialized properly, bulk() cannot continue, and writing will be disabled.` + ); + expect(ruleDataClient.isWriteEnabled()).toBe(false); + }); + + test('logs error and returns undefined if bulk function throws error', async () => { + const error = new Error('something went wrong!'); + scopedClusterClient.bulk.mockRejectedValueOnce(error); + const ruleDataClient = new RuleDataClient(getRuleDataClientOptions({})); + expect(ruleDataClient.isWriteEnabled()).toBe(true); + const writer = ruleDataClient.getWriter(); + + // Previously, a delay between calling getWriter() and using a writer function + // would cause an Unhandled promise rejection if there were any errors getting a writer + // Adding this delay in the tests to ensure this does not pop up again. + await delay(); + + expect(await writer.bulk({})).toEqual(undefined); + expect(mockLogger.error).toHaveBeenNthCalledWith(1, error); + expect(ruleDataClient.isWriteEnabled()).toBe(true); + }); + + test('waits until cluster client is ready before calling bulk', async () => { + const ruleDataClient = new RuleDataClient( + getRuleDataClientOptions({ + waitUntilReadyForWriting: new Promise((resolve) => + setTimeout(resolve, 3000, right(scopedClusterClient)) + ), + }) + ); + + const writer = ruleDataClient.getWriter(); + // Previously, a delay between calling getWriter() and using a writer function + // would cause an Unhandled promise rejection if there were any errors getting a writer + // Adding this delay in the tests to ensure this does not pop up again. + await delay(); + + const response = await writer.bulk({}); + + expect(response).toEqual({ + body: {}, + headers: { + 'x-elastic-product': 'Elasticsearch', + }, + meta: {}, + statusCode: 200, + warnings: [], + }); + + expect(scopedClusterClient.bulk).toHaveBeenCalledWith( + { + index: `.alerts-observability.apm.alerts-default`, + require_alias: true, + }, + { meta: true } + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts index 491c9ff22d21f6..6fe9d43ddbee01 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts @@ -18,12 +18,12 @@ import { RuleDataWriterInitializationError, } from '../rule_data_plugin_service/errors'; import { IndexInfo } from '../rule_data_plugin_service/index_info'; -import { ResourceInstaller } from '../rule_data_plugin_service/resource_installer'; +import { IResourceInstaller } from '../rule_data_plugin_service/resource_installer'; import { IRuleDataClient, IRuleDataReader, IRuleDataWriter } from './types'; -interface ConstructorOptions { +export interface RuleDataClientConstructorOptions { indexInfo: IndexInfo; - resourceInstaller: ResourceInstaller; + resourceInstaller: IResourceInstaller; isWriteEnabled: boolean; isWriterCacheEnabled: boolean; waitUntilReadyForReading: Promise; @@ -40,7 +40,7 @@ export class RuleDataClient implements IRuleDataClient { // Writers cached by namespace private writerCache: Map; - constructor(private readonly options: ConstructorOptions) { + constructor(private readonly options: RuleDataClientConstructorOptions) { this.writeEnabled = this.options.isWriteEnabled; this.writerCacheEnabled = this.options.isWriterCacheEnabled; this.writerCache = new Map(); @@ -181,43 +181,46 @@ export class RuleDataClient implements IRuleDataClient { } }; - const prepareForWritingResult = prepareForWriting(); + const prepareForWritingResult = prepareForWriting().catch((error) => { + if (error instanceof RuleDataWriterInitializationError) { + this.options.logger.error(error); + this.options.logger.error( + `The writer for the Rule Data Client for the ${indexInfo.indexOptions.registrationContext} registration context was not initialized properly, bulk() cannot continue, and writing will be disabled.` + ); + turnOffWrite(); + } else if (error instanceof RuleDataWriteDisabledError) { + this.options.logger.debug(`Writing is disabled, bulk() will not write any data.`); + } + return undefined; + }); return { bulk: async (request: estypes.BulkRequest) => { - return prepareForWritingResult - .then((clusterClient) => { + try { + const clusterClient = await prepareForWritingResult; + if (clusterClient) { const requestWithDefaultParameters = { ...request, require_alias: true, index: alias, }; - return clusterClient - .bulk(requestWithDefaultParameters, { meta: true }) - .then((response) => { - if (response.body.errors) { - const error = new errors.ResponseError(response); - this.options.logger.error(error); - } - return response; - }); - }) - .catch((error) => { - if (error instanceof RuleDataWriterInitializationError) { - this.options.logger.error(error); - this.options.logger.error( - `The writer for the Rule Data Client for the ${indexInfo.indexOptions.registrationContext} registration context was not initialized properly, bulk() cannot continue, and writing will be disabled.` - ); - turnOffWrite(); - } else if (error instanceof RuleDataWriteDisabledError) { - this.options.logger.debug(`Writing is disabled, bulk() will not write any data.`); - } else { + const response = await clusterClient.bulk(requestWithDefaultParameters, { meta: true }); + + if (response.body.errors) { + const error = new errors.ResponseError(response); this.options.logger.error(error); } + return response; + } else { + this.options.logger.debug(`Writing is disabled, bulk() will not write any data.`); + } + return undefined; + } catch (error) { + this.options.logger.error(error); - return undefined; - }); + return undefined; + } }, }; } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.mock.ts index 0b3940b936424e..6e84f569d481c1 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.mock.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.mock.ts @@ -5,11 +5,11 @@ * 2.0. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ResourceInstaller } from './resource_installer'; +import { IResourceInstaller, ResourceInstaller } from './resource_installer'; type Schema = PublicMethodsOf; export type ResourceInstallerMock = jest.Mocked; -const createResourceInstallerMock = () => { +const createResourceInstallerMock = (): jest.Mocked => { return { installCommonResources: jest.fn(), installIndexLevelResources: jest.fn(), diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 8e7d13b0dc210d..ab7bc28af8c137 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -10,6 +10,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient, Logger } from 'kibana/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { DEFAULT_ILM_POLICY_ID, ECS_COMPONENT_TEMPLATE_NAME, @@ -31,6 +32,7 @@ interface ConstructorOptions { disabledRegistrationContexts: string[]; } +export type IResourceInstaller = PublicMethodsOf; export class ResourceInstaller { constructor(private readonly options: ConstructorOptions) {} diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts index 126be5c6d2972a..b916091510319c 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts @@ -14,7 +14,7 @@ import { INDEX_PREFIX } from '../config'; import { IRuleDataClient, RuleDataClient, WaitResult } from '../rule_data_client'; import { IndexInfo } from './index_info'; import { Dataset, IndexOptions } from './index_options'; -import { ResourceInstaller } from './resource_installer'; +import { IResourceInstaller, ResourceInstaller } from './resource_installer'; import { joinWithDash } from './utils'; /** @@ -89,7 +89,7 @@ interface ConstructorOptions { export class RuleDataService implements IRuleDataService { private readonly indicesByBaseName: Map; private readonly indicesByFeatureId: Map; - private readonly resourceInstaller: ResourceInstaller; + private readonly resourceInstaller: IResourceInstaller; private installCommonResources: Promise>; private isInitialized: boolean; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 3593030913ba74..dbd91164987005 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -112,6 +112,7 @@ function createRule(shouldWriteAlerts: boolean = true) { services: { alertFactory, savedObjectsClient: {} as any, + uiSettingsClient: {} as any, scopedClusterClient: {} as any, shouldWriteAlerts: () => shouldWriteAlerts, shouldStopExecution: () => false, diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts index 3d880988182b12..38843d95d6b730 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock, + uiSettingsServiceMock, } from '../../../../../src/core/server/mocks'; import { AlertExecutorOptions, @@ -69,6 +70,7 @@ export const createDefaultAlertExecutorOptions = < services: { alertFactory: alertsMock.createAlertServices().alertFactory, savedObjectsClient: savedObjectsClientMock.create(), + uiSettingsClient: uiSettingsServiceMock.createClient(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), shouldWriteAlerts: () => shouldWriteAlerts, shouldStopExecution: () => false, diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts index 6fbe54578f4692..6ddb2fc19ef07e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -118,7 +118,7 @@ export class BaseDataGenerator { } /** generate random OS family value */ - protected randomOSFamily(): string { + public randomOSFamily(): string { return this.randomChoice(OS_FAMILY); } @@ -133,7 +133,7 @@ export class BaseDataGenerator { } /** Generate a random number up to the max provided */ - protected randomN(max: number): number { + public randomN(max: number): number { return Math.floor(this.random() * max); } diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts index daf96a3149649a..99683bcd11868c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts @@ -5,7 +5,10 @@ * 2.0. */ -import type { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { + CreateExceptionListItemSchema, + ExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { BaseDataGenerator } from './base_data_generator'; import { ExceptionsListItemGenerator } from './exceptions_list_item_generator'; @@ -13,7 +16,7 @@ import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from '../service/a const EFFECT_SCOPE_TYPES = [BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG]; export class EventFilterGenerator extends BaseDataGenerator { - generate(): CreateExceptionListItemSchema { + generate(overrides: Partial = {}): CreateExceptionListItemSchema { const eventFilterGenerator = new ExceptionsListItemGenerator(); const eventFilterData: CreateExceptionListItemSchema = eventFilterGenerator.generateEventFilter( { @@ -29,6 +32,7 @@ export class EventFilterGenerator extends BaseDataGenerator { describe('for GET List', () => { diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 4b04f15682777a..88ac65768e163b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -6,7 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { ConditionEntry, ConditionEntryField, OperatingSystem } from '../types'; +import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; +import { ConditionEntry } from '../types'; import { getDuplicateFields, isValidHash } from '../service/trusted_apps/validations'; export const DeleteTrustedAppsRequestSchema = { diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts index 0e6f2a5a7df418..2d2c50572a8bc7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { - ConditionEntry, - ConditionEntryField, - OperatingSystem, - TrustedAppEntryTypes, -} from '../../types'; +import { ConditionEntryField } from '@kbn/securitysolution-utils'; +import { ConditionEntry } from '../../types'; const HASH_LENGTHS: readonly number[] = [ 32, // MD5 @@ -37,118 +33,3 @@ export const getDuplicateFields = (entries: ConditionEntry[]) => { .filter((entry) => entry[1].length > 1) .map((entry) => entry[0]); }; - -/* - * regex to match executable names - * starts matching from the eol of the path - * file names with a single or multiple spaces (for spaced names) - * and hyphens and combinations of these that produce complex names - * such as: - * c:\home\lib\dmp.dmp - * c:\home\lib\my-binary-app-+/ some/ x/ dmp.dmp - * /home/lib/dmp.dmp - * /home/lib/my-binary-app+-\ some\ x\ dmp.dmp - */ -const WIN_EXEC_PATH = /\\(\w+|\w*[\w+|-]+\/ +)+\w+[\w+|-]+\.*\w+$/i; -const UNIX_EXEC_PATH = /(\/|\w*[\w+|-]+\\ +)+\w+[\w+|-]+\.*\w*$/i; - -export const hasSimpleExecutableName = ({ - os, - type, - value, -}: { - os: OperatingSystem; - type: TrustedAppEntryTypes; - value: string; -}): boolean => { - if (type === 'wildcard') { - return os === OperatingSystem.WINDOWS ? WIN_EXEC_PATH.test(value) : UNIX_EXEC_PATH.test(value); - } - return true; -}; - -export const isPathValid = ({ - os, - field, - type, - value, -}: { - os: OperatingSystem; - field: ConditionEntryField; - type: TrustedAppEntryTypes; - value: string; -}): boolean => { - if (field === ConditionEntryField.PATH) { - if (type === 'wildcard') { - return os === OperatingSystem.WINDOWS - ? isWindowsWildcardPathValid(value) - : isLinuxMacWildcardPathValid(value); - } - return doesPathMatchRegex({ value, os }); - } - return true; -}; - -const doesPathMatchRegex = ({ os, value }: { os: OperatingSystem; value: string }): boolean => { - if (os === OperatingSystem.WINDOWS) { - const filePathRegex = - /^[a-z]:(?:|\\\\[^<>:"'/\\|?*]+\\[^<>:"'/\\|?*]+|%\w+%|)[\\](?:[^<>:"'/\\|?*]+[\\/])*([^<>:"'/\\|?*])+$/i; - return filePathRegex.test(value); - } - return /^(\/|(\/[\w\-]+)+|\/[\w\-]+\.[\w]+|(\/[\w-]+)+\/[\w\-]+\.[\w]+)$/i.test(value); -}; - -const isWindowsWildcardPathValid = (path: string): boolean => { - const firstCharacter = path[0]; - const lastCharacter = path.slice(-1); - const trimmedValue = path.trim(); - const hasSlash = /\//.test(trimmedValue); - if (path.length === 0) { - return false; - } else if ( - hasSlash || - trimmedValue.length !== path.length || - firstCharacter === '^' || - lastCharacter === '\\' || - !hasWildcard({ path, isWindowsPath: true }) - ) { - return false; - } else { - return true; - } -}; - -const isLinuxMacWildcardPathValid = (path: string): boolean => { - const firstCharacter = path[0]; - const lastCharacter = path.slice(-1); - const trimmedValue = path.trim(); - if (path.length === 0) { - return false; - } else if ( - trimmedValue.length !== path.length || - firstCharacter !== '/' || - lastCharacter === '/' || - path.length > 1024 === true || - path.includes('//') === true || - !hasWildcard({ path, isWindowsPath: false }) - ) { - return false; - } else { - return true; - } -}; - -const hasWildcard = ({ - path, - isWindowsPath, -}: { - path: string; - isWindowsPath: boolean; -}): boolean => { - for (const pathComponent of path.split(isWindowsPath ? '\\' : '/')) { - if (/[\*|\?]+/.test(pathComponent) === true) { - return true; - } - } - return false; -}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/os.ts b/x-pack/plugins/security_solution/common/endpoint/types/os.ts index f892d077a9ed88..af73dcd91ae8d4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/os.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/os.ts @@ -5,12 +5,6 @@ * 2.0. */ -export enum OperatingSystem { - LINUX = 'linux', - MAC = 'macos', - WINDOWS = 'windows', -} - // PolicyConfig uses mac instead of macos export enum PolicyOperatingSystem { windows = 'windows', diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 9815bc3535de46..3872df8d102474 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -6,7 +6,11 @@ */ import { TypeOf } from '@kbn/config-schema'; - +import { + ConditionEntryField, + OperatingSystem, + TrustedAppEntryTypes, +} from '@kbn/securitysolution-utils'; import { DeleteTrustedAppsRequestSchema, GetOneTrustedAppRequestSchema, @@ -15,7 +19,6 @@ import { PutTrustedAppUpdateRequestSchema, GetTrustedAppsSummaryRequestSchema, } from '../schema/trusted_apps'; -import { OperatingSystem } from './os'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; @@ -67,18 +70,11 @@ export interface GetTrustedAppsSummaryResponse { linux: number; } -export enum ConditionEntryField { - HASH = 'process.hash.*', - PATH = 'process.executable.caseless', - SIGNER = 'process.Ext.code_signature', -} - export enum OperatorFieldIds { is = 'is', matches = 'matches', } -export type TrustedAppEntryTypes = 'match' | 'wildcard'; export interface ConditionEntry { field: T; type: TrustedAppEntryTypes; diff --git a/x-pack/plugins/security_solution/common/utils/path_placeholder.test.ts b/x-pack/plugins/security_solution/common/utils/path_placeholder.test.ts index 9618440c105dc4..58f9fa70f73955 100644 --- a/x-pack/plugins/security_solution/common/utils/path_placeholder.test.ts +++ b/x-pack/plugins/security_solution/common/utils/path_placeholder.test.ts @@ -6,7 +6,11 @@ */ import { getPlaceholderTextByOSType, getPlaceholderText } from './path_placeholder'; -import { ConditionEntryField, OperatingSystem, TrustedAppEntryTypes } from '../endpoint/types'; +import { + ConditionEntryField, + OperatingSystem, + TrustedAppEntryTypes, +} from '@kbn/securitysolution-utils'; const trustedAppEntry = { os: OperatingSystem.LINUX, diff --git a/x-pack/plugins/security_solution/common/utils/path_placeholder.ts b/x-pack/plugins/security_solution/common/utils/path_placeholder.ts index baa9b71cd4483b..328df398dd5760 100644 --- a/x-pack/plugins/security_solution/common/utils/path_placeholder.ts +++ b/x-pack/plugins/security_solution/common/utils/path_placeholder.ts @@ -4,8 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { ConditionEntryField, OperatingSystem, TrustedAppEntryTypes } from '../endpoint/types'; +import { + ConditionEntryField, + OperatingSystem, + TrustedAppEntryTypes, +} from '@kbn/securitysolution-utils'; export const getPlaceholderText = () => ({ windows: { diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts index 538fa3a008a1fd..69bdafd5dccddd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts @@ -5,30 +5,17 @@ * 2.0. */ -import { - getException, - getExceptionList, - expectedExportedExceptionList, -} from '../../objects/exception'; +import { getExceptionList, expectedExportedExceptionList } from '../../objects/exception'; import { getNewRule } from '../../objects/rule'; -import { RULE_STATUS } from '../../screens/create_new_rule'; - import { createCustomRule } from '../../tasks/api_calls/rules'; -import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; -import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange, waitForPageWithoutDateRange, } from '../../tasks/login'; -import { - addsExceptionFromRuleSettings, - goBackToAllRulesTable, - goToExceptionsTab, -} from '../../tasks/rule_details'; import { DETECTIONS_RULE_MANAGEMENT_URL, EXCEPTIONS_URL } from '../../urls/navigation'; -import { cleanKibana, reload } from '../../tasks/common'; +import { cleanKibana } from '../../tasks/common'; import { deleteExceptionListWithRuleReference, deleteExceptionListWithoutRuleReference, @@ -43,30 +30,47 @@ import { } from '../../screens/exceptions'; import { createExceptionList } from '../../tasks/api_calls/exceptions'; +const getExceptionList1 = () => ({ + ...getExceptionList(), + name: 'Test a new list 1', + list_id: 'exception_list_1', +}); +const getExceptionList2 = () => ({ + ...getExceptionList(), + name: 'Test list 2', + list_id: 'exception_list_2', +}); + describe('Exceptions Table', () => { before(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - createCustomRule(getNewRule()); - reload(); - goToRuleDetails(); - cy.get(RULE_STATUS).should('have.text', '—'); - - esArchiverLoad('auditbeat_for_exceptions'); - - // Add a detections exception list - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); + // Create exception list associated with a rule + createExceptionList(getExceptionList2(), getExceptionList2().list_id).then((response) => + createCustomRule({ + ...getNewRule(), + exceptionLists: [ + { + id: response.body.id, + list_id: getExceptionList2().list_id, + type: getExceptionList2().type, + namespace_type: getExceptionList2().namespace_type, + }, + ], + }) + ); // Create exception list not used by any rules - createExceptionList(getExceptionList(), getExceptionList().list_id).as('exceptionListResponse'); + createExceptionList(getExceptionList1(), getExceptionList1().list_id).as( + 'exceptionListResponse' + ); - goBackToAllRulesTable(); - }); + waitForPageWithoutDateRange(EXCEPTIONS_URL); - after(() => { - esArchiverUnload('auditbeat_for_exceptions'); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); }); it('Exports exception list', function () { @@ -87,60 +91,80 @@ describe('Exceptions Table', () => { waitForPageWithoutDateRange(EXCEPTIONS_URL); waitForExceptionsTableToBeLoaded(); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 3 lists`); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); // Single word search searchForExceptionList('Endpoint'); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1'); cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('have.text', 'Endpoint Security Exception List'); // Multi word search clearSearchSelection(); - searchForExceptionList('New Rule Test'); + searchForExceptionList('test'); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 2 lists`); - cy.get(EXCEPTIONS_TABLE_LIST_NAME).eq(0).should('have.text', 'Test exception list'); - cy.get(EXCEPTIONS_TABLE_LIST_NAME).eq(1).should('have.text', 'New Rule Test'); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '2'); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).eq(1).should('have.text', 'Test list 2'); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).eq(0).should('have.text', 'Test a new list 1'); // Exact phrase search clearSearchSelection(); - searchForExceptionList('"New Rule Test"'); + searchForExceptionList(`"${getExceptionList1().name}"`); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); - cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('have.text', 'New Rule Test'); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1'); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('have.text', getExceptionList1().name); // Field search clearSearchSelection(); searchForExceptionList('list_id:endpoint_list'); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1'); cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('have.text', 'Endpoint Security Exception List'); clearSearchSelection(); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 3 lists`); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); }); it('Deletes exception list without rule reference', () => { waitForPageWithoutDateRange(EXCEPTIONS_URL); waitForExceptionsTableToBeLoaded(); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 3 lists`); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); deleteExceptionListWithoutRuleReference(); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 2 lists`); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '2'); }); it('Deletes exception list with rule reference', () => { waitForPageWithoutDateRange(EXCEPTIONS_URL); waitForExceptionsTableToBeLoaded(); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 2 lists`); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '2'); deleteExceptionListWithRuleReference(); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/exception.ts b/x-pack/plugins/security_solution/cypress/objects/exception.ts index 1a70bb10383203..fbb000f43fdd23 100644 --- a/x-pack/plugins/security_solution/cypress/objects/exception.ts +++ b/x-pack/plugins/security_solution/cypress/objects/exception.ts @@ -41,5 +41,5 @@ export const expectedExportedExceptionList = ( exceptionListResponse: Cypress.Response ): string => { const jsonrule = exceptionListResponse.body; - return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"elastic","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"detection","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","version":1}\n{"exported_exception_list_count":1,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`; + return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"elastic","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"${jsonrule.list_id}","name":"${jsonrule.name}","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"${jsonrule.type}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","version":1}\n{"exported_exception_list_count":1,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`; }; diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 2f81c160f28017..65e61c48ec64d4 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -59,6 +59,7 @@ export interface CustomRule { timeline: CompleteTimeline; maxSignals: number; buildingBlockType?: string; + exceptionLists?: Array<{ id: string; list_id: string; type: string; namespace_type: string }>; } export interface ThresholdRule extends CustomRule { diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 13ba3af59be9a0..405c1181403958 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -24,6 +24,7 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte query: rule.customQuery, language: 'kuery', enabled: false, + exceptions_list: rule.exceptionLists ?? [], }, headers: { 'kbn-xsrf': 'cypress-creds' }, failOnStatusCode: false, 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 a9fd9a5d9d44f8..5e3fc4e81f9dc3 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 @@ -13,7 +13,7 @@ import type { Filter } from '@kbn/es-query'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; -import { APP_UI_ID } from '../../../../common/constants'; +import { APP_ID, APP_UI_ID } from '../../../../common/constants'; import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; import type { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -27,7 +27,7 @@ import { TGridCellAction } from '../../../../../timelines/common/types'; import { DetailsPanel } from '../../../timelines/components/side_panel'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; -import { useKibana } from '../../lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import { CreateFieldEditorActions, @@ -109,7 +109,7 @@ const StatefulEventsViewerComponent: React.FC = ({ unit, }) => { const dispatch = useDispatch(); - const { timelines: timelinesUi } = useKibana().services; + const { timelines: timelinesUi, cases: casesUi } = useKibana().services; const { browserFields, dataViewId, @@ -179,63 +179,68 @@ const StatefulEventsViewerComponent: React.FC = ({ const createFieldComponent = useCreateFieldButton(scopeId, id, editorActionsRef); + const casesPermissions = useGetUserCasesPermissions(); + const CasesContext = casesUi.getCasesContext(); + return ( <> - - - {timelinesUi.getTGrid<'embedded'>({ - additionalFilters, - appId: APP_UI_ID, - browserFields, - bulkActions, - columns, - dataProviders, - dataViewId, - defaultCellActions, - deletedEventIds, - disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, - docValueFields, - end, - entityType, - filters: globalFilters, - filterStatus: currentFilter, - globalFullScreen, - graphEventId, - graphOverlay, - hasAlertsCrud, - id, - indexNames: selectedPatterns, - indexPattern, - isLive, - isLoadingIndexPattern, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - leadingControlColumns, - onRuleChange, - query, - renderCellValue, - rowRenderers, - runtimeMappings, - setQuery, - sort, - start, - tGridEventRenderedViewEnabled, - trailingControlColumns, - type: 'embedded', - unit, - createFieldComponent, - })} - - - + + + + {timelinesUi.getTGrid<'embedded'>({ + additionalFilters, + appId: APP_UI_ID, + browserFields, + bulkActions, + columns, + dataProviders, + dataViewId, + defaultCellActions, + deletedEventIds, + disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, + docValueFields, + end, + entityType, + filters: globalFilters, + filterStatus: currentFilter, + globalFullScreen, + graphEventId, + graphOverlay, + hasAlertsCrud, + id, + indexNames: selectedPatterns, + indexPattern, + isLive, + isLoadingIndexPattern, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + leadingControlColumns, + onRuleChange, + query, + renderCellValue, + rowRenderers, + runtimeMappings, + setQuery, + sort, + start, + tGridEventRenderedViewEnabled, + trailingControlColumns, + type: 'embedded', + unit, + createFieldComponent, + })} + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index aacc1dc9516934..b76b5ee99843e8 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -18,6 +18,7 @@ import { createWithKibanaMock, } from '../kibana_react.mock'; import { APP_UI_ID } from '../../../../../common/constants'; +import { mockCasesContract } from '../../../../../../cases/public/mocks'; const mockStartServicesMock = createStartServicesMock(); export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; @@ -28,6 +29,7 @@ export const useKibana = jest.fn().mockReturnValue({ get: jest.fn(), set: jest.fn(), }, + cases: mockCasesContract(), data: { ...mockStartServicesMock.data, search: { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 3c9d2115f7ef28..d5fc54c5cbac73 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -12,6 +12,7 @@ import { TestProviders } from '../../../../common/mock'; import React from 'react'; import { Ecs } from '../../../../../common/ecs'; import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; +import { mockCasesContract } from '../../../../../../cases/public/mocks'; const ecsRowData: Ecs = { _id: '1', @@ -51,6 +52,7 @@ jest.mock('../../../../common/lib/kibana', () => ({ application: { capabilities: { siem: { crud_alerts: true, read_alerts: true } }, }, + cases: mockCasesContract(), }, }), useGetUserCasesPermissions: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index b5e630de50f79d..d472b9bf3f1915 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -33,7 +33,6 @@ import { useExceptionFlyout } from './use_add_exception_flyout'; import { useExceptionActions } from './use_add_exception_actions'; import { useEventFilterModal } from './use_event_filter_modal'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { useKibana } from '../../../../common/lib/kibana'; import { ATTACH_ALERT_TO_CASE_FOR_ROW } from '../../../../timelines/components/timeline/body/translations'; import { useEventFilterAction } from './use_event_filter_action'; import { useAddToCaseActions } from './use_add_to_case_actions'; @@ -65,16 +64,15 @@ const AlertContextMenuComponent: React.FC { + const onMenuItemClick = useCallback(() => { setPopover(false); }, []); const ruleId = get(0, ecsRowData?.kibana?.alert?.rule?.uuid); const ruleName = get(0, ecsRowData?.kibana?.alert?.rule?.name); - const { timelines: timelinesUi } = useKibana().services; - const { addToCaseActionProps, addToCaseActionItems } = useAddToCaseActions({ + const { addToCaseActionItems } = useAddToCaseActions({ ecsData: ecsRowData, - afterCaseSelection: afterItemSelection, + onMenuItemClick, timelineId, ariaLabel: ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues }), }); @@ -186,7 +184,6 @@ const AlertContextMenuComponent: React.FC - {addToCaseActionProps && timelinesUi.getAddToCaseAction(addToCaseActionProps)} {items.length > 0 && (
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 cc0ef8d4e8b797..2ca4525c7e1ab5 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 @@ -5,16 +5,19 @@ * 2.0. */ -import { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; +import { CommentType } from '../../../../../../cases/common'; +import { CaseAttachments } from '../../../../../../cases/public'; import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; import { TimelineId } from '../../../../../common/types'; -import { APP_ID, APP_UI_ID } from '../../../../../common/constants'; -import { useInsertTimeline } from '../../../../cases/components/use_insert_timeline'; +import { APP_ID } from '../../../../../common/constants'; import { Ecs } from '../../../../../common/ecs'; +import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations'; export interface UseAddToCaseActions { - afterCaseSelection: () => void; + onMenuItemClick: () => void; ariaLabel?: string; ecsData?: Ecs; nonEcsData?: TimelineNonEcsData[]; @@ -22,51 +25,92 @@ export interface UseAddToCaseActions { } export const useAddToCaseActions = ({ - afterCaseSelection, + onMenuItemClick, ariaLabel, ecsData, nonEcsData, timelineId, }: UseAddToCaseActions) => { - const { timelines: timelinesUi } = useKibana().services; + const { cases: casesUi } = useKibana().services; const casePermissions = useGetUserCasesPermissions(); - const insertTimelineHook = useInsertTimeline; + const hasWritePermissions = casePermissions?.crud ?? false; - const addToCaseActionProps = useMemo( - () => - ecsData?._id - ? { - ariaLabel, - event: { data: nonEcsData ?? [], ecs: ecsData, _id: ecsData?._id }, - useInsertTimeline: insertTimelineHook, - casePermissions, - appId: APP_UI_ID, + const caseAttachments: CaseAttachments = useMemo(() => { + return ecsData?._id + ? [ + { + alertId: ecsData?._id ?? '', + index: ecsData?._index ?? '', owner: APP_ID, - onClose: afterCaseSelection, - } - : null, - [ecsData, ariaLabel, nonEcsData, insertTimelineHook, casePermissions, afterCaseSelection] - ); - const hasWritePermissions = casePermissions?.crud ?? false; - const addToCaseActionItems = useMemo( - () => + type: CommentType.alert, + rule: casesUi.helpers.getRuleIdFromEvent({ ecs: ecsData, data: nonEcsData ?? [] }), + }, + ] + : []; + }, [casesUi.helpers, ecsData, nonEcsData]); + + const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({ + attachments: caseAttachments, + onClose: onMenuItemClick, + }); + + const selectCaseModal = casesUi.hooks.getUseCasesAddToExistingCaseModal({ + attachments: caseAttachments, + onClose: onMenuItemClick, + }); + + const handleAddToNewCaseClick = useCallback(() => { + // TODO rename this, this is really `closePopover()` + onMenuItemClick(); + createCaseFlyout.open(); + }, [onMenuItemClick, createCaseFlyout]); + + const handleAddToExistingCaseClick = useCallback(() => { + // TODO rename this, this is really `closePopover()` + onMenuItemClick(); + selectCaseModal.open(); + }, [onMenuItemClick, selectCaseModal]); + + const addToCaseActionItems = useMemo(() => { + if ( [ TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active, ].includes(timelineId as TimelineId) && - hasWritePermissions && - addToCaseActionProps - ? [ - timelinesUi.getAddToExistingCaseButton(addToCaseActionProps), - timelinesUi.getAddToNewCaseButton(addToCaseActionProps), - ] - : [], - [addToCaseActionProps, hasWritePermissions, timelineId, timelinesUi] - ); + hasWritePermissions + ) { + return [ + // add to existing case menu item + + {ADD_TO_EXISTING_CASE} + , + // add to new case menu item + + {ADD_TO_NEW_CASE} + , + ]; + } + return []; + }, [ + ariaLabel, + handleAddToExistingCaseClick, + handleAddToNewCaseClick, + hasWritePermissions, + timelineId, + ]); return { addToCaseActionItems, - addToCaseActionProps, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 590b5759ecae45..bdddd8ab462076 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -285,3 +285,17 @@ export const TRIGGERED = i18n.translate( defaultMessage: 'Triggered', } ); + +export const ADD_TO_EXISTING_CASE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.addToCase', + { + defaultMessage: 'Add to existing case', + } +); + +export const ADD_TO_NEW_CASE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.addToNewCase', + { + defaultMessage: 'Add to new case', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index 0c525a2d77706a..a15a717f6f42a2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -17,6 +17,7 @@ import { TestProviders } from '../../../common/mock'; import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; import { useKibana } from '../../../common/lib/kibana'; +import { mockCasesContract } from '../../../../../cases/public/mocks'; jest.mock('../user_info', () => ({ useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), @@ -82,6 +83,7 @@ describe('take action dropdown', () => { services: { ...mockStartServicesMock, timelines: { ...mockTimelines }, + cases: mockCasesContract(), application: { capabilities: { siem: { crud_alerts: true, read_alerts: true } }, }, diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 8ad76c70247bf8..d9dfcd0fee7dae 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -137,7 +137,7 @@ export const TakeActionDropdown = React.memo( disabled: !isEndpointEvent, }); - const afterCaseSelection = useCallback(() => { + const onMenuItemClick = useCallback(() => { closePopoverHandler(); }, [closePopoverHandler]); @@ -175,7 +175,7 @@ export const TakeActionDropdown = React.memo( const { addToCaseActionItems } = useAddToCaseActions({ ecsData, nonEcsData: detailsData?.map((d) => ({ field: d.field, value: d.values })) ?? [], - afterCaseSelection, + onMenuItemClick, timelineId, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 4b6cbb6f7e16dd..4891c75744e385 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -24,7 +24,7 @@ import { createStore, State } from '../../../common/store'; import { mockHistory, Router } from '../../../common/mock/router'; import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { mockCasesContext } from '../../../common/mock/mock_cases_context'; +import { mockCasesContext } from '../../../../../cases/public/mocks/mock_cases_context'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar diff --git a/x-pack/plugins/security_solution/public/management/common/translations.ts b/x-pack/plugins/security_solution/public/management/common/translations.ts index 8e6fcd4cc951e1..e79c1c0b344967 100644 --- a/x-pack/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/plugins/security_solution/public/management/common/translations.ts @@ -6,10 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { ServerApiError } from '../../common/types'; -import { OperatingSystem } from '../../../common/endpoint/types'; - export const ENDPOINTS_TAB = i18n.translate('xpack.securitySolution.endpointsTab', { defaultMessage: 'Endpoints', }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx index 238fe87c058904..9b656a97a94a0b 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx @@ -22,7 +22,7 @@ import { OS_MAC, OS_WINDOWS, CONDITION_AND, - CONDITION_OPERATOR_TYPE_WILDCARD, + CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES, CONDITION_OPERATOR_TYPE_NESTED, CONDITION_OPERATOR_TYPE_MATCH, CONDITION_OPERATOR_TYPE_MATCH_ANY, @@ -45,7 +45,7 @@ const OPERATOR_TYPE_LABELS_INCLUDED = Object.freeze({ [ListOperatorTypeEnum.NESTED]: CONDITION_OPERATOR_TYPE_NESTED, [ListOperatorTypeEnum.MATCH_ANY]: CONDITION_OPERATOR_TYPE_MATCH_ANY, [ListOperatorTypeEnum.MATCH]: CONDITION_OPERATOR_TYPE_MATCH, - [ListOperatorTypeEnum.WILDCARD]: CONDITION_OPERATOR_TYPE_WILDCARD, + [ListOperatorTypeEnum.WILDCARD]: CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES, [ListOperatorTypeEnum.EXISTS]: CONDITION_OPERATOR_TYPE_EXISTS, [ListOperatorTypeEnum.LIST]: CONDITION_OPERATOR_TYPE_LIST, }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts index 3290a52c1c37d4..273cda46aa7210 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts @@ -61,8 +61,8 @@ export const CONDITION_OPERATOR_TYPE_NOT_MATCH = i18n.translate( } ); -export const CONDITION_OPERATOR_TYPE_WILDCARD = i18n.translate( - 'xpack.securitySolution.artifactCard.conditions.wildcardOperator', +export const CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES = i18n.translate( + 'xpack.securitySolution.artifactCard.conditions.wildcardMatchesOperator', { defaultMessage: 'MATCHES', } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index fa68215cc768ba..6d24b9558ea532 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -25,8 +25,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { EVENT_FILTERS_OPERATORS } from '@kbn/securitysolution-list-utils'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { OperatingSystem, PolicyData } from '../../../../../../../common/endpoint/types'; +import { PolicyData } from '../../../../../../../common/endpoint/types'; import { AddExceptionComments } from '../../../../../../common/components/exceptions/add_exception_comments'; import { filterIndexPatterns } from '../../../../../../common/components/exceptions/helpers'; import { Loader } from '../../../../../../common/components/loader'; @@ -225,6 +226,7 @@ export const EventFiltersForm: React.FC = memo( onChange: handleOnBuilderChange, listTypeSpecificIndexPatternFilter: filterIndexPatterns, operatorsList: EVENT_FILTERS_OPERATORS, + osTypes: exception?.os_types, }), [data, handleOnBuilderChange, http, indexPatterns, exception] ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx index 45aad6c3d1432c..c02969993e62d1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx @@ -10,7 +10,7 @@ import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; -import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { ConfigForm } from '../config_form'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx index 89fe46445b20eb..79e32cf2e36720 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx @@ -11,7 +11,7 @@ import { addDecorator, storiesOf } from '@storybook/react'; import { euiLightVars } from '@kbn/ui-theme'; import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; -import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { ConfigForm } from '.'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx index 9d753749dabed7..6a5f7d187478d9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { ThemeContext } from 'styled-components'; -import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { OS_TITLES } from '../../../../../common/translations'; const TITLES = { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx index 4364d921412403..a3f4b2fdc7fb1d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx @@ -8,11 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCheckbox, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui'; -import { - OperatingSystem, - PolicyOperatingSystem, - UIPolicyConfig, -} from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { PolicyOperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; const OPERATING_SYSTEM_TO_TEST_SUBJ: { [K in OperatingSystem]: string } = { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index 7f7163b68e7c3e..1980877eea95d7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; -import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { policyConfig } from '../../../store/policy_details/selectors'; import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index 65b4ce9964d86e..8bc1f0fcaf17c2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; -import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { policyConfig } from '../../../store/policy_details/selectors'; import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index eb48fc3ffa28b1..4ca72da6abfdf2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; -import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { policyConfig } from '../../../store/policy_details/selectors'; import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx index 4c358bc3e3a46a..4d177c5cf6d30a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx @@ -9,11 +9,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - Immutable, - OperatingSystem, - PolicyOperatingSystem, -} from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { Immutable, PolicyOperatingSystem } from '../../../../../../../common/endpoint/types'; import { BehaviorProtectionOSes } from '../../../types'; import { ConfigForm } from '../../components/config_form'; import { RadioButtons } from '../components/radio_buttons'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index e348b1b8022292..9f9ac475d41866 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -9,13 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; -import { - Immutable, - OperatingSystem, - PolicyOperatingSystem, -} from '../../../../../../../common/endpoint/types'; +import { Immutable, PolicyOperatingSystem } from '../../../../../../../common/endpoint/types'; import { MalwareProtectionOSes } from '../../../types'; import { ConfigForm } from '../../components/config_form'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx index 82a14e4fa98084..ae3b2f7a1abc65 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx @@ -9,13 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; -import { - Immutable, - OperatingSystem, - PolicyOperatingSystem, -} from '../../../../../../../common/endpoint/types'; +import { Immutable, PolicyOperatingSystem } from '../../../../../../../common/endpoint/types'; import { MemoryProtectionOSes } from '../../../types'; import { ConfigForm } from '../../components/config_form'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx index 22266ef7351a04..da1b2e06b3a092 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx @@ -9,13 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; -import { - Immutable, - OperatingSystem, - PolicyOperatingSystem, -} from '../../../../../../../common/endpoint/types'; +import { Immutable, PolicyOperatingSystem } from '../../../../../../../common/endpoint/types'; import { RansomwareProtectionOSes } from '../../../types'; import { ConfigForm } from '../../components/config_form'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts index 8069d18169dd1c..f440a0a394631c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts @@ -18,13 +18,15 @@ import { } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { - ConditionEntry, ConditionEntryField, + OperatingSystem, + TrustedAppEntryTypes, +} from '@kbn/securitysolution-utils'; +import { + ConditionEntry, EffectScope, NewTrustedApp, - OperatingSystem, TrustedApp, - TrustedAppEntryTypes, UpdateTrustedApp, } from '../../../../../common/endpoint/types'; import { tagsToEffectScope } from '../../../../../common/endpoint/service/trusted_apps/mapping'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 3f9e9d53f69e47..22aeedca7312c7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { ConditionEntryField } from '@kbn/securitysolution-utils'; import { ConditionEntry, - ConditionEntryField, EffectScope, GlobalEffectScope, MacosLinuxConditionEntry, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts index 363da5cd273907..431894274ee009 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { - ConditionEntry, - ConditionEntryField, - NewTrustedApp, - OperatingSystem, -} from '../../../../../common/endpoint/types'; +import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; +import { ConditionEntry, NewTrustedApp } from '../../../../../common/endpoint/types'; import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts index 3c2f177520271b..32e1867db567c7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts @@ -6,7 +6,8 @@ */ import { combineReducers, createStore } from 'redux'; -import { TrustedApp, OperatingSystem } from '../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { TrustedApp } from '../../../../../common/endpoint/types'; import { RoutingAction } from '../../../../common/store/routing'; import { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx index 9d6c35d64b2d5e..4ea42c896847c8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx @@ -8,11 +8,8 @@ import { shallow, mount } from 'enzyme'; import React from 'react'; import { keys } from 'lodash'; -import { - ConditionEntry, - ConditionEntryField, - OperatingSystem, -} from '../../../../../../../common/endpoint/types'; +import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; +import { ConditionEntry } from '../../../../../../../common/endpoint/types'; import { ConditionEntryInput } from '.'; import { EuiSuperSelectProps } from '@elastic/eui'; @@ -53,6 +50,7 @@ describe('Condition entry input', () => { /> ); + // @ts-ignore it.each(keys(ConditionEntryField).map((k) => [k]))( 'should call on change for field input with value %s', (field) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx index f487a38401ef03..4f4f89b80f28dc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx @@ -16,13 +16,8 @@ import { EuiSuperSelectOption, EuiText, } from '@elastic/eui'; - -import { - ConditionEntry, - ConditionEntryField, - OperatorFieldIds, - OperatingSystem, -} from '../../../../../../../common/endpoint/types'; +import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; +import { ConditionEntry, OperatorFieldIds } from '../../../../../../../common/endpoint/types'; import { CONDITION_FIELD_DESCRIPTION, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx index fb7135b1173e0f..aed69128847f60 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx @@ -9,7 +9,8 @@ import React, { memo } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHideFor, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ConditionEntry, OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { ConditionEntry } from '../../../../../../../common/endpoint/types'; import { AndOrBadge } from '../../../../../../common/components/and_or_badge'; import { ConditionEntryInput, ConditionEntryInputProps } from '../condition_entry_input'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx index cc2c51c5f4c405..68dd43fa411521 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx @@ -9,11 +9,8 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; import { fireEvent, getByTestId } from '@testing-library/dom'; -import { - ConditionEntryField, - NewTrustedApp, - OperatingSystem, -} from '../../../../../../common/endpoint/types'; +import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; +import { NewTrustedApp } from '../../../../../../common/endpoint/types'; import { AppContextTestRender, createAppRootMockRenderer, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index 7cff989f008a0a..2812bdc9c3c0ba 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -18,20 +18,23 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { + hasSimpleExecutableName, + isPathValid, + ConditionEntryField, + OperatingSystem, +} from '@kbn/securitysolution-utils'; import { EuiFormProps } from '@elastic/eui/src/components/form/form'; + import { ConditionEntry, - ConditionEntryField, EffectScope, MacosLinuxConditionEntry, MaybeImmutable, NewTrustedApp, - OperatingSystem, } from '../../../../../../common/endpoint/types'; import { isValidHash, - isPathValid, - hasSimpleExecutableName, getDuplicateFields, } from '../../../../../../common/endpoint/service/trusted_apps/validations'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index dd9b8fe4324c13..3d8a56ad743155 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -6,10 +6,10 @@ */ import { i18n } from '@kbn/i18n'; +import { ConditionEntryField } from '@kbn/securitysolution-utils'; import { MacosLinuxConditionEntry, WindowsConditionEntry, - ConditionEntryField, OperatorFieldIds, } from '../../../../../common/endpoint/types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 82169fcd19c104..3666164676ee3e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -11,11 +11,8 @@ import { TrustedAppsPage } from './trusted_apps_page'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { fireEvent } from '@testing-library/dom'; import { MiddlewareActionSpyHelper } from '../../../../common/store/test_utils'; -import { - ConditionEntryField, - OperatingSystem, - TrustedApp, -} from '../../../../../common/endpoint/types'; +import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; +import { TrustedApp } from '../../../../../common/endpoint/types'; import { HttpFetchOptions, HttpFetchOptionsWithPath } from 'kibana/public'; import { isFailedResourceState, isLoadedResourceState } from '../state'; import { forceHTMLElementOffsetWidth } from '../../../components/effected_policy_select/test_utils'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx index 71d6f6253010d7..4a0b7d0fe0501e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx @@ -15,6 +15,7 @@ import { mockAlertDetailsData } from '../../../../common/components/event_detail import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { mockCasesContract } from '../../../../../../cases/public/mocks'; const ecsData: Ecs = { _id: '1', @@ -114,6 +115,7 @@ describe('event details footer component', () => { }, query: jest.fn(), }, + cases: mockCasesContract(), }, }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index b6e6aa40876ccf..7a94dcef31cf7b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -12,6 +12,7 @@ import { TestProviders, mockTimelineModel, mockTimelineData } from '../../../../ import { Actions } from '.'; import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { mockCasesContract } from '../../../../../../../cases/public/mocks'; jest.mock('../../../../../detections/components/user_info', () => ({ useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), @@ -43,6 +44,7 @@ jest.mock('../../../../../common/lib/kibana', () => ({ siem: { crud_alerts: true, read_alerts: true }, }, }, + cases: mockCasesContract(), uiSettings: { get: jest.fn(), }, 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 index b9e04060881d4f..890175ac8daf9a 100644 --- 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 @@ -20,6 +20,7 @@ import { getDefaultControlColumn } from '../control_columns'; import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeline_control_columns'; import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; import { getActionsColumnWidth } from '../../../../../../../timelines/public'; +import { mockCasesContract } from '../../../../../../../cases/public/mocks'; jest.mock('../../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; @@ -40,6 +41,7 @@ jest.mock('../../../../../common/lib/kibana', () => ({ siem: { crud_alerts: true, read_alerts: true }, }, }, + cases: mockCasesContract(), }, }), useToasts: jest.fn().mockReturnValue({ 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 index 66a140987475c5..f616b4afc2af5f 100644 --- 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 @@ -24,12 +24,12 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { timelineActions } from '../../../store/timeline'; import { ColumnHeaderOptions, TimelineTabs } from '../../../../../common/types/timeline'; import { defaultRowRenderers } from './renderers'; -import { mockCasesContext } from '../../../../common/mock/mock_cases_context'; jest.mock('../../../../common/lib/kibana/hooks'); jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../../../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../../../common/lib/kibana'); + const mockCasesContract = jest.requireActual('../../../../../../cases/public/mocks'); return { ...originalModule, useKibana: jest.fn().mockReturnValue({ @@ -41,9 +41,7 @@ jest.mock('../../../../common/lib/kibana', () => { siem: { crud_alerts: true, read_alerts: true }, }, }, - cases: { - getCasesContext: () => mockCasesContext, - }, + cases: mockCasesContract.mockCasesContract(), data: { search: jest.fn(), query: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx index 43622b7e453652..943abc88cf2b05 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx @@ -23,7 +23,7 @@ import { useTimelineEventsDetails } from '../../../containers/details/index'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks'; import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components'; -import { mockCasesContext } from '../../../../common/mock/mock_cases_context'; +import { mockCasesContext } from '../../../../../../cases/public/mocks/mock_cases_context'; jest.mock('../../../containers/index', () => ({ useTimelineEvents: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx index ffe50f935b9fec..954f54fdba7770 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx @@ -24,7 +24,7 @@ import { mockSourcererScope } from '../../../../common/containers/sourcerer/mock import { PinnedTabContentComponent, Props as PinnedTabContentComponentProps } from '.'; import { Direction } from '../../../../../common/search_strategy'; import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components'; -import { mockCasesContext } from '../../../../common/mock/mock_cases_context'; +import { mockCasesContext } from '../../../../../../cases/public/mocks/mock_cases_context'; jest.mock('../../../containers/index', () => ({ useTimelineEvents: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index 019bedacbffe88..c16afa945cc08d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -26,7 +26,7 @@ import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks'; import { Direction } from '../../../../../common/search_strategy'; import * as helpers from '../helpers'; -import { mockCasesContext } from '../../../../common/mock/mock_cases_context'; +import { mockCasesContext } from '../../../../../../cases/public/mocks/mock_cases_context'; jest.mock('../../../containers/index', () => ({ useTimelineEvents: jest.fn(), diff --git a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts index 7f18c0b40fed7c..05baa6d4ade041 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts @@ -9,7 +9,11 @@ import { run, RunFn, createFailError } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; import { AxiosError } from 'axios'; import pMap from 'p-map'; -import type { CreateExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { + CreateExceptionListItemSchema, + CreateExceptionListSchema, + ExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, ENDPOINT_EVENT_FILTERS_LIST_ID, @@ -41,8 +45,8 @@ export const cli = () => { kibana: 'http://elastic:changeme@localhost:5601', }, help: ` - --count Number of event filters to create. Default: 10 - --kibana The URL to kibana including credentials. Default: http://elastic:changeme@localhost:5601 + --count Number of event filters to create. Default: 10 + --kibana The URL to kibana including credentials. Default: http://elastic:changeme@localhost:5601 `, }, } @@ -77,7 +81,25 @@ const createEventFilters: RunFn = async ({ flags, log }) => { await pMap( Array.from({ length: flags.count as unknown as number }), () => { - const body = eventGenerator.generateEventFilterForCreate(); + let options: Partial = {}; + const listSize = (flags.count ?? 10) as number; + const randomN = eventGenerator.randomN(listSize); + if (randomN > Math.floor(listSize / 2)) { + const os = eventGenerator.randomOSFamily() as ExceptionListItemSchema['os_types'][number]; + options = { + os_types: [os], + entries: [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: os === 'windows' ? 'C:\\Fol*\\file.*' : '/usr/*/*.dmg', + }, + ], + }; + } + + const body = eventGenerator.generateEventFilterForCreate(options); if (isArtifactByPolicy(body)) { const nmExceptions = Math.floor(Math.random() * 3) || 1; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index d4a486539855bf..b23c2fe08bf103 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -13,6 +13,7 @@ import type { ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { hasSimpleExecutableName, OperatingSystem } from '@kbn/securitysolution-utils'; import { ENDPOINT_EVENT_FILTERS_LIST_ID, @@ -20,7 +21,6 @@ import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, } from '@kbn/securitysolution-list-constants'; -import { OperatingSystem } from '../../../../common/endpoint/types'; import { ExceptionListClient } from '../../../../../lists/server'; import { InternalArtifactCompleteSchema, @@ -40,7 +40,6 @@ import { WrappedTranslatedExceptionList, wrappedTranslatedExceptionList, } from '../../schemas'; -import { hasSimpleExecutableName } from '../../../../common/endpoint/service/trusted_apps/validations'; export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, @@ -227,7 +226,7 @@ function getMatcherWildcardFunction({ field: string; os: ExceptionListItemSchema['os_types'][number]; }): TranslatedEntryMatchWildcardMatcher { - return field.endsWith('.caseless') + return field.endsWith('.caseless') || field.endsWith('.text') ? os === 'linux' ? 'wildcard_cased' : 'wildcard_caseless' diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 95d0c8b607cb66..c878c02df2a081 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -449,6 +449,7 @@ describe('ManifestManager', () => { } }); + // test('Builds manifest with policy specific exception list items for trusted apps', async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index 11396864d802d7..95dcb918cd1658 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -195,6 +195,7 @@ export const previewRulesRoute = async ( }), savedObjectsClient: context.core.savedObjects.client, scopedClusterClient: context.core.elasticsearch.client, + uiSettingsClient: context.core.uiSettings.client, }, spaceId, startedAt: startedAt.toDate(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/file_ex.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/file_ex.json new file mode 100644 index 00000000000000..b20857bb07543b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/file_ex.json @@ -0,0 +1 @@ +{"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"aggregatable":false,"description":"For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.","columnHeaderType":"not-filtered","id":"message","category":"base","type":"string","example":"Hello World"},{"aggregatable":true,"description":"Process name. Sometimes called program name or similar.","columnHeaderType":"not-filtered","id":"process.name","category":"process","type":"string","example":"ssh"},{"aggregatable":true,"description":"The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","example":"user-password-change"},{"aggregatable":true,"description":"Name of the file including the extension, without the directory.","columnHeaderType":"not-filtered","id":"file.name","category":"file","type":"string","example":"example.png"},{"columnHeaderType":"not-filtered","id":"file.size"},{"columnHeaderType":"not-filtered","id":"file.path"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"file","queryMatch":{"displayValue":null,"field":"event.category","displayField":null,"value":"file","operator":":"},"id":"timeline-1-fd6cfcf0-cfbd-4a42-b58e-9efccca7ecdd","type":"default","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.group.id}","queryMatch":{"displayValue":null,"field":"signal.group.id","displayField":null,"value":"{signal.group.id}","operator":":"},"id":"timeline-1-072c4726-d198-41c5-a3dc-561062c454a9","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.original_event.id}","queryMatch":{"field":"signal.original_event.id","value":"{signal.original_event.id}","operator":":"},"id":"timeline-1-dba415d2-9968-4961-8b0f-a381c3d28c87","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"template","kqlQuery":{"filterQuery":null},"title":"Comprehensive File Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":"4d4c0b59-ea83-483f-b8c1-8c360ee53c5c","templateTimelineVersion":2,"created":1618433758898,"createdBy":"1674059739","updated":1618500709024,"updatedBy":"elastic","dateRange":{"start":"2021-04-13T20:55:34.156Z","end":"2021-04-14T20:55:34.157Z"},"indexNames":[],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson index 84972a837a3e82..bf2e16ede0def2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/index.ndjson @@ -9,6 +9,10 @@ // Do not hand edit. Run that script to regenerate package information instead {"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"kibana.alert.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"","queryMatch":{"displayValue":"endpoint","field":"agent.type","displayField":"agent.type","value":"endpoint","operator":":"},"id":"timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568","type":"default","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Endpoint Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"db366523-f1c6-4c1f-8731-6ce5ed9e5717","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735857110,"createdBy":"Elastic","updated":1611609999115,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"aggregatable":false,"description":"For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.","columnHeaderType":"not-filtered","id":"message","category":"base","type":"string","example":"Hello World"},{"aggregatable":true,"description":"Process name. Sometimes called program name or similar.","columnHeaderType":"not-filtered","id":"process.name","category":"process","type":"string","example":"ssh"},{"aggregatable":true,"description":"The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","example":"user-password-change"},{"aggregatable":true,"description":"Name of the file including the extension, without the directory.","columnHeaderType":"not-filtered","id":"file.name","category":"file","type":"string","example":"example.png"},{"columnHeaderType":"not-filtered","id":"file.size"},{"columnHeaderType":"not-filtered","id":"file.path"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"file","queryMatch":{"displayValue":null,"field":"event.category","displayField":null,"value":"file","operator":":"},"id":"timeline-1-fd6cfcf0-cfbd-4a42-b58e-9efccca7ecdd","type":"default","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.group.id}","queryMatch":{"displayValue":null,"field":"signal.group.id","displayField":null,"value":"{signal.group.id}","operator":":"},"id":"timeline-1-072c4726-d198-41c5-a3dc-561062c454a9","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.original_event.id}","queryMatch":{"field":"signal.original_event.id","value":"{signal.original_event.id}","operator":":"},"id":"timeline-1-dba415d2-9968-4961-8b0f-a381c3d28c87","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"template","kqlQuery":{"filterQuery":null},"title":"Comprehensive File Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":"4d4c0b59-ea83-483f-b8c1-8c360ee53c5c","templateTimelineVersion":2,"created":1618433758898,"createdBy":"1674059739","updated":1618500709024,"updatedBy":"elastic","dateRange":{"start":"2021-04-13T20:55:34.156Z","end":"2021-04-14T20:55:34.157Z"},"indexNames":[],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"aggregatable":false,"description":"For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.","columnHeaderType":"not-filtered","id":"message","category":"base","type":"string","example":"Hello World"},{"aggregatable":true,"description":"The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","example":"user-password-change"},{"aggregatable":true,"description":"In the OSI Model this would be the Network Layer. ipv4, ipv6, ipsec, pim, etc The field value must be normalized to lowercase for querying. See the documentation section \"Implementing ECS\".","columnHeaderType":"not-filtered","id":"network.type","category":"network","type":"string","example":"ipv4"},{"aggregatable":true,"description":"Same as network.iana_number, but instead using the Keyword name of the transport layer (udp, tcp, ipv6-icmp, etc.) The field value must be normalized to lowercase for querying. See the documentation section \"Implementing ECS\".","columnHeaderType":"not-filtered","id":"network.transport","category":"network","type":"string","example":"tcp"},{"aggregatable":true,"description":"Direction of the network traffic. Recommended values are: * inbound * outbound * internal * external * unknown When mapping events from a host-based monitoring context, populate this field from the host's point of view. When mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.","columnHeaderType":"not-filtered","id":"network.direction","category":"network","type":"string","example":"inbound"},{"aggregatable":true,"description":"IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip"},{"columnHeaderType":"not-filtered","id":"source.port"},{"aggregatable":true,"description":"IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip"},{"columnHeaderType":"not-filtered","id":"destination.port"},{"aggregatable":true,"description":"Name of the host. It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"network","queryMatch":{"displayValue":null,"field":"event.category","displayField":null,"value":"network","operator":":"},"id":"timeline-1-dbab0164-2150-47a1-a66f-75ebafe24d5c","type":"default","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.group.id}","queryMatch":{"displayValue":null,"field":"signal.group.id","displayField":null,"value":"{signal.group.id}","operator":":"},"id":"timeline-1-15b52ead-4956-4ed0-bd12-e137eaf4467e","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.original_event.id}","queryMatch":{"field":"signal.original_event.id","value":"{signal.original_event.id}","operator":":"},"id":"timeline-1-2164774f-6409-4ac4-b73c-907914baf058","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"template","kqlQuery":{"filterQuery":null},"title":"Comprehensive Network Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":"300afc76-072d-4261-864d-4149714bf3f1","templateTimelineVersion":2,"created":1618432938016,"createdBy":"1674059739","updated":1618500782465,"updatedBy":"elastic","dateRange":{"start":"2021-04-13T20:40:01.909Z","end":"2021-04-14T20:40:01.909Z"},"indexNames":[],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} {"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"kibana.alert.rule.description","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","searchable":null,"example":"user-password-change"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the source (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Port of the source.","columnHeaderType":"not-filtered","id":"source.port","category":"source","type":"number","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"IP address of the destination (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"destination.port","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"host.name","searchable":null}],"dataProviders":[{"and":[{"enabled":true,"excluded":false,"id":"timeline-1-e37e37c5-a6e7-4338-af30-47bfbc3c0e1e","kqlQuery":"","name":"{destination.ip}","queryMatch":{"displayField":"destination.ip","displayValue":"{destination.ip}","field":"destination.ip","operator":":","value":"{destination.ip}"},"type":"template"}],"enabled":true,"excluded":false,"id":"timeline-1-ec778f01-1802-40f0-9dfb-ed8de1f656cb","kqlQuery":"","name":"{source.ip}","queryMatch":{"displayField":"source.ip","displayValue":"{source.ip}","field":"source.ip","operator":":","value":"{source.ip}"},"type":"template"}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Network Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"91832785-286d-4ebe-b884-1a208d111a70","dateRange":{"start":1588255858373,"end":1588256218373},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735573866,"createdBy":"Elastic","updated":1611609960850,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"aggregatable":false,"description":"For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.","columnHeaderType":"not-filtered","id":"message","category":"base","type":"string","example":"Hello World"},{"columnHeaderType":"not-filtered","id":"process.code_signature.status"},{"columnHeaderType":"not-filtered","id":"process.code_signature.subject_name"},{"columnHeaderType":"not-filtered","id":"process.command_line"},{"columnHeaderType":"not-filtered","id":"process.executable"},{"columnHeaderType":"not-filtered","id":"process.name"},{"columnHeaderType":"not-filtered","id":"process.parent.name"},{"columnHeaderType":"not-filtered","id":"event.action"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"process","queryMatch":{"displayValue":null,"field":"event.category","displayField":null,"value":"process","operator":":"},"id":"timeline-1-44c387b3-14e2-4493-9702-869311bb7fb1","type":"default","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.group.id}","queryMatch":{"displayValue":null,"field":"signal.group.id","displayField":null,"value":"{signal.group.id}","operator":":"},"id":"timeline-1-690a2939-b1d3-417b-8332-281147d8d0a0","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.original_event.id}","queryMatch":{"field":"signal.original_event.id","value":"{signal.original_event.id}","operator":":"},"id":"timeline-1-8a39602a-78f6-4de2-a3b1-60c1112701c4","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"template","kqlQuery":{"filterQuery":null},"title":"Comprehensive Process Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":"e70679c2-6cde-4510-9764-4823df18f7db","templateTimelineVersion":2,"created":1618431743530,"createdBy":"1674059739","updated":1618500593280,"updatedBy":"elastic","dateRange":{"start":"2021-04-13T20:21:59.161Z","end":"2021-04-14T20:21:59.161Z"},"indexNames":[],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} {"savedObjectId":null,"version":null,"columns":[{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"@timestamp","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"kibana.alert.rule.description","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"event.action","searchable":null},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.name","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"The working directory of the process.","columnHeaderType":"not-filtered","id":"process.working_directory","category":"process","type":"string","searchable":null,"example":"/home/alice"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","searchable":null,"example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"name":null,"columnHeaderType":"not-filtered","id":"process.pid","searchable":null},{"indexes":null,"aggregatable":true,"name":null,"description":"Absolute path to the process executable.","columnHeaderType":"not-filtered","id":"process.parent.executable","category":"process","type":"string","searchable":null,"example":"/usr/bin/ssh"},{"indexes":null,"aggregatable":true,"name":null,"description":"Array of process arguments.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.parent.args","category":"process","type":"string","searchable":null,"example":"[\"ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"indexes":null,"aggregatable":true,"name":null,"description":"Process id.","columnHeaderType":"not-filtered","id":"process.parent.pid","category":"process","type":"number","searchable":null,"example":"4242"},{"indexes":null,"aggregatable":true,"name":null,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","searchable":null,"example":"albert"},{"indexes":null,"aggregatable":true,"name":null,"description":"Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string","searchable":null}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"{process.name}","queryMatch":{"displayValue":null,"field":"process.name","displayField":null,"value":"{process.name}","operator":":"},"id":"timeline-1-8622010a-61fb-490d-b162-beac9c36a853","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"title":"Generic Process Timeline","timelineType":"template","templateTimelineVersion":2,"templateTimelineId":"76e52245-7519-4251-91ab-262fb1a1728c","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1594735629389,"createdBy":"Elastic","updated":1611609848602,"updatedBy":"Elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} +{"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"aggregatable":false,"description":"For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.","columnHeaderType":"not-filtered","id":"message","category":"base","type":"string","example":"Hello World"},{"aggregatable":true,"description":"Process name. Sometimes called program name or similar.","columnHeaderType":"not-filtered","id":"process.name","category":"process","type":"string","example":"ssh"},{"aggregatable":true,"description":"The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","example":"user-password-change"},{"aggregatable":true,"description":"Hive-relative path of keys.","columnHeaderType":"not-filtered","id":"registry.key","category":"registry","type":"string","example":"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe"},{"aggregatable":true,"description":"Name of the value written.","columnHeaderType":"not-filtered","id":"registry.value","category":"registry","type":"string","example":"Debugger"},{"aggregatable":true,"description":"Full path, including hive, key and value","columnHeaderType":"not-filtered","id":"registry.path","category":"registry","type":"string","example":"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe\\Debugger"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"registry","queryMatch":{"displayValue":null,"field":"event.category","displayField":null,"value":"registry","operator":":"},"id":"timeline-1-f9cfd451-4826-4042-9814-d42e17e4a982","type":"default","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.group.id}","queryMatch":{"displayValue":null,"field":"signal.group.id","displayField":null,"value":"{signal.group.id}","operator":":"},"id":"timeline-1-b940d03a-db9b-4f0f-9e1e-26076a74f482","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.original_event.id}","queryMatch":{"field":"signal.original_event.id","value":"{signal.original_event.id}","operator":":"},"id":"timeline-1-51ebb99b-7723-4451-834a-b5d922684d6e","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"template","kqlQuery":{"filterQuery":null},"title":"Comprehensive Registry Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":"3e47ef71-ebfc-4520-975c-cb27fc090799","templateTimelineVersion":2,"created":1618433313346,"createdBy":"1674059739","updated":1618500745983,"updatedBy":"elastic","dateRange":{"start":"2021-04-13T20:48:06.119Z","end":"2021-04-14T20:48:06.120Z"},"indexNames":[],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} {"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp"},{"columnHeaderType":"not-filtered","id":"kibana.alert.rule.description"},{"aggregatable":true,"description":"The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","example":"user-password-change"},{"aggregatable":true,"description":"Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.","columnHeaderType":"not-filtered","id":"process.args","category":"process","type":"string","example":"[\"/usr/bin/ssh\",\"-l\",\"user\",\"10.0.0.16\"]"},{"columnHeaderType":"not-filtered","id":"process.pid"},{"aggregatable":true,"description":"IP address of the source (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip"},{"aggregatable":true,"description":"Port of the source.","columnHeaderType":"not-filtered","id":"source.port","category":"source","type":"number"},{"aggregatable":true,"description":"IP address of the destination (IPv4 or IPv6).","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip"},{"columnHeaderType":"not-filtered","id":"destination.port"},{"aggregatable":true,"description":"Short name or login of the user.","columnHeaderType":"not-filtered","id":"user.name","category":"user","type":"string","example":"albert"},{"columnHeaderType":"not-filtered","id":"host.name"}],"dataProviders":[{"excluded":false,"and":[{"excluded":false,"kqlQuery":"","name":"{threat.enrichments.matched.type}","queryMatch":{"displayValue":null,"field":"threat.enrichments.matched.type","displayField":null,"value":"{threat.enrichments.matched.type}","operator":":"},"id":"timeline-1-ae18ef4b-f690-4122-a24d-e13b6818fba8","type":"template","enabled":true},{"excluded":false,"kqlQuery":"","name":"{threat.enrichments.matched.field}","queryMatch":{"displayValue":null,"field":"threat.enrichments.matched.field","displayField":null,"value":"{threat.enrichments.matched.field}","operator":":"},"id":"timeline-1-7b4cf27e-6788-4d8e-9188-7687f0eba0f2","type":"template","enabled":true}],"kqlQuery":"","name":"{threat.enrichments.matched.atomic}","queryMatch":{"displayValue":null,"field":"threat.enrichments.matched.atomic","displayField":null,"value":"{threat.enrichments.matched.atomic}","operator":":"},"id":"timeline-1-7db7d278-a80a-4853-971a-904319c50777","type":"template","enabled":true}],"description":"This Timeline template is for alerts generated by Indicator Match detection rules.","eqlOptions":{"eventCategoryField":"event.category","tiebreakerField":"","timestampField":"@timestamp","query":"","size":100},"eventType":"alert","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"kind":"kuery","expression":""},"serializedQuery":""}},"dataViewId": "security-solution","indexNames":[],"title":"Generic Threat Match Timeline","timelineType":"template","templateTimelineVersion":3,"templateTimelineId":"495ad7a7-316e-4544-8a0f-9c098daee76e","dateRange":{"start":1588161020848,"end":1588162280848},"savedQueryId":null,"sort":[{"sortDirection":"desc","columnId":"@timestamp"}],"created":1616696609311,"createdBy":"elastic","updated":1616788372794,"updatedBy":"elastic","eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network_ex.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network_ex.json new file mode 100644 index 00000000000000..b3ecd20cc25e4c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/network_ex.json @@ -0,0 +1 @@ +{"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"aggregatable":false,"description":"For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.","columnHeaderType":"not-filtered","id":"message","category":"base","type":"string","example":"Hello World"},{"aggregatable":true,"description":"The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","example":"user-password-change"},{"aggregatable":true,"description":"In the OSI Model this would be the Network Layer. ipv4, ipv6, ipsec, pim, etc The field value must be normalized to lowercase for querying. See the documentation section \"Implementing ECS\".","columnHeaderType":"not-filtered","id":"network.type","category":"network","type":"string","example":"ipv4"},{"aggregatable":true,"description":"Same as network.iana_number, but instead using the Keyword name of the transport layer (udp, tcp, ipv6-icmp, etc.) The field value must be normalized to lowercase for querying. See the documentation section \"Implementing ECS\".","columnHeaderType":"not-filtered","id":"network.transport","category":"network","type":"string","example":"tcp"},{"aggregatable":true,"description":"Direction of the network traffic. Recommended values are: * inbound * outbound * internal * external * unknown When mapping events from a host-based monitoring context, populate this field from the host's point of view. When mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.","columnHeaderType":"not-filtered","id":"network.direction","category":"network","type":"string","example":"inbound"},{"aggregatable":true,"description":"IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.","columnHeaderType":"not-filtered","id":"source.ip","category":"source","type":"ip"},{"columnHeaderType":"not-filtered","id":"source.port"},{"aggregatable":true,"description":"IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.","columnHeaderType":"not-filtered","id":"destination.ip","category":"destination","type":"ip"},{"columnHeaderType":"not-filtered","id":"destination.port"},{"aggregatable":true,"description":"Name of the host. It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.","columnHeaderType":"not-filtered","id":"host.name","category":"host","type":"string"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"network","queryMatch":{"displayValue":null,"field":"event.category","displayField":null,"value":"network","operator":":"},"id":"timeline-1-dbab0164-2150-47a1-a66f-75ebafe24d5c","type":"default","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.group.id}","queryMatch":{"displayValue":null,"field":"signal.group.id","displayField":null,"value":"{signal.group.id}","operator":":"},"id":"timeline-1-15b52ead-4956-4ed0-bd12-e137eaf4467e","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.original_event.id}","queryMatch":{"field":"signal.original_event.id","value":"{signal.original_event.id}","operator":":"},"id":"timeline-1-2164774f-6409-4ac4-b73c-907914baf058","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"template","kqlQuery":{"filterQuery":null},"title":"Comprehensive Network Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":"300afc76-072d-4261-864d-4149714bf3f1","templateTimelineVersion":2,"created":1618432938016,"createdBy":"1674059739","updated":1618500782465,"updatedBy":"elastic","dateRange":{"start":"2021-04-13T20:40:01.909Z","end":"2021-04-14T20:40:01.909Z"},"indexNames":[],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process_ex.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process_ex.json new file mode 100644 index 00000000000000..6627d445ec9c6e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/process_ex.json @@ -0,0 +1 @@ +{"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"aggregatable":false,"description":"For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.","columnHeaderType":"not-filtered","id":"message","category":"base","type":"string","example":"Hello World"},{"columnHeaderType":"not-filtered","id":"process.code_signature.status"},{"columnHeaderType":"not-filtered","id":"process.code_signature.subject_name"},{"columnHeaderType":"not-filtered","id":"process.command_line"},{"columnHeaderType":"not-filtered","id":"process.executable"},{"columnHeaderType":"not-filtered","id":"process.name"},{"columnHeaderType":"not-filtered","id":"process.parent.name"},{"columnHeaderType":"not-filtered","id":"event.action"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"process","queryMatch":{"displayValue":null,"field":"event.category","displayField":null,"value":"process","operator":":"},"id":"timeline-1-44c387b3-14e2-4493-9702-869311bb7fb1","type":"default","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.group.id}","queryMatch":{"displayValue":null,"field":"signal.group.id","displayField":null,"value":"{signal.group.id}","operator":":"},"id":"timeline-1-690a2939-b1d3-417b-8332-281147d8d0a0","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.original_event.id}","queryMatch":{"field":"signal.original_event.id","value":"{signal.original_event.id}","operator":":"},"id":"timeline-1-8a39602a-78f6-4de2-a3b1-60c1112701c4","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"template","kqlQuery":{"filterQuery":null},"title":"Comprehensive Process Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":"e70679c2-6cde-4510-9764-4823df18f7db","templateTimelineVersion":2,"created":1618431743530,"createdBy":"1674059739","updated":1618500593280,"updatedBy":"elastic","dateRange":{"start":"2021-04-13T20:21:59.161Z","end":"2021-04-14T20:21:59.161Z"},"indexNames":[],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/registry_ex.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/registry_ex.json new file mode 100644 index 00000000000000..42599a8c9eb511 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_timelines/registry_ex.json @@ -0,0 +1 @@ +{"savedObjectId":null,"version":null,"columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"aggregatable":false,"description":"For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.","columnHeaderType":"not-filtered","id":"message","category":"base","type":"string","example":"Hello World"},{"aggregatable":true,"description":"Process name. Sometimes called program name or similar.","columnHeaderType":"not-filtered","id":"process.name","category":"process","type":"string","example":"ssh"},{"aggregatable":true,"description":"The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.","columnHeaderType":"not-filtered","id":"event.action","category":"event","type":"string","example":"user-password-change"},{"aggregatable":true,"description":"Hive-relative path of keys.","columnHeaderType":"not-filtered","id":"registry.key","category":"registry","type":"string","example":"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe"},{"aggregatable":true,"description":"Name of the value written.","columnHeaderType":"not-filtered","id":"registry.value","category":"registry","type":"string","example":"Debugger"},{"aggregatable":true,"description":"Full path, including hive, key and value","columnHeaderType":"not-filtered","id":"registry.path","category":"registry","type":"string","example":"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe\\Debugger"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"registry","queryMatch":{"displayValue":null,"field":"event.category","displayField":null,"value":"registry","operator":":"},"id":"timeline-1-f9cfd451-4826-4042-9814-d42e17e4a982","type":"default","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.group.id}","queryMatch":{"displayValue":null,"field":"signal.group.id","displayField":null,"value":"{signal.group.id}","operator":":"},"id":"timeline-1-b940d03a-db9b-4f0f-9e1e-26076a74f482","type":"template","enabled":true},{"excluded":false,"and":[],"kqlQuery":"","name":"{signal.original_event.id}","queryMatch":{"field":"signal.original_event.id","value":"{signal.original_event.id}","operator":":"},"id":"timeline-1-51ebb99b-7723-4451-834a-b5d922684d6e","type":"template","enabled":true}],"description":"","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"template","kqlQuery":{"filterQuery":null},"title":"Comprehensive Registry Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":"3e47ef71-ebfc-4520-975c-cb27fc090799","templateTimelineVersion":2,"created":1618433313346,"createdBy":"1674059739","updated":1618500745983,"updatedBy":"elastic","dateRange":{"start":"2021-04-13T20:48:06.119Z","end":"2021-04-14T20:48:06.120Z"},"indexNames":[],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"eventNotes":[],"globalNotes":[],"pinnedEventIds":[],"status":"immutable"} \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index f8270c53b07ae2..99230627cb6b82 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -176,7 +176,13 @@ export const searchAfterAndBulkCreate = async ({ buildRuleMessage(`enrichedEvents.hits.hits: ${enrichedEvents.hits.hits.length}`) ); - sendAlertTelemetryEvents(logger, eventsTelemetry, enrichedEvents, buildRuleMessage); + sendAlertTelemetryEvents( + logger, + eventsTelemetry, + enrichedEvents, + createdItems, + buildRuleMessage + ); } if (!hasSortId) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts index 991378983e1b28..36bb90936620bc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { selectEvents } from './send_telemetry_events'; +import { selectEvents, enrichEndpointAlertsSignalID } from './send_telemetry_events'; describe('sendAlertTelemetry', () => { it('selectEvents', () => { @@ -33,6 +33,9 @@ describe('sendAlertTelemetry', () => { data_stream: { dataset: 'endpoint.events', }, + event: { + id: 'foo', + }, }, }, { @@ -47,6 +50,9 @@ describe('sendAlertTelemetry', () => { dataset: 'endpoint.alerts', other: 'x', }, + event: { + id: 'bar', + }, }, }, { @@ -58,13 +64,52 @@ describe('sendAlertTelemetry', () => { '@timestamp': 'x', key3: 'hello', data_stream: {}, + event: { + id: 'baz', + }, + }, + }, + { + _index: 'y', + _type: 'y', + _id: 'y', + _score: 0, + _source: { + '@timestamp': 'y', + key3: 'hello', + data_stream: { + dataset: 'endpoint.alerts', + other: 'y', + }, + event: { + id: 'not-in-map', + }, + }, + }, + { + _index: 'z', + _type: 'z', + _id: 'z', + _score: 0, + _source: { + '@timestamp': 'z', + key3: 'no-event-id', + data_stream: { + dataset: 'endpoint.alerts', + other: 'z', + }, }, }, ], }, }; - - const sources = selectEvents(filteredEvents); + const joinMap = new Map([ + ['foo', '1234'], + ['bar', 'abcd'], + ['baz', '4567'], + ]); + const subsetEvents = selectEvents(filteredEvents); + const sources = enrichEndpointAlertsSignalID(subsetEvents, joinMap); expect(sources).toStrictEqual([ { '@timestamp': 'x', @@ -73,6 +118,31 @@ describe('sendAlertTelemetry', () => { dataset: 'endpoint.alerts', other: 'x', }, + event: { + id: 'bar', + }, + signal_id: 'abcd', + }, + { + '@timestamp': 'y', + key3: 'hello', + data_stream: { + dataset: 'endpoint.alerts', + other: 'y', + }, + event: { + id: 'not-in-map', + }, + signal_id: undefined, + }, + { + '@timestamp': 'z', + key3: 'no-event-id', + data_stream: { + dataset: 'endpoint.alerts', + other: 'z', + }, + signal_id: undefined, }, ]); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts index 5904f943183c39..fc3aed36939cde 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts @@ -11,14 +11,17 @@ import { BuildRuleMessage } from './rule_messages'; import { SignalSearchResponse, SignalSource } from './types'; import { Logger } from '../../../../../../../src/core/server'; -export interface SearchResultWithSource { +interface SearchResultSource { _source: SignalSource; } +type CreatedSignalId = string; +type AlertId = string; + export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEvent[] { // @ts-expect-error @elastic/elasticsearch _source is optional const sources: TelemetryEvent[] = filteredEvents.hits.hits.map(function ( - obj: SearchResultWithSource + obj: SearchResultSource ): TelemetryEvent { return obj._source; }); @@ -27,20 +30,49 @@ export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEve return sources.filter((obj: TelemetryEvent) => obj.data_stream?.dataset === 'endpoint.alerts'); } +export function enrichEndpointAlertsSignalID( + events: TelemetryEvent[], + signalIdMap: Map +): TelemetryEvent[] { + return events.map(function (obj: TelemetryEvent): TelemetryEvent { + obj.signal_id = undefined; + if (obj?.event?.id !== undefined) { + obj.signal_id = signalIdMap.get(obj.event.id); + } + return obj; + }); +} + export function sendAlertTelemetryEvents( logger: Logger, eventsTelemetry: ITelemetryEventsSender | undefined, filteredEvents: SignalSearchResponse, + createdEvents: SignalSource[], buildRuleMessage: BuildRuleMessage ) { if (eventsTelemetry === undefined) { return; } - const sources = selectEvents(filteredEvents); + let selectedEvents = selectEvents(filteredEvents); + if (selectedEvents.length > 0) { + // Create map of ancenstor_id -> alert_id + let signalIdMap = new Map(); + /* eslint-disable no-param-reassign */ + signalIdMap = createdEvents.reduce((signalMap, obj) => { + const ancestorId = obj['kibana.alert.original_event.id']?.toString(); + const alertId = obj._id?.toString(); + if (ancestorId !== null && ancestorId !== undefined && alertId !== undefined) { + signalMap = signalIdMap.set(ancestorId, alertId); + } + + return signalMap; + }, new Map()); + selectedEvents = enrichEndpointAlertsSignalID(selectedEvents, signalIdMap); + } try { - eventsTelemetry.queueTelemetryEvents(sources); + eventsTelemetry.queueTelemetryEvents(selectedEvents); } catch (exc) { logger.error(buildRuleMessage(`[-] queing telemetry events failed ${exc}`)); } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts index 452717f1efb4f7..bd41bc454e8760 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts @@ -108,6 +108,7 @@ const allowlistBaseEventFields: AllowlistFields = { export const allowlistEventFields: AllowlistFields = { _id: true, '@timestamp': true, + signal_id: true, agent: true, Endpoint: true, /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 70852aa3093c67..d055f3843d479a 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -35,6 +35,7 @@ describe('TelemetryEventsSender', () => { { event: { kind: 'alert', + id: 'test', }, dns: { question: { @@ -108,6 +109,7 @@ describe('TelemetryEventsSender', () => { { event: { kind: 'alert', + id: 'test', }, dns: { question: { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index 35b701552b6ba4..35b531ae6941c3 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -58,6 +58,10 @@ export interface TelemetryEvent { }; }; license?: ESLicense; + event?: { + id?: string; + kind?: string; + }; } // EP Policy Response diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts index 3f53e59c348ab8..293066a3a18242 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts @@ -9,6 +9,7 @@ import { KibanaRequest } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { isEqual } from 'lodash/fp'; import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import { ExceptionItemLikeOptions } from '../types'; import { getEndpointAuthzInitialState } from '../../../../common/endpoint/service/authz'; @@ -16,7 +17,6 @@ import { getPolicyIdsFromArtifact, isArtifactByPolicy, } from '../../../../common/endpoint/service/artifacts'; -import { OperatingSystem } from '../../../../common/endpoint/types'; import { EndpointArtifactExceptionValidationError } from './errors'; import type { FeatureKeys } from '../../../endpoint/services/feature_usage/service'; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts index 28bc408165d4bc..64545847838631 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; import { BaseValidator, BasicEndpointExceptionDataSchema } from './base_validator'; import { EndpointArtifactExceptionValidationError } from './errors'; import { ExceptionItemLikeOptions } from '../types'; @@ -16,7 +17,6 @@ import { UpdateExceptionListItemOptions, } from '../../../../../lists/server'; import { isValidIPv4OrCIDR } from '../../../../common/endpoint/utils/is_valid_ip'; -import { OperatingSystem } from '../../../../common/endpoint/types'; function validateIp(value: string) { if (!isValidIPv4OrCIDR(value)) { diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts index d7ca2c0f05672b..fc69153f0b21b5 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -8,17 +8,14 @@ import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { schema, TypeOf } from '@kbn/config-schema'; import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { OperatingSystem, TrustedAppEntryTypes } from '@kbn/securitysolution-utils'; import { BaseValidator } from './base_validator'; import { ExceptionItemLikeOptions } from '../types'; import { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, } from '../../../../../lists/server'; -import { - ConditionEntry, - OperatingSystem, - TrustedAppEntryTypes, -} from '../../../../common/endpoint/types'; +import { ConditionEntry } from '../../../../common/endpoint/types'; import { getDuplicateFields, isValidHash, diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index 75f6803ddf0b4d..f0488d26f5f16e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -49,7 +49,7 @@ const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` `; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px -const STANDALONE_ID = 'standalone-t-grid'; +export const STANDALONE_ID = 'standalone-t-grid'; const EMPTY_DATA_PROVIDERS: DataProvider[] = []; const TitleText = styled.span` diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts index 0638425564a70e..a20bebe531d164 100644 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ b/x-pack/plugins/timelines/public/container/use_update_alerts.ts @@ -45,11 +45,11 @@ export const useUpdateAlertsStatus = ( body: JSON.stringify({ status, query }), }); } else { - const { body } = await http.post<{ body: estypes.UpdateByQueryResponse }>( + const response = await http.post( RAC_ALERTS_BULK_UPDATE_URL, { body: JSON.stringify({ index, status, query }) } ); - return body; + return response; } }, }; diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index c6e0e13c4dcb4d..8fc81a57e2b86e 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -12,6 +12,7 @@ import * as i18n from '../components/t_grid/translations'; import type { AlertStatus, StatusBulkActionsProps } from '../../common/types/timeline'; import { useUpdateAlertsStatus } from '../container/use_update_alerts'; import { useAppToasts } from './use_app_toasts'; +import { STANDALONE_ID } from '../components/t_grid/standalone'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { return { bool: { filter: { terms: { _id: eventIds } } } }; @@ -28,7 +29,7 @@ export const useStatusBulkActionItems = ({ onUpdateFailure, timelineId, }: StatusBulkActionsProps) => { - const { updateAlertStatus } = useUpdateAlertsStatus(timelineId != null); + const { updateAlertStatus } = useUpdateAlertsStatus(timelineId !== STANDALONE_ID); const { addSuccess, addError, addWarning } = useAppToasts(); const onAlertStatusUpdateSuccess = useCallback( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json new file mode 100644 index 00000000000000..96327533d4e291 --- /dev/null +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -0,0 +1,25710 @@ +{ + "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": { + "units": "year" + }, + "months": { + "units": "month" + }, + "days": { + "units": "day" + }, + "hours": { + "units": "hour" + }, + "minutes": { + "units": "minute" + }, + "seconds": { + "units": "second" + } + } + }, + "messages": { + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.expFunction.markdown": "\nÉlève *e* à la puissance n.\n\nExemple : calculer la fonction exponentielle naturelle\n\n`exp(last_value(duration))`\n ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formula.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 ", + "xpack.lens.formulaDocumentation.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 ", + "xpack.lens.formulaDocumentation.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 ", + "xpack.lens.formulaDocumentation.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 ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.count.documentation.markdown": "\nCalcule le nombre de documents.\n\nExemple : calculer le nombre de documents :\n\"count()\"\n\nExemple : calculer le nombre de documents correspondant à un filtre spécifique :\n\"count(kql='price > 500')\"\n ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.lastValue.documentation.markdown": "\nRenvoie la valeur d'un champ du dernier document, triée par le champ d'heure par défaut du modèle d'indexation.\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 ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.indexPattern.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 de total\n\"sum(bytes) / overall_sum(sum(bytes))\"\n ", + "xpack.lens.indexPattern.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 ", + "xpack.lens.app.addToLibrary": "Enregistrer dans la bibliothèque", + "xpack.lens.app.cancel": "Annuler", + "xpack.lens.app.cancelButtonAriaLabel": "Retour à la dernière application sans enregistrer les modifications", + "xpack.lens.app.docLoadingError": "Erreur lors du chargement du document enregistré", + "xpack.lens.app.downloadButtonAriaLabel": "Télécharger les données en fichier CSV", + "xpack.lens.app.downloadButtonFormulasWarning": "Votre fichier CSV contient des caractères que les applications de feuilles de calcul pourraient considérer comme des formules.", + "xpack.lens.app.downloadCSV": "Télécharger en tant que CSV", + "xpack.lens.app.save": "Enregistrer", + "xpack.lens.app.saveAndReturn": "Enregistrer et revenir", + "xpack.lens.app.saveAndReturnButtonAriaLabel": "Enregistrer la visualisation Lens en cours et revenir à l'application précédente", + "xpack.lens.app.saveAs": "Enregistrer sous", + "xpack.lens.app.saveButtonAriaLabel": "Enregistrer la visualisation Lens en cours", + "xpack.lens.app.saveModalType": "Visualisation Lens", + "xpack.lens.app.saveVisualization.successNotificationText": "'{visTitle}' enregistré", + "xpack.lens.app.unsavedFilename": "non enregistré", + "xpack.lens.app.unsavedWorkMessage": "Quitter Lens avec un travail non enregistré ?", + "xpack.lens.app.unsavedWorkTitle": "Modifications non enregistrées", + "xpack.lens.app.updatePanel": "Mettre à jour le panneau sur {originatingAppName}", + "xpack.lens.app404": "404 Page introuvable", + "xpack.lens.breadcrumbsByValue": "Modifier la visualisation", + "xpack.lens.breadcrumbsCreate": "Créer", + "xpack.lens.breadcrumbsTitle": "Visualiser la bibliothèque", + "xpack.lens.chartSwitch.dataLossDescription": "La sélection de ce type de visualisation entraînera une perte partielle des sélections de configuration actuellement appliquées.", + "xpack.lens.chartSwitch.dataLossLabel": "Avertissement", + "xpack.lens.chartSwitch.experimentalLabel": "Expérimental", + "xpack.lens.chartSwitch.noResults": "Résultats introuvables pour {term}.", + "xpack.lens.chartTitle.unsaved": "Visualisation non enregistrée", + "xpack.lens.chartWarnings.number": "{warningsCount} {warningsCount, plural, one {avertissement} other {avertissements}}", + "xpack.lens.configPanel.addLayerButton": "Ajouter un calque", + "xpack.lens.configPanel.color.tooltip.auto": "Lens choisit automatiquement des couleurs à votre place sauf si vous spécifiez une couleur personnalisée.", + "xpack.lens.configPanel.color.tooltip.custom": "Effacez la couleur personnalisée pour revenir au mode \"Auto\".", + "xpack.lens.configPanel.color.tooltip.disabled": "Les séries individuelles n'acceptent pas les couleurs personnalisées lorsque le calque inclut l'option \"Répartir par\".", + "xpack.lens.configPanel.selectVisualization": "Sélectionner une visualisation", + "xpack.lens.configPanel.visualizationType": "Type de visualisation", + "xpack.lens.configure.configurePanelTitle": "{groupLabel}", + "xpack.lens.configure.editConfig": "Modifier la configuration {label}", + "xpack.lens.configure.emptyConfig": "Ajouter ou glisser-déposer un champ", + "xpack.lens.configure.invalidConfigTooltip": "Configuration non valide.", + "xpack.lens.configure.invalidConfigTooltipClick": "Cliquez pour en savoir plus.", + "xpack.lens.customBucketContainer.dragToReorder": "Faire glisser pour réorganiser", + "xpack.lens.dataPanelWrapper.switchDatasource": "Basculer vers la source de données", + "xpack.lens.datatable.addLayer": "Ajouter un calque de visualisation", + "xpack.lens.datatable.breakdownColumns": "Colonnes", + "xpack.lens.datatable.breakdownColumns.description": "Divisez les colonnes d'indicateurs par champ. Il est recommandé de conserver un faible nombre de colonnes pour éviter le défilement horizontal.", + "xpack.lens.datatable.breakdownRows": "Lignes", + "xpack.lens.datatable.breakdownRows.description": "Divisez le tableau par champ. Cette opération est recommandée pour les répartitions à cardinalité élevée.", + "xpack.lens.datatable.conjunctionSign": " & ", + "xpack.lens.datatable.expressionHelpLabel": "Outil de rendu de tableaux de données", + "xpack.lens.datatable.groupLabel": "Valeur tabulaire et unique", + "xpack.lens.datatable.label": "Tableau", + "xpack.lens.datatable.metrics": "Indicateurs", + "xpack.lens.datatable.suggestionLabel": "En tant que tableau", + "xpack.lens.datatable.titleLabel": "Titre", + "xpack.lens.datatable.visualizationName": "Tableau de données", + "xpack.lens.datatable.visualizationOf": "Tableau {operations}", + "xpack.lens.datatypes.boolean": "booléen", + "xpack.lens.datatypes.date": "date", + "xpack.lens.datatypes.geoPoint": "geo_point", + "xpack.lens.datatypes.geoShape": "geo_shape", + "xpack.lens.datatypes.histogram": "histogramme", + "xpack.lens.datatypes.ipAddress": "IP", + "xpack.lens.datatypes.number": "numéro", + "xpack.lens.datatypes.record": "enregistrement", + "xpack.lens.datatypes.string": "chaîne", + "xpack.lens.deleteLayerAriaLabel": "Supprimer le calque {index}", + "xpack.lens.dimensionContainer.close": "Fermer", + "xpack.lens.dimensionContainer.closeConfiguration": "Fermer la configuration", + "xpack.lens.discover.visualizeFieldLegend": "Visualiser le champ", + "xpack.lens.dragDrop.altOption": "Alt/Option", + "xpack.lens.dragDrop.announce.cancelled": "Mouvement annulé. {label} revenu à sa position initiale", + "xpack.lens.dragDrop.announce.cancelledItem": "Mouvement annulé. {label} revenu au groupe {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.duplicated": "{label} dupliqué dans le groupe {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.duplicateIncompatible": "Copie de {label} convertie en {nextLabel} et ajoutée au groupe {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.moveCompatible": "{label} déplacé dans le groupe {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.moveIncompatible": "{label} converti en {nextLabel} et déplacé dans le groupe {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.reordered": "{label} réorganisé dans le groupe {groupLabel} de la position {prevPosition} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible": "Copie de {label} convertie en {nextLabel} et {dropLabel} remplacé dans le groupe {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.replaceIncompatible": "{label} converti en {nextLabel} et {dropLabel} remplacé dans le groupe {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.swapCompatible": "{label} déplacé dans {dropGroupLabel} à la position {dropPosition} et {dropLabel} dans {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.dropped.swapIncompatible": "{label} converti en {nextLabel} dans le groupe {groupLabel} à la position {position} et permuté avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}", + "xpack.lens.dragDrop.announce.droppedDefault": "{label} ajouté dans le groupe {dropGroupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.droppedNoPosition": "{label} ajouté à {dropLabel}", + "xpack.lens.dragDrop.announce.duplicate.short": " Maintenez la touche Alt ou Option enfoncée pour dupliquer.", + "xpack.lens.dragDrop.announce.duplicated.replace": "{dropLabel} remplacé par {label} dans {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible": "{dropLabel} remplacé par une copie de {label} dans {groupLabel} à la position {position}", + "xpack.lens.dragDrop.announce.lifted": "{label} levé", + "xpack.lens.dragDrop.announce.selectedTarget.default": "Ajoutez {label} au groupe {dropGroupLabel} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour ajouter", + "xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition": "Ajoutez {label} à {dropLabel}. Appuyer sur la barre d'espace ou sur Entrée pour ajouter", + "xpack.lens.dragDrop.announce.selectedTarget.duplicated": "Dupliquez {label} dans le groupe {dropGroupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer", + "xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup": "Dupliquez {label} dans le groupe {dropGroupLabel} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour dupliquer", + "xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible": "Convertissez la copie de {label} en {nextLabel} et ajoutez-la au groupe {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer", + "xpack.lens.dragDrop.announce.selectedTarget.moveCompatible": "Déplacez {label} dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour déplacer", + "xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain": "Vous faites glisser {label} de {groupLabel} à la position {position} vers la position {dropPosition} dans le groupe {dropGroupLabel}. Appuyez sur la barre d'espace ou sur Entrée pour déplacer.{duplicateCopy}{swapCopy}", + "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible": "Convertissez {label} en {nextLabel} et déplacez-le dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour déplacer", + "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain": "Vous faites glisser {label} de {groupLabel} à la position {position} vers la position {dropPosition} dans le groupe {dropGroupLabel}. Appuyez sur la barre d'espace ou sur Entrée pour convertir {label} en {nextLabel} et déplacer.{duplicateCopy}{swapCopy}", + "xpack.lens.dragDrop.announce.selectedTarget.noSelected": "Aucune cible sélectionnée. Utiliser les touches fléchées pour sélectionner une cible", + "xpack.lens.dragDrop.announce.selectedTarget.reordered": "Réorganisez {label} dans le groupe {groupLabel} de la position {prevPosition} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour réorganiser", + "xpack.lens.dragDrop.announce.selectedTarget.reorderedBack": "{label} revenu à sa position initiale {prevPosition}", + "xpack.lens.dragDrop.announce.selectedTarget.replace": "Remplacez {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} avec {label}. Appuyez sur la barre d'espace ou sur Entrée pour remplacer.", + "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible": "Dupliquez {label} et remplacez {dropLabel} dans {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer et remplacer", + "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible": "Convertissez la copie de {label} en {nextLabel} et remplacez {dropLabel} dans le groupe {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer et remplacer", + "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible": "Convertissez {label} en {nextLabel} et remplacez {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour remplacer", + "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour convertir {label} en {nextLabel} et remplacer {dropLabel}.{duplicateCopy}{swapCopy}", + "xpack.lens.dragDrop.announce.selectedTarget.replaceMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour remplacer {dropLabel} par {label}.{duplicateCopy}{swapCopy}", + "xpack.lens.dragDrop.announce.selectedTarget.swapCompatible": "Permutez {label} dans le groupe {groupLabel} à la position {position} avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenir la touche Maj enfoncée tout en appuyant sur la barre d'espace ou sur Entrée pour permuter", + "xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible": "Convertir {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} et permutez avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenir la touche Maj enfoncée tout en appuyant sur la barre d'espace ou sur Entrée pour permuter", + "xpack.lens.dragDrop.announce.swap.short": " Maintenez la touche Maj enfoncée pour permuter.", + "xpack.lens.dragDrop.duplicate": "Dupliquer", + "xpack.lens.dragDrop.keyboardInstructions": "Appuyez sur la barre d'espace ou sur Entrée pour commencer à faire glisser. Lors du glissement, utilisez les touches fléchées gauche/droite pour vous déplacer entre les cibles de dépôt. Appuyez à nouveau sur la barre d'espace ou sur Entrée pour terminer.", + "xpack.lens.dragDrop.keyboardInstructionsReorder": "Appuyez sur la barre d'espace ou sur Entrée pour commencer à faire glisser. Lors du glissement, utilisez les touches fléchées haut/bas pour réorganiser les éléments dans le groupe et les touches gauche/droite pour choisir les cibles de dépôt à l'extérieur du groupe. Appuyez à nouveau sur la barre d'espace ou sur Entrée pour terminer.", + "xpack.lens.dragDrop.shift": "Déplacer", + "xpack.lens.dragDrop.swap": "Permuter", + "xpack.lens.dynamicColoring.customPalette.deleteButtonAriaLabel": "Supprimer", + "xpack.lens.editorFrame.buildExpressionError": "Une erreur inattendue s'est produite lors de la préparation du graphique", + "xpack.lens.editorFrame.colorIndicatorLabel": "Couleur de cette dimension : {hex}", + "xpack.lens.editorFrame.configurationFailureMoreErrors": " +{errors} {errors, plural, one {erreur} other {erreurs}}", + "xpack.lens.editorFrame.dataFailure": "Une erreur s'est produite lors du chargement des données.", + "xpack.lens.editorFrame.emptyWorkspace": "Déposer quelques champs ici pour commencer", + "xpack.lens.editorFrame.emptyWorkspaceHeading": "Lens est un nouvel outil permettant de créer des visualisations", + "xpack.lens.editorFrame.emptyWorkspaceSimple": "Déposer le champ ici", + "xpack.lens.editorFrame.expandRenderingErrorButton": "Afficher les détails de l'erreur", + "xpack.lens.editorFrame.expressionFailure": "Une erreur s'est produite dans l'expression", + "xpack.lens.editorFrame.expressionFailureMessage": "Erreur de requête : {type}, {reason}", + "xpack.lens.editorFrame.expressionFailureMessageWithContext": "Erreur de requête : {type}, {reason} dans {context}", + "xpack.lens.editorFrame.expressionMissingDatasource": "Impossible de trouver la source de données pour la visualisation", + "xpack.lens.editorFrame.expressionMissingVisualizationType": "Type de visualisation non trouvé.", + "xpack.lens.editorFrame.goToForums": "Formuler des requêtes et donner un retour", + "xpack.lens.editorFrame.invisibleIndicatorLabel": "Cette dimension n'est pas visible actuellement dans le graphique", + "xpack.lens.editorFrame.networkErrorMessage": "Erreur réseau, réessayez plus tard ou contactez votre administrateur.", + "xpack.lens.editorFrame.noColorIndicatorLabel": "Cette dimension n'a pas de couleur individuelle", + "xpack.lens.editorFrame.paletteColorIndicatorLabel": "Cette dimension utilise une palette", + "xpack.lens.editorFrame.previewErrorLabel": "L'aperçu du rendu a échoué", + "xpack.lens.editorFrame.suggestionPanelTitle": "Suggestions", + "xpack.lens.editorFrame.workspaceLabel": "Espace de travail", + "xpack.lens.embeddable.failure": "Impossible d'afficher la visualisation", + "xpack.lens.embeddable.fixErrors": "Effectuez des modifications dans l'éditeur Lens pour corriger l'erreur", + "xpack.lens.embeddable.moreErrors": "Effectuez des modifications dans l'éditeur Lens pour afficher plus d'erreurs", + "xpack.lens.embeddableDisplayName": "lens", + "xpack.lens.fieldFormats.longSuffix.d": "par jour", + "xpack.lens.fieldFormats.longSuffix.h": "par heure", + "xpack.lens.fieldFormats.longSuffix.m": "par minute", + "xpack.lens.fieldFormats.longSuffix.s": "par seconde", + "xpack.lens.fieldFormats.suffix.d": "/d", + "xpack.lens.fieldFormats.suffix.h": "/h", + "xpack.lens.fieldFormats.suffix.m": "/m", + "xpack.lens.fieldFormats.suffix.s": "/s", + "xpack.lens.fieldFormats.suffix.title": "Suffixe", + "xpack.lens.filterBy.removeLabel": "Supprimer le filtre", + "xpack.lens.fittingFunctionsDescription.carry": "Remplit les blancs avec la dernière valeur", + "xpack.lens.fittingFunctionsDescription.linear": "Remplit les blancs avec une ligne", + "xpack.lens.fittingFunctionsDescription.lookahead": "Remplit les blancs avec la valeur suivante", + "xpack.lens.fittingFunctionsDescription.none": "Ne remplit pas les blancs", + "xpack.lens.fittingFunctionsDescription.zero": "Remplit les blancs avec des zéros", + "xpack.lens.fittingFunctionsTitle.carry": "Dernier", + "xpack.lens.fittingFunctionsTitle.linear": "Linéaire", + "xpack.lens.fittingFunctionsTitle.lookahead": "Suivant", + "xpack.lens.fittingFunctionsTitle.none": "Masquer", + "xpack.lens.fittingFunctionsTitle.zero": "Zéro", + "xpack.lens.formula.base": "base", + "xpack.lens.formula.decimals": "décimales", + "xpack.lens.formula.disableWordWrapLabel": "Désactiver le renvoi à la ligne des mots", + "xpack.lens.formula.editorHelpInlineHideLabel": "Masquer la référence des fonctions", + "xpack.lens.formula.editorHelpInlineHideToolTip": "Masquer la référence des fonctions", + "xpack.lens.formula.editorHelpInlineShowToolTip": "Afficher la référence des fonctions", + "xpack.lens.formula.editorHelpOverlayToolTip": "Référence des fonctions", + "xpack.lens.formula.fullScreenEnterLabel": "Développer", + "xpack.lens.formula.fullScreenExitLabel": "Réduire", + "xpack.lens.formula.kqlExtraArguments": "[kql]?: string, [lucene]?: string", + "xpack.lens.formula.left": "gauche", + "xpack.lens.formula.max": "max", + "xpack.lens.formula.min": "min", + "xpack.lens.formula.number": "numéro", + "xpack.lens.formula.optionalArgument": "Facultatif. La valeur par défaut est {defaultValue}", + "xpack.lens.formula.requiredArgument": "Requis", + "xpack.lens.formula.right": "droite", + "xpack.lens.formula.shiftExtraArguments": "[shift]?: string", + "xpack.lens.formula.string": "chaîne", + "xpack.lens.formula.value": "valeur", + "xpack.lens.formulaCommonFormulaDocumentation": "Les formules les plus courantes divisent deux valeurs pour produire un pourcentage. Pour obtenir un affichage correct, définissez \"Format de valeur\" sur \"pourcent\".", + "xpack.lens.formulaDocumentation.columnCalculationSection": "Calculs de colonnes", + "xpack.lens.formulaDocumentation.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.", + "xpack.lens.formulaDocumentation.elasticsearchSection": "Elasticsearch", + "xpack.lens.formulaDocumentation.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.", + "xpack.lens.formulaDocumentation.filterRatio": "Rapport de filtre", + "xpack.lens.formulaDocumentation.header": "Référence de formule", + "xpack.lens.formulaDocumentation.mathSection": "Mathématique", + "xpack.lens.formulaDocumentation.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.", + "xpack.lens.formulaDocumentation.percentOfTotal": "Pourcentage du total", + "xpack.lens.formulaDocumentation.weekOverWeek": "Semaine après semaine", + "xpack.lens.formulaDocumentationHeading": "Fonctionnement", + "xpack.lens.formulaEnableWordWrapLabel": "Activer le renvoi à la ligne des mots", + "xpack.lens.formulaErrorCount": "{count} {count, plural, one {erreur} other {erreurs}}", + "xpack.lens.formulaExampleMarkdown": "Exemples", + "xpack.lens.formulaFrequentlyUsedHeading": "Formules courantes", + "xpack.lens.formulaPlaceholderText": "Saisissez une formule en combinant des fonctions avec la fonction mathématique, telle que :", + "xpack.lens.formulaSearchPlaceholder": "Rechercher des fonctions", + "xpack.lens.formulaWarningCount": "{count} {count, plural, one {avertissement} other {avertissements}}", + "xpack.lens.functions.counterRate.args.byHelpText": "Colonne selon laquelle le calcul du taux de compteur sera divisé", + "xpack.lens.functions.counterRate.args.inputColumnIdHelpText": "Colonne pour laquelle le taux de compteur sera calculé", + "xpack.lens.functions.counterRate.args.outputColumnIdHelpText": "Colonne dans laquelle le taux de compteur résultant sera stocké", + "xpack.lens.functions.counterRate.args.outputColumnNameHelpText": "Nom de la colonne dans laquelle le taux de compteur résultant sera stocké", + "xpack.lens.functions.counterRate.help": "Calcule le taux de compteur d'une colonne dans un tableau de données", + "xpack.lens.functions.lastValue.missingSortField": "Ce modèle d'indexation ne contient aucun champ de date", + "xpack.lens.functions.mergeTables.help": "Aide pour fusionner n'importe quel nombre de tableaux Kibana en un tableau unique et l'exposer via un adaptateur d'inspecteur", + "xpack.lens.functions.renameColumns.help": "Aide pour renommer les colonnes d'un tableau de données", + "xpack.lens.functions.renameColumns.idMap.help": "Un objet encodé JSON dans lequel les clés sont les anciens ID de colonne et les valeurs sont les nouveaux ID correspondants. Tous les autres ID de colonne sont conservés.", + "xpack.lens.functions.timeScale.dateColumnMissingMessage": "L'ID de colonne de date {columnId} n'existe pas.", + "xpack.lens.functions.timeScale.timeInfoMissingMessage": "Impossible de récupérer les informations d'histogramme des dates", + "xpack.lens.geoFieldWorkspace.dropMessage": "Déposer le champ ici pour l'ouvrir dans Maps", + "xpack.lens.geoFieldWorkspace.dropZoneLabel": "zone de dépôt pour ouvrir dans Maps", + "xpack.lens.heatmap.addLayer": "Ajouter un calque de visualisation", + "xpack.lens.heatmap.cellValueLabel": "Valeur de cellule", + "xpack.lens.heatmap.groupLabel": "Carte thermique", + "xpack.lens.heatmap.heatmapLabel": "Carte thermique", + "xpack.lens.heatmap.horizontalAxisLabel": "Axe horizontal", + "xpack.lens.heatmap.verticalAxisLabel": "Axe vertical", + "xpack.lens.heatmapChart.legendVisibility.hide": "Masquer", + "xpack.lens.heatmapChart.legendVisibility.show": "Afficher", + "xpack.lens.heatmapVisualization.arrayValuesWarningMessage": "{label} contient des valeurs de tableau. Le rendu de votre visualisation peut ne pas se présenter comme attendu.", + "xpack.lens.heatmapVisualization.heatmapGroupLabel": "Carte thermique", + "xpack.lens.heatmapVisualization.heatmapLabel": "Carte thermique", + "xpack.lens.heatmapVisualization.missingXAccessorLongMessage": "La configuration de l'axe horizontal est manquante.", + "xpack.lens.heatmapVisualization.missingXAccessorShortMessage": "Axe horizontal manquant.", + "xpack.lens.indexPattern.advancedSettings": "Ajouter des options avancées", + "xpack.lens.indexPattern.allFieldsLabel": "Tous les champs", + "xpack.lens.indexPattern.allFieldsLabelHelp": "Les champs disponibles ont des données dans les 500 premiers documents correspondant à vos filtres. Pour afficher tous les filtres, développez les champs vides. Certains types de champ ne peuvent pas être visualisés dans Lens, y compris les champ de texte intégral et champs géographiques.", + "xpack.lens.indexPattern.availableFieldsLabel": "Champs disponibles", + "xpack.lens.indexPattern.avg": "Moyenne", + "xpack.lens.indexPattern.avg.description": "Agrégation d'indicateurs à valeur unique qui calcule la moyenne des valeurs numériques extraites des documents agrégés", + "xpack.lens.indexPattern.avgOf": "Moyenne de {name}", + "xpack.lens.indexPattern.bytesFormatLabel": "Octets (1024)", + "xpack.lens.indexPattern.calculations.dateHistogramErrorMessage": "{name} requiert un histogramme des dates pour fonctionner. Ajoutez un histogramme des dates ou sélectionnez une autre fonction.", + "xpack.lens.indexPattern.calculations.layerDataType": "{name} est désactivé pour ce type de calque.", + "xpack.lens.indexPattern.cardinality": "Compte unique", + "xpack.lens.indexPattern.cardinality.signature": "champ : chaîne", + "xpack.lens.indexPattern.cardinalityOf": "Compte unique de {name}", + "xpack.lens.indexPattern.chooseField": "Sélectionner un champ", + "xpack.lens.indexPattern.chooseFieldLabel": "Pour utiliser cette fonction, sélectionnez un champ.", + "xpack.lens.indexPattern.chooseSubFunction": "Choisir une sous-fonction", + "xpack.lens.indexPattern.columnFormatLabel": "Format de valeur", + "xpack.lens.indexPattern.columnLabel": "Afficher le nom", + "xpack.lens.indexPattern.count": "Compte", + "xpack.lens.indexPattern.counterRate": "Taux de compteur", + "xpack.lens.indexPattern.counterRate.signature": "indicateur : nombre", + "xpack.lens.indexPattern.CounterRateOf": "Taux de compteur de {name}", + "xpack.lens.indexPattern.countOf": "Nombre d'enregistrements", + "xpack.lens.indexPattern.cumulative_sum.signature": "indicateur : nombre", + "xpack.lens.indexPattern.cumulativeSum": "Somme cumulée", + "xpack.lens.indexPattern.cumulativeSumOf": "Somme cumulée de {name}", + "xpack.lens.indexPattern.dateHistogram": "Histogramme des dates", + "xpack.lens.indexPattern.dateHistogram.autoAdvancedExplanation": "L'intervalle suit cette logique :", + "xpack.lens.indexPattern.dateHistogram.autoBasicExplanation": "L'histogramme des dates automatique divise un champ de données en groupes par intervalle.", + "xpack.lens.indexPattern.dateHistogram.autoBoundHeader": "Intervalle cible mesuré", + "xpack.lens.indexPattern.dateHistogram.autoHelpText": "Fonctionnement", + "xpack.lens.indexPattern.dateHistogram.autoInterval": "Personnaliser l'intervalle de temps", + "xpack.lens.indexPattern.dateHistogram.autoIntervalHeader": "Intervalle utilisé", + "xpack.lens.indexPattern.dateHistogram.autoLongerExplanation": "Pour choisir l'intervalle, Lens divise la plage temporelle spécifiée par le paramètre {targetBarSetting}. Lens calcule le meilleur intervalle pour vos données. Par exemple 30m, 1h et 12. Le nombre maximal de barres est défini par la valeur {maxBarSetting}.", + "xpack.lens.indexPattern.dateHistogram.days": "jours", + "xpack.lens.indexPattern.dateHistogram.hours": "heures", + "xpack.lens.indexPattern.dateHistogram.milliseconds": "millisecondes", + "xpack.lens.indexPattern.dateHistogram.minimumInterval": "Intervalle minimal", + "xpack.lens.indexPattern.dateHistogram.minutes": "minutes", + "xpack.lens.indexPattern.dateHistogram.month": "mois", + "xpack.lens.indexPattern.dateHistogram.moreThanYear": "Plus d'un an", + "xpack.lens.indexPattern.dateHistogram.restrictedInterval": "Intervalle fixé à {intervalValue} en raison de restrictions d'agrégation.", + "xpack.lens.indexPattern.dateHistogram.seconds": "secondes", + "xpack.lens.indexPattern.dateHistogram.titleHelp": "Fonctionnement de l'histogramme des dates automatique", + "xpack.lens.indexPattern.dateHistogram.upTo": "Jusqu'à", + "xpack.lens.indexPattern.dateHistogram.week": "semaine", + "xpack.lens.indexPattern.dateHistogram.year": "an", + "xpack.lens.indexPattern.decimalPlacesLabel": "Décimales", + "xpack.lens.indexPattern.defaultFormatLabel": "Par défaut", + "xpack.lens.indexPattern.derivative": "Différences", + "xpack.lens.indexPattern.derivativeOf": "Différences de {name}", + "xpack.lens.indexPattern.differences.signature": "indicateur : nombre", + "xpack.lens.indexPattern.editFieldLabel": "Modifier le champ de modèle d'indexation", + "xpack.lens.indexPattern.emptyDimensionButton": "Dimension vide", + "xpack.lens.indexPattern.emptyFieldsLabel": "Champs vides", + "xpack.lens.indexPattern.emptyFieldsLabelHelp": "Les champs vides ne contenaient aucune valeur dans les 500 premiers documents basés sur vos filtres.", + "xpack.lens.indexPattern.existenceErrorAriaLabel": "La récupération de l'existence a échoué", + "xpack.lens.indexPattern.existenceErrorLabel": "Impossible de charger les informations de champ", + "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "La récupération de l'existence a expiré", + "xpack.lens.indexPattern.existenceTimeoutLabel": "Les informations de champ ont pris trop de temps", + "xpack.lens.indexPattern.fieldDistributionLabel": "Distribution", + "xpack.lens.indexPattern.fieldItem.visualizeGeoFieldLinkText": "Visualiser dans Maps", + "xpack.lens.indexPattern.fieldItemTooltip": "Effectuez un glisser-déposer pour visualiser.", + "xpack.lens.indexPattern.fieldNoOperation": "Le champ {field} ne peut pas être utilisé sans opération", + "xpack.lens.indexPattern.fieldNotFound": "Champ {invalidField} introuvable", + "xpack.lens.indexPattern.fieldPanelEmptyStringValue": "Chaîne vide", + "xpack.lens.indexPattern.fieldPlaceholder": "Champ", + "xpack.lens.indexPattern.fieldStatsButtonAriaLabel": "Prévisualiser {fieldName} : {fieldType}", + "xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "Ce champ ne comporte aucune donnée mais vous pouvez toujours effectuer un glisser-déposer pour visualiser.", + "xpack.lens.indexPattern.fieldStatsButtonLabel": "Cliquez pour obtenir un aperçu du champ, ou effectuez un glisser-déposer pour visualiser.", + "xpack.lens.indexPattern.fieldStatsCountLabel": "Compte", + "xpack.lens.indexPattern.fieldStatsDisplayToggle": "Basculer soit", + "xpack.lens.indexPattern.fieldStatsLimited": "Le résumé des informations n'est pas disponible pour les champs de type de gamme.", + "xpack.lens.indexPattern.fieldStatsNoData": "Ce champ est vide car il n'existe pas dans les 500 documents échantillonnés. L'ajout de ce champ à la configuration peut générer un graphique vide.", + "xpack.lens.indexPattern.fieldTimeDistributionLabel": "Répartition du temps", + "xpack.lens.indexPattern.fieldTopValuesLabel": "Valeurs les plus élevées", + "xpack.lens.indexPattern.fieldWrongType": "Le champ {invalidField} a un type incorrect", + "xpack.lens.indexPattern.filterBy.clickToEdit": "Cliquer pour modifier", + "xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(vide)", + "xpack.lens.indexPattern.filterBy.label": "Filtrer par", + "xpack.lens.indexPattern.filters": "Filtres", + "xpack.lens.indexPattern.filters.addaFilter": "Ajouter un filtre", + "xpack.lens.indexPattern.filters.clickToEdit": "Cliquer pour modifier", + "xpack.lens.indexPattern.filters.isInvalid": "Cette requête n'est pas valide", + "xpack.lens.indexPattern.filters.label.placeholder": "Tous les enregistrements", + "xpack.lens.indexPattern.filters.queryPlaceholderKql": "{example}", + "xpack.lens.indexPattern.filters.queryPlaceholderLucene": "{example}", + "xpack.lens.indexPattern.filters.removeFilter": "Retirer un filtre", + "xpack.lens.indexPattern.formulaExpressionNotHandled": "L'opération {operation} dans la formule ne comprend pas les paramètres suivants : {params}", + "xpack.lens.indexPattern.formulaExpressionParseError": "La formule {expression} ne peut pas être analysée", + "xpack.lens.indexPattern.formulaExpressionWrongType": "Les paramètres de l'opération {operation} dans la formule ont un type incorrect : {params}", + "xpack.lens.indexPattern.formulaFieldNotFound": "{variablesLength, plural, one {Champ} other {Champs}} {variablesList} introuvable(s)", + "xpack.lens.indexPattern.formulaFieldNotRequired": "L'opération {operation} n'accepte aucun champ comme argument", + "xpack.lens.indexPattern.formulaFieldValue": "champ", + "xpack.lens.indexPattern.formulaLabel": "Formule", + "xpack.lens.indexPattern.formulaMathMissingArgument": "L'opération {operation} dans la formule ne comprend pas les arguments {count} : {params}", + "xpack.lens.indexPattern.formulaMetricValue": "indicateur", + "xpack.lens.indexPattern.formulaNoFieldForOperation": "aucun champ", + "xpack.lens.indexPattern.formulaNoOperation": "aucune opération", + "xpack.lens.indexPattern.formulaOperationDoubleQueryError": "Utilisez uniquement kql= ou lucene=, mais pas les deux", + "xpack.lens.indexPattern.formulaOperationDuplicateParams": "Les paramètres de l'opération {operation} ont été déclarés plusieurs fois : {params}", + "xpack.lens.indexPattern.formulaOperationQueryError": "Des guillemets simples sont requis pour {language}='' à {rawQuery}", + "xpack.lens.indexPattern.formulaOperationTooManyFirstArguments": "L'opération {operation} dans la formule requiert un {type} {supported, plural, one {unique} other {pris en charge}}, trouvé : {text}", + "xpack.lens.indexPattern.formulaOperationValue": "opération", + "xpack.lens.indexPattern.formulaOperationwrongArgument": "L'opération {operation} dans la formule ne prend pas en charge les paramètres {type}, trouvé : {text}", + "xpack.lens.indexPattern.formulaOperationWrongFirstArgument": "Le premier argument pour {operation} doit être un nom {type}. Trouvé {argument}", + "xpack.lens.indexPattern.formulaParameterNotRequired": "L'opération {operation} n'accepte aucun paramètre", + "xpack.lens.indexPattern.formulaPartLabel": "Partie de {label}", + "xpack.lens.indexPattern.formulaWarning": "Formule actuellement appliquée", + "xpack.lens.indexPattern.formulaWarningText": "Pour écraser votre formule, sélectionnez une fonction rapide", + "xpack.lens.indexPattern.formulaWithTooManyArguments": "L'opération {operation} a trop d'arguments", + "xpack.lens.indexPattern.functionsLabel": "Sélectionner une fonction", + "xpack.lens.indexPattern.groupByDropdown": "Regrouper par", + "xpack.lens.indexPattern.incompleteOperation": "(incomplet)", + "xpack.lens.indexPattern.intervals": "Intervalles", + "xpack.lens.indexPattern.invalidFieldLabel": "Champ non valide. Vérifiez votre modèle d'indexation ou choisissez un autre champ.", + "xpack.lens.indexPattern.invalidInterval": "Valeur d'intervalle non valide", + "xpack.lens.indexPattern.invalidOperationLabel": "Ce champ ne fonctionne pas avec la fonction sélectionnée.", + "xpack.lens.indexPattern.invalidReferenceConfiguration": "La dimension \"{dimensionLabel}\" n'est pas configurée correctement", + "xpack.lens.indexPattern.invalidTimeShift": "Décalage non valide. Entrez un entier positif suivi par l'une des unités suivantes : s, m, h, d, w, M, y. Par exemple, 3h pour 3 heures", + "xpack.lens.indexPattern.lastValue": "Dernière valeur", + "xpack.lens.indexPattern.lastValue.disabled": "Cette fonction requiert la présence d'un champ de date dans votre index", + "xpack.lens.indexPattern.lastValue.invalidTypeSortField": "Le champ {invalidField} n'est pas un champ de date et ne peut pas être utilisé pour le tri", + "xpack.lens.indexPattern.lastValue.signature": "champ : chaîne", + "xpack.lens.indexPattern.lastValue.sortField": "Trier par le champ de date", + "xpack.lens.indexPattern.lastValue.sortFieldNotFound": "Champ {invalidField} introuvable", + "xpack.lens.indexPattern.lastValue.sortFieldPlaceholder": "Champ de tri", + "xpack.lens.indexPattern.lastValueOf": "Dernière valeur de {name}", + "xpack.lens.indexPattern.layerErrorWrapper": "Erreur de {position} pour le calque : {wrappedMessage}", + "xpack.lens.indexPattern.max": "Maximum", + "xpack.lens.indexPattern.max.description": "Agrégation d'indicateurs à valeur unique qui renvoie la valeur maximale des valeurs numériques extraites des documents agrégés.", + "xpack.lens.indexPattern.maxOf": "Maximum de {name}", + "xpack.lens.indexPattern.median": "Médiane", + "xpack.lens.indexPattern.median.description": "Agrégation d'indicateurs à valeur unique qui calcule la valeur médiane des valeurs numériques extraites des documents agrégés.", + "xpack.lens.indexPattern.medianOf": "Médiane de {name}", + "xpack.lens.indexPattern.metaFieldsLabel": "Champs méta", + "xpack.lens.indexPattern.metric.signature": "champ : chaîne", + "xpack.lens.indexPattern.min": "Minimum", + "xpack.lens.indexPattern.min.description": "Agrégation d'indicateurs à valeur unique qui renvoie la valeur minimale des valeurs numériques extraites des documents agrégés.", + "xpack.lens.indexPattern.minOf": "Minimum de {name}", + "xpack.lens.indexPattern.missingFieldLabel": "Champ manquant", + "xpack.lens.indexPattern.missingReferenceError": "\"{dimensionLabel}\" n'est pas entièrement configuré", + "xpack.lens.indexPattern.moveToWorkspace": "Ajouter {field} à l'espace de travail", + "xpack.lens.indexPattern.moveToWorkspaceDisabled": "Ce champ ne peut pas être ajouté automatiquement à l'espace de travail. Vous pouvez toujours l'utiliser directement dans le panneau de configuration.", + "xpack.lens.indexPattern.moving_average.signature": "indicateur : nombre, [window] : nombre", + "xpack.lens.indexPattern.movingAverage": "Moyenne mobile", + "xpack.lens.indexPattern.movingAverage.basicExplanation": "La moyenne mobile fait glisser une fenêtre sur les données et affiche la valeur moyenne. La moyenne mobile est prise en charge uniquement par les histogrammes des dates.", + "xpack.lens.indexPattern.movingAverage.helpText": "Fonctionnement", + "xpack.lens.indexPattern.movingAverage.limitations": "La première valeur de moyenne mobile commence au deuxième élément.", + "xpack.lens.indexPattern.movingAverage.longerExplanation": "Pour calculer la moyenne mobile, Lens utilise la moyenne de la fenêtre et applique une politique d'omission pour les blancs. Pour les valeurs manquantes, le groupe est ignoré, et le calcul est effectué sur la valeur suivante.", + "xpack.lens.indexPattern.movingAverage.tableExplanation": "Par exemple, avec les données [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], vous pouvez calculer une moyenne mobile simple avec une taille de fenêtre de 5 :", + "xpack.lens.indexPattern.movingAverage.titleHelp": "Fonctionnement de la moyenne mobile", + "xpack.lens.indexPattern.movingAverage.window": "Taille de fenêtre", + "xpack.lens.indexPattern.movingAverage.windowInitialPartial": "La fenêtre est partielle jusqu'à ce qu'elle atteigne le nombre demandé d'éléments. Par exemple, avec une taille de fenêtre de 5 :", + "xpack.lens.indexPattern.movingAverage.windowLimitations": "La fenêtre n'inclut pas la valeur actuelle.", + "xpack.lens.indexPattern.movingAverageOf": "Moyenne mobile de {name}", + "xpack.lens.indexPattern.multipleDateHistogramsError": "\"{dimensionLabel}\" n'est pas le seul histogramme des dates. Lorsque vous utilisez des décalages, veillez à n'utiliser qu'un seul histogramme des dates.", + "xpack.lens.indexPattern.numberFormatLabel": "Numéro", + "xpack.lens.indexPattern.ofDocumentsLabel": "documents", + "xpack.lens.indexPattern.operationsNotFound": "{operationLength, plural, one {Opération} other {Opérations}} {operationsList} non trouvée(s)", + "xpack.lens.indexPattern.otherDocsLabel": "Autre", + "xpack.lens.indexPattern.overall_metric": "indicateur : nombre", + "xpack.lens.indexPattern.overallAverageOf": "Moyenne générale de {name}", + "xpack.lens.indexPattern.overallMax": "Max général", + "xpack.lens.indexPattern.overallMaxOf": "Max général de {name}", + "xpack.lens.indexPattern.overallMin": "Min général", + "xpack.lens.indexPattern.overallMinOf": "Min général de {name}", + "xpack.lens.indexPattern.overallSum": "Somme générale", + "xpack.lens.indexPattern.overallSumOf": "Somme générale de {name}", + "xpack.lens.indexPattern.percentageOfLabel": "{percentage} % de", + "xpack.lens.indexPattern.percentFormatLabel": "Pour cent", + "xpack.lens.indexPattern.percentile": "Centile", + "xpack.lens.indexPattern.percentile.errorMessage": "Le centile doit être un entier compris entre 1 et 99", + "xpack.lens.indexPattern.percentile.percentileValue": "Centile", + "xpack.lens.indexPattern.percentile.signature": "champ : chaîne, [percentile] : nombre", + "xpack.lens.indexPattern.percentileOf": "{percentile, selectordinal, one {#er} two {#e} few {#e} other {#e}} centile de {name}", + "xpack.lens.indexPattern.pinnedTopValuesLabel": "Filtres de {field}", + "xpack.lens.indexPattern.quickFunctionsLabel": "Fonctions rapides", + "xpack.lens.indexPattern.range.isInvalid": "Cette plage n'est pas valide", + "xpack.lens.indexPattern.ranges.addRange": "Ajouter une plage", + "xpack.lens.indexPattern.ranges.customIntervalsToggle": "Créer des plages personnalisées", + "xpack.lens.indexPattern.ranges.customRangeLabelPlaceholder": "Étiquette personnalisée", + "xpack.lens.indexPattern.ranges.customRanges": "Plages", + "xpack.lens.indexPattern.ranges.customRangesRemoval": "Retirer les plages personnalisées", + "xpack.lens.indexPattern.ranges.decreaseButtonLabel": "Diminuer la granularité", + "xpack.lens.indexPattern.ranges.deleteRange": "Supprimer la plage", + "xpack.lens.indexPattern.ranges.granularity": "Granularité des intervalles", + "xpack.lens.indexPattern.ranges.granularityHelpText": "Fonctionnement", + "xpack.lens.indexPattern.ranges.granularityPopoverAdvancedExplanation": "Les intervalles sont incrémentés par 10, 5 ou 2. Par exemple, un intervalle peut être 100 ou 0,2 .", + "xpack.lens.indexPattern.ranges.granularityPopoverBasicExplanation": "La granularité des intervalles divise le champ en intervalles régulièrement espacés sur la base des valeurs minimales et maximales du champ.", + "xpack.lens.indexPattern.ranges.granularityPopoverExplanation": "La taille de l'intervalle est une valeur de \"gentillesse\". Lorsque la granularité du curseur change, l'intervalle reste le même lorsque l'intervalle de \"gentillesse\" est le même. La granularité minimale est 1, et la valeur maximale est {setting}. Pour modifier la granularité maximale, accédez aux Paramètres avancés.", + "xpack.lens.indexPattern.ranges.granularityPopoverTitle": "Fonctionnement de la granularité des intervalles", + "xpack.lens.indexPattern.ranges.increaseButtonLabel": "Augmenter la granularité", + "xpack.lens.indexPattern.ranges.lessThanOrEqualAppend": "≤", + "xpack.lens.indexPattern.ranges.lessThanOrEqualTooltip": "Inférieur ou égal à", + "xpack.lens.indexPattern.ranges.lessThanPrepend": "<", + "xpack.lens.indexPattern.ranges.lessThanTooltip": "Inférieur à", + "xpack.lens.indexPattern.records": "Enregistrements", + "xpack.lens.indexPattern.referenceFunctionPlaceholder": "Sous-fonction", + "xpack.lens.indexPattern.removeColumnAriaLabel": "Ajouter ou glisser-déposer un champ dans {groupLabel}", + "xpack.lens.indexPattern.removeColumnLabel": "Retirer la configuration de \"{groupLabel}\"", + "xpack.lens.indexPattern.removeFieldLabel": "Retirer le champ du modèle d'indexation", + "xpack.lens.indexPattern.sortField.invalid": "Champ non valide. Vérifiez votre modèle d'indexation ou choisissez un autre champ.", + "xpack.lens.indexpattern.suggestions.nestingChangeLabel": "{innerOperation} pour chaque {outerOperation}", + "xpack.lens.indexpattern.suggestions.overallLabel": "{operation} générale", + "xpack.lens.indexpattern.suggestions.overTimeLabel": "Sur la durée", + "xpack.lens.indexPattern.sum": "Somme", + "xpack.lens.indexPattern.sum.description": "Agrégation d'indicateurs à valeur unique qui récapitule les valeurs numériques extraites des documents agrégés.", + "xpack.lens.indexPattern.sumOf": "Somme de {name}", + "xpack.lens.indexPattern.terms": "Valeurs les plus élevées", + "xpack.lens.indexPattern.terms.advancedSettings": "Avancé", + "xpack.lens.indexPattern.terms.missingBucketDescription": "Inclure les documents sans ce champ", + "xpack.lens.indexPattern.terms.missingLabel": "(valeur manquante)", + "xpack.lens.indexPattern.terms.orderAlphabetical": "Alphabétique", + "xpack.lens.indexPattern.terms.orderAscending": "Croissant", + "xpack.lens.indexPattern.terms.orderBy": "Classer par", + "xpack.lens.indexPattern.terms.orderByHelp": "Spécifie la dimension selon laquelle les valeurs les plus élevées sont classées.", + "xpack.lens.indexPattern.terms.orderDescending": "Décroissant", + "xpack.lens.indexPattern.terms.orderDirection": "Sens de classement", + "xpack.lens.indexPattern.terms.otherBucketDescription": "Regrouper les autres valeurs sous \"Autre\"", + "xpack.lens.indexPattern.terms.otherLabel": "Autre", + "xpack.lens.indexPattern.terms.size": "Nombre de valeurs", + "xpack.lens.indexPattern.termsOf": "Valeurs les plus élevées de {name}", + "xpack.lens.indexPattern.termsWithMultipleShifts": "Dans un seul calque, il est impossible de combiner des indicateurs avec des décalages temporels différents et des valeurs dynamiques les plus élevées. Utilisez la même valeur de décalage pour tous les indicateurs, ou utilisez des filtres à la place des valeurs les plus élevées.", + "xpack.lens.indexPattern.termsWithMultipleShiftsFixActionLabel": "Utiliser des filtres", + "xpack.lens.indexPattern.timeScale.enableTimeScale": "Normaliser par unité", + "xpack.lens.indexPattern.timeScale.label": "Normaliser par unité", + "xpack.lens.indexPattern.timeScale.tooltip": "Normalisez les valeurs pour qu'elles soient toujours affichées en tant que taux par unité de temps spécifiée, indépendamment de l'intervalle de dates sous-jacent.", + "xpack.lens.indexPattern.timeShift.12hours": "Il y a 12 heures (12h)", + "xpack.lens.indexPattern.timeShift.3hours": "Il y a 3 heures (3h)", + "xpack.lens.indexPattern.timeShift.3months": "Il y a 3 mois (3M)", + "xpack.lens.indexPattern.timeShift.6hours": "Il y a 6 heures (6h)", + "xpack.lens.indexPattern.timeShift.6months": "Il y a 6 mois (6M)", + "xpack.lens.indexPattern.timeShift.day": "Il y a 1 jour (1d)", + "xpack.lens.indexPattern.timeShift.help": "Entrer le nombre et l'unité du décalage temporel", + "xpack.lens.indexPattern.timeShift.hour": "Il y a 1 heure (1h)", + "xpack.lens.indexPattern.timeShift.label": "Décalage temporel", + "xpack.lens.indexPattern.timeShift.month": "Il y a 1 mois (1M)", + "xpack.lens.indexPattern.timeShift.noMultipleHelp": "Le décalage temporel doit être un multiple de l'intervalle de l'histogramme des dates. Ajustez le décalage ou l'intervalle de l'histogramme des dates", + "xpack.lens.indexPattern.timeShift.tooSmallHelp": "Le décalage temporel doit être supérieur à l'intervalle de l'histogramme des dates. Augmentez le décalage ou spécifiez un intervalle plus petit dans l'histogramme des dates", + "xpack.lens.indexPattern.timeShift.week": "Il y a 1 semaine (1w)", + "xpack.lens.indexPattern.timeShift.year": "Il y a 1 an (1y)", + "xpack.lens.indexPattern.timeShiftMultipleWarning": "{label} utilise un décalage temporel de {columnTimeShift} qui n'est pas un multiple de l'intervalle de l'histogramme des dates de {interval}. Pour éviter une non-correspondance des données, utilisez un multiple de {interval} comme décalage.", + "xpack.lens.indexPattern.timeShiftPlaceholder": "Saisissez des valeurs personnalisées (par ex. 8w)", + "xpack.lens.indexPattern.timeShiftSmallWarning": "{label} utilise un décalage temporel de {columnTimeShift} qui est inférieur à l'intervalle de l'histogramme des dates de {interval}. Pour éviter une non-correspondance des données, utilisez un multiple de {interval} comme décalage.", + "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]", + "xpack.lens.indexPattern.useAsTopLevelAgg": "Regrouper d'abord en fonction de ce champ", + "xpack.lens.indexPatterns.actionsPopoverLabel": "Paramètres du modèle d'indexation", + "xpack.lens.indexPatterns.addFieldButton": "Ajouter un champ au modèle d'indexation", + "xpack.lens.indexPatterns.clearFiltersLabel": "Effacer le nom et saisissez les filtres", + "xpack.lens.indexPatterns.fieldFiltersLabel": "Filtrer par type", + "xpack.lens.indexPatterns.fieldSearchLiveRegion": "{availableFields} {availableFields, plural, one {champ} other {champs}} disponible(s). {emptyFields} {emptyFields, plural, one {champ} other {champs}} vide(s). {metaFields} {metaFields, plural, one {champ} other {champs}} méta.", + "xpack.lens.indexPatterns.filterByNameLabel": "Rechercher les noms des champs", + "xpack.lens.indexPatterns.manageFieldButton": "Gérer les champs du modèle d'indexation", + "xpack.lens.indexPatterns.noAvailableDataLabel": "Aucun champ disponible ne contient de données.", + "xpack.lens.indexPatterns.noDataLabel": "Aucun champ.", + "xpack.lens.indexPatterns.noEmptyDataLabel": "Aucun champ vide.", + "xpack.lens.indexPatterns.noFields.extendTimeBullet": "Extension de la plage temporelle", + "xpack.lens.indexPatterns.noFields.fieldTypeFilterBullet": "Utilisation de différents filtres de champ", + "xpack.lens.indexPatterns.noFields.globalFiltersBullet": "Modification des filtres globaux", + "xpack.lens.indexPatterns.noFields.tryText": "Essayer :", + "xpack.lens.indexPatterns.noFieldsLabel": "Aucun champ n'existe dans ce modèle d'indexation.", + "xpack.lens.indexPatterns.noFilteredFieldsLabel": "Aucun champ ne correspond aux filtres sélectionnés.", + "xpack.lens.indexPatterns.noMetaDataLabel": "Aucun champ méta.", + "xpack.lens.indexPatternSuggestion.removeLayerLabel": "Afficher uniquement {indexPatternTitle}", + "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "Afficher uniquement le calque {layerNumber}", + "xpack.lens.labelInput.label": "Étiquette", + "xpack.lens.layerPanel.layerVisualizationType": "Type de visualisation du calque", + "xpack.lens.lensSavedObjectLabel": "Visualisation Lens", + "xpack.lens.metric.addLayer": "Ajouter un calque de visualisation", + "xpack.lens.metric.groupLabel": "Valeur tabulaire et unique", + "xpack.lens.metric.label": "Indicateur", + "xpack.lens.pageTitle": "Lens", + "xpack.lens.paletteHeatmapGradient.customize": "Modifier", + "xpack.lens.paletteHeatmapGradient.customizeLong": "Modifier la palette", + "xpack.lens.paletteHeatmapGradient.label": "Couleur", + "xpack.lens.palettePicker.label": "Palette de couleurs", + "xpack.lens.paletteTableGradient.customize": "Modifier", + "xpack.lens.paletteTableGradient.label": "Couleur", + "xpack.lens.pie.addLayer": "Ajouter un calque de visualisation", + "xpack.lens.pie.arrayValues": "{label} contient des valeurs de tableau. Le rendu de votre visualisation peut ne pas se présenter comme attendu.", + "xpack.lens.pie.donutLabel": "Graphique en anneau", + "xpack.lens.pie.groupLabel": "Proportion", + "xpack.lens.pie.groupsizeLabel": "Taille par", + "xpack.lens.pie.pielabel": "Camembert", + "xpack.lens.pie.sliceGroupLabel": "Section par", + "xpack.lens.pie.suggestionLabel": "Comme {chartName}", + "xpack.lens.pie.treemapGroupLabel": "Regrouper par", + "xpack.lens.pie.treemaplabel": "Compartimentage", + "xpack.lens.pie.treemapSuggestionLabel": "Comme compartimentage", + "xpack.lens.pieChart.categoriesInLegendLabel": "Masquer les étiquettes", + "xpack.lens.pieChart.fitInsideOnlyLabel": "À l'intérieur uniquement", + "xpack.lens.pieChart.hiddenNumbersLabel": "Masquer dans le graphique", + "xpack.lens.pieChart.labelPositionLabel": "Position", + "xpack.lens.pieChart.legendVisibility.auto": "Auto", + "xpack.lens.pieChart.legendVisibility.hide": "Masquer", + "xpack.lens.pieChart.legendVisibility.show": "Afficher", + "xpack.lens.pieChart.nestedLegendLabel": "Imbriqué", + "xpack.lens.pieChart.numberLabels": "Valeurs", + "xpack.lens.pieChart.percentDecimalsLabel": "Nombre maximal de décimales pour les pourcentages", + "xpack.lens.pieChart.showCategoriesLabel": "Intérieur ou extérieur", + "xpack.lens.pieChart.showFormatterValuesLabel": "Afficher la valeur", + "xpack.lens.pieChart.showPercentValuesLabel": "Afficher le pourcentage", + "xpack.lens.pieChart.showTreemapCategoriesLabel": "Afficher les étiquettes", + "xpack.lens.pieChart.valuesLabel": "Étiquettes", + "xpack.lens.resetLayerAriaLabel": "Réinitialiser le calque {index}", + "xpack.lens.resetVisualizationAriaLabel": "Réinitialiser la visualisation", + "xpack.lens.searchTitle": "Lens : créer des visualisations", + "xpack.lens.section.configPanelLabel": "Panneau de configuration", + "xpack.lens.section.dataPanelLabel": "Panneau de données", + "xpack.lens.section.workspaceLabel": "Espace de travail de visualisation", + "xpack.lens.shared.chartValueLabelVisibilityLabel": "Étiquettes", + "xpack.lens.shared.curveLabel": "Options visuelles", + "xpack.lens.shared.legend.filterForValueButtonAriaLabel": "Filtre pour la valeur", + "xpack.lens.shared.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", + "xpack.lens.shared.legend.filterOutValueButtonAriaLabel": "Filtrer la valeur", + "xpack.lens.shared.legendAlignmentLabel": "Alignement", + "xpack.lens.shared.legendInsideAlignmentLabel": "Alignement", + "xpack.lens.shared.legendInsideColumnsLabel": "Nombre de colonnes", + "xpack.lens.shared.legendInsideLocationAlignmentLabel": "Alignement", + "xpack.lens.shared.legendInsideTooltip": "Requiert que la légende soit placée dans la visualisation", + "xpack.lens.shared.legendIsTruncated": "Requiert que le texte soit tronqué", + "xpack.lens.shared.legendLabel": "Légende", + "xpack.lens.shared.legendLocationBottomLeft": "En bas à gauche", + "xpack.lens.shared.legendLocationBottomRight": "En bas à droite", + "xpack.lens.shared.legendLocationLabel": "Emplacement", + "xpack.lens.shared.legendLocationTopLeft": "En haut à gauche", + "xpack.lens.shared.legendLocationTopRight": "En haut à droite", + "xpack.lens.shared.legendPositionBottom": "Bas", + "xpack.lens.shared.legendPositionLeft": "Gauche", + "xpack.lens.shared.legendPositionRight": "Droite", + "xpack.lens.shared.legendPositionTop": "Haut", + "xpack.lens.shared.legendVisibilityLabel": "Affichage", + "xpack.lens.shared.legendVisibleTooltip": "Requiert que la légende soit affichée", + "xpack.lens.shared.maxLinesLabel": "Nombre maximal de lignes", + "xpack.lens.shared.nestedLegendLabel": "Imbriqué", + "xpack.lens.shared.truncateLegend": "Tronquer le texte", + "xpack.lens.shared.valueInLegendLabel": "Afficher la valeur", + "xpack.lens.sugegstion.refreshSuggestionLabel": "Actualiser", + "xpack.lens.suggestion.refreshSuggestionTooltip": "Actualisez les suggestions en fonction de la visualisation sélectionnée.", + "xpack.lens.suggestions.currentVisLabel": "Visualisation en cours", + "xpack.lens.table.actionsLabel": "Afficher les actions", + "xpack.lens.table.alignment.center": "Centre", + "xpack.lens.table.alignment.label": "Alignement du texte", + "xpack.lens.table.alignment.left": "Gauche", + "xpack.lens.table.alignment.right": "Droite", + "xpack.lens.table.columnFilter.filterForValueText": "Filtre pour la colonne", + "xpack.lens.table.columnFilter.filterOutValueText": "Filtrer la colonne", + "xpack.lens.table.columnVisibilityLabel": "Masquer la colonne", + "xpack.lens.table.defaultAriaLabel": "Visualisation du tableau de données", + "xpack.lens.table.dynamicColoring.cell": "Cellule", + "xpack.lens.table.dynamicColoring.customPalette.colorStopsHelpPercentage": "Les types de valeurs en pourcentage sont relatifs à la plage complète des valeurs de données disponibles.", + "xpack.lens.table.dynamicColoring.label": "Couleur par valeur", + "xpack.lens.table.dynamicColoring.none": "Aucune", + "xpack.lens.table.dynamicColoring.rangeType.label": "Type de valeur", + "xpack.lens.table.dynamicColoring.rangeType.number": "Numéro", + "xpack.lens.table.dynamicColoring.rangeType.percent": "Pour cent", + "xpack.lens.table.dynamicColoring.text": "Texte", + "xpack.lens.table.hide.hideLabel": "Masquer", + "xpack.lens.table.palettePanelContainer.back": "Retour", + "xpack.lens.table.palettePanelTitle": "Modifier la couleur", + "xpack.lens.table.resize.reset": "Réinitialiser la largeur", + "xpack.lens.table.sort.ascLabel": "Trier dans l'ordre croissant", + "xpack.lens.table.sort.descLabel": "Trier dans l'ordre décroissant", + "xpack.lens.table.summaryRow.average": "Moyenne", + "xpack.lens.table.summaryRow.count": "Compte de valeurs", + "xpack.lens.table.summaryRow.customlabel": "Étiquette de résumé", + "xpack.lens.table.summaryRow.label": "Ligne de résumé", + "xpack.lens.table.summaryRow.maximum": "Maximum", + "xpack.lens.table.summaryRow.minimum": "Minimum", + "xpack.lens.table.summaryRow.none": "Aucune", + "xpack.lens.table.summaryRow.sum": "Somme", + "xpack.lens.table.tableCellFilter.filterForValueAriaLabel": "Filtre pour la valeur : {cellContent}", + "xpack.lens.table.tableCellFilter.filterForValueText": "Filtre pour la valeur", + "xpack.lens.table.tableCellFilter.filterOutValueAriaLabel": "Filtrer la valeur : {cellContent}", + "xpack.lens.table.tableCellFilter.filterOutValueText": "Filtrer la valeur", + "xpack.lens.timeScale.removeLabel": "Retirer la normalisation par unité de temps", + "xpack.lens.timeShift.removeLabel": "Retirer le décalage temporel", + "xpack.lens.visTypeAlias.description": "Créez des visualisations avec notre éditeur de glisser-déposer. Basculez entre les différents types de visualisation à tout moment.", + "xpack.lens.visTypeAlias.note": "Recommandé pour la plupart des utilisateurs.", + "xpack.lens.visTypeAlias.title": "Lens", + "xpack.lens.visTypeAlias.type": "Lens", + "xpack.lens.visualizeGeoFieldMessage": "Lens ne peut pas visualiser les champs {fieldType}", + "xpack.lens.xyChart.addDataLayerLabel": "Ajouter un calque de visualisation", + "xpack.lens.xyChart.addLayer": "Ajouter un calque", + "xpack.lens.xyChart.addLayerTooltip": "Utilisez plusieurs calques pour combiner les types de visualisation ou pour visualiser différents modèles d'indexation.", + "xpack.lens.xyChart.axisExtent.custom": "Personnalisé", + "xpack.lens.xyChart.axisExtent.dataBounds": "Limites de données", + "xpack.lens.xyChart.axisExtent.disabledDataBoundsMessage": "Seuls les graphiques linéaires peuvent être adaptés aux limites de données", + "xpack.lens.xyChart.axisExtent.full": "Plein", + "xpack.lens.xyChart.axisExtent.label": "Limites", + "xpack.lens.xyChart.axisOrientation.angled": "En angle", + "xpack.lens.xyChart.axisOrientation.horizontal": "Horizontal", + "xpack.lens.xyChart.axisOrientation.label": "Orientation", + "xpack.lens.xyChart.axisOrientation.vertical": "Vertical", + "xpack.lens.xyChart.axisSide.auto": "Auto", + "xpack.lens.xyChart.axisSide.bottom": "Bas", + "xpack.lens.xyChart.axisSide.label": "Côté de l'axe", + "xpack.lens.xyChart.axisSide.left": "Gauche", + "xpack.lens.xyChart.axisSide.right": "Droite", + "xpack.lens.xyChart.axisSide.top": "Haut", + "xpack.lens.xyChart.axisTitlesSettings.help": "Afficher les titres des axes X et Y", + "xpack.lens.xyChart.bottomAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe du bas est activé.", + "xpack.lens.xyChart.bottomAxisLabel": "Axe du bas", + "xpack.lens.xyChart.boundaryError": "La limite inférieure doit être plus grande que la limite supérieure", + "xpack.lens.xyChart.curveStyleLabel": "Courbes", + "xpack.lens.xyChart.curveType.help": "Définir de quelle façon le type de courbe est rendu pour un graphique linéaire", + "xpack.lens.xyChart.emptyXLabel": "(vide)", + "xpack.lens.xyChart.extentMode.help": "Mode d'extension", + "xpack.lens.xyChart.fillOpacity.help": "Définir l'opacité du remplissage du graphique en aires", + "xpack.lens.xyChart.fillOpacityLabel": "Opacité de remplissage", + "xpack.lens.xyChart.fittingFunction.help": "Définir le mode de traitement des valeurs manquantes", + "xpack.lens.xyChart.floatingColumns.help": "Spécifie le nombre de colonnes lorsque la légende est affichée à l'intérieur du graphique.", + "xpack.lens.xyChart.Gridlines": "Quadrillage", + "xpack.lens.xyChart.gridlinesSettings.help": "Afficher le quadrillage des axes X et Y", + "xpack.lens.xyChart.help": "Graphique X/Y", + "xpack.lens.xyChart.hideEndzones.help": "Masquer les marqueurs de zone de fin pour les données partielles", + "xpack.lens.xyChart.horizontalAlignment.help": "Spécifie l'alignement horizontal de la légende lorsqu'elle est affichée à l'intérieur du graphique.", + "xpack.lens.xyChart.horizontalAxisLabel": "Axe horizontal", + "xpack.lens.xyChart.inclusiveZero": "Les limites doivent inclure zéro.", + "xpack.lens.xyChart.isInside.help": "Spécifie si une légende se trouve à l'intérieur d'un graphique", + "xpack.lens.xyChart.isVisible.help": "Spécifie si la légende est visible ou non.", + "xpack.lens.xyChart.labelsOrientation.help": "Définit la rotation des étiquettes des axes", + "xpack.lens.xyChart.leftAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe de gauche est activé.", + "xpack.lens.xyChart.leftAxisLabel": "Axe de gauche", + "xpack.lens.xyChart.legend.help": "Configurez la légende du graphique.", + "xpack.lens.xyChart.legendLocation.inside": "Intérieur", + "xpack.lens.xyChart.legendLocation.outside": "Extérieur", + "xpack.lens.xyChart.legendVisibility.auto": "Auto", + "xpack.lens.xyChart.legendVisibility.hide": "Masquer", + "xpack.lens.xyChart.legendVisibility.show": "Afficher", + "xpack.lens.xyChart.lowerBoundLabel": "Limite inférieure", + "xpack.lens.xyChart.maxLines.help": "Spécifie le nombre de lignes par élément de légende.", + "xpack.lens.xyChart.missingValuesLabel": "Valeurs manquantes", + "xpack.lens.xyChart.missingValuesLabelHelpText": "Par défaut, Lens masque les blancs dans les données. Pour remplir le blanc, effectuez une sélection.", + "xpack.lens.xyChart.nestUnderRoot": "Ensemble de données entier", + "xpack.lens.xyChart.position.help": "Spécifie la position de la légende.", + "xpack.lens.xyChart.renderer.help": "Outil de rendu de graphique X/Y", + "xpack.lens.xyChart.rightAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe de droite est activé.", + "xpack.lens.xyChart.rightAxisLabel": "Axe de droite", + "xpack.lens.xyChart.seriesColor.auto": "Auto", + "xpack.lens.xyChart.seriesColor.label": "Couleur de la série", + "xpack.lens.xyChart.shouldTruncate.help": "Spécifie si les éléments de légende seront tronqués ou non", + "xpack.lens.xyChart.showEnzones": "Afficher les marqueurs de données partielles", + "xpack.lens.xyChart.showSingleSeries.help": "Spécifie si une légende comportant une seule entrée doit être affichée", + "xpack.lens.xyChart.splitSeries": "Répartir par", + "xpack.lens.xyChart.tickLabels": "Étiquettes de graduation", + "xpack.lens.xyChart.tickLabelsSettings.help": "Afficher les étiquettes de graduation des axes X et Y", + "xpack.lens.xyChart.title.help": "Titre de l'axe", + "xpack.lens.xyChart.topAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe du haut est activé.", + "xpack.lens.xyChart.topAxisLabel": "Axe du haut", + "xpack.lens.xyChart.upperBoundLabel": "Limite supérieure", + "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "Ce paramètre ne peut pas être modifié dans les histogrammes.", + "xpack.lens.xyChart.valuesInLegend.help": "Afficher les valeurs dans la légende", + "xpack.lens.xyChart.valuesPercentageDisabledHelpText": "Ce paramètre ne peut pas être modifié dans les graphiques en aires à pourcentages.", + "xpack.lens.xyChart.valuesStackedDisabledHelpText": "Ce paramètre ne peut pas être modifié dans les graphiques empilés ou les graphiques à barres à pourcentages", + "xpack.lens.xyChart.verticalAlignment.help": "Spécifie l'alignement vertical de la légende lorsqu'elle est affichée à l'intérieur du graphique.", + "xpack.lens.xyChart.verticalAxisLabel": "Axe vertical", + "xpack.lens.xyChart.xAxisGridlines.help": "Spécifie si le quadrillage de l'axe X est visible ou non.", + "xpack.lens.xyChart.xAxisLabelsOrientation.help": "Spécifie l'orientation des étiquettes de l'axe X.", + "xpack.lens.xyChart.xAxisTickLabels.help": "Spécifie si les étiquettes de graduation de l'axe X sont visibles ou non.", + "xpack.lens.xyChart.xAxisTitle.help": "Spécifie si le titre de l'axe X est visible ou non.", + "xpack.lens.xyChart.xTitle.help": "Titre de l'axe X", + "xpack.lens.xyChart.yLeftAxisgridlines.help": "Spécifie si le quadrillage de l'axe Y de gauche est visible ou non.", + "xpack.lens.xyChart.yLeftAxisLabelsOrientation.help": "Spécifie l'orientation des étiquettes de l'axe Y de gauche.", + "xpack.lens.xyChart.yLeftAxisTickLabels.help": "Spécifie si les étiquettes de graduation de l'axe Y de gauche sont visibles ou non.", + "xpack.lens.xyChart.yLeftAxisTitle.help": "Spécifie si le titre de l'axe Y de gauche est visible ou non.", + "xpack.lens.xyChart.yLeftExtent.help": "Portée de l'axe Y de gauche", + "xpack.lens.xyChart.yLeftTitle.help": "Titre de l'axe Y de gauche", + "xpack.lens.xyChart.yRightAxisgridlines.help": "Spécifie si le quadrillage de l'axe Y de droite est visible ou non.", + "xpack.lens.xyChart.yRightAxisLabelsOrientation.help": "Spécifie l'orientation des étiquettes de l'axe Y de droite.", + "xpack.lens.xyChart.yRightAxisTickLabels.help": "Spécifie si les étiquettes de graduation de l'axe Y de droite sont visibles ou non.", + "xpack.lens.xyChart.yRightAxisTitle.help": "Spécifie si le titre de l'axe Y de droite est visible ou non.", + "xpack.lens.xyChart.yRightExtent.help": "Portée de l'axe Y de droite", + "xpack.lens.xyChart.yRightTitle.help": "Titre de l'axe Y de droite", + "xpack.lens.xySuggestions.asPercentageTitle": "Pourcentage", + "xpack.lens.xySuggestions.barChartTitle": "Graphique à barres", + "xpack.lens.xySuggestions.dateSuggestion": "{yTitle} sur {xTitle}", + "xpack.lens.xySuggestions.emptyAxisTitle": "(vide)", + "xpack.lens.xySuggestions.flipTitle": "Retourner", + "xpack.lens.xySuggestions.lineChartTitle": "Graphique linéaire", + "xpack.lens.xySuggestions.nonDateSuggestion": "{yTitle} de {xTitle}", + "xpack.lens.xySuggestions.stackedChartTitle": "Empilé", + "xpack.lens.xySuggestions.unstackedChartTitle": "Non empilé", + "xpack.lens.xySuggestions.yAxixConjunctionSign": " & ", + "xpack.lens.xyVisualization.areaLabel": "Zone", + "xpack.lens.xyVisualization.arrayValues": "{label} contient des valeurs de tableau. Le rendu de votre visualisation peut ne pas se présenter comme attendu.", + "xpack.lens.xyVisualization.barGroupLabel": "Barre", + "xpack.lens.xyVisualization.barHorizontalFullLabel": "Horizontal à barres", + "xpack.lens.xyVisualization.barHorizontalLabel": "H. Barres", + "xpack.lens.xyVisualization.barLabel": "Vertical à barres", + "xpack.lens.xyVisualization.dataFailureSplitLong": "{layers, plural, one {Le calque} other {Les calques}} {layersList} {layers, plural, one {requiert} other {requièrent}} un champ pour {axis}.", + "xpack.lens.xyVisualization.dataFailureSplitShort": "{axis} manquant.", + "xpack.lens.xyVisualization.dataFailureYLong": "{layers, plural, one {Le calque} other {Les calques}} {layersList} {layers, plural, one {requiert} other {requièrent}} un champ pour {axis}.", + "xpack.lens.xyVisualization.dataFailureYShort": "{axis} manquant.", + "xpack.lens.xyVisualization.dataTypeFailureXLong": "Non-correspondance des types de données pour {axis}. Impossible de mélanger les types d'intervalle date et nombre.", + "xpack.lens.xyVisualization.dataTypeFailureXOrdinalLong": "Non-correspondance de type de données pour {axis}, utilisez une autre fonction.", + "xpack.lens.xyVisualization.dataTypeFailureXShort": "Type de données incorrect pour {axis}.", + "xpack.lens.xyVisualization.dataTypeFailureYLong": "La dimension {label} fournie pour {axis} possède un type de données incorrect. Nombre attendu mais possède {dataType}", + "xpack.lens.xyVisualization.dataTypeFailureYShort": "Type de données incorrect pour {axis}.", + "xpack.lens.xyVisualization.lineGroupLabel": "Linéaire et en aires", + "xpack.lens.xyVisualization.lineLabel": "Ligne", + "xpack.lens.xyVisualization.mixedBarHorizontalLabel": "Horizontal à barres mixte", + "xpack.lens.xyVisualization.mixedLabel": "XY mixte", + "xpack.lens.xyVisualization.stackedAreaLabel": "En aires empilées", + "xpack.lens.xyVisualization.stackedBarHorizontalFullLabel": "Horizontal à barres empilées", + "xpack.lens.xyVisualization.stackedBarHorizontalLabel": "H. À barres empilées", + "xpack.lens.xyVisualization.stackedBarLabel": "Vertical à barres empilées", + "xpack.lens.xyVisualization.stackedPercentageAreaLabel": "En aires à pourcentages", + "xpack.lens.xyVisualization.stackedPercentageBarHorizontalFullLabel": "Horizontal à barres à pourcentages", + "xpack.lens.xyVisualization.stackedPercentageBarHorizontalLabel": "H. À barres à pourcentages", + "xpack.lens.xyVisualization.stackedPercentageBarLabel": "Vertical à barres à pourcentages", + "xpack.lens.xyVisualization.xyLabel": "XY", + "advancedSettings.advancedSettingsLabel": "Paramètres avancés", + "advancedSettings.badge.readOnly.text": "Lecture seule", + "advancedSettings.badge.readOnly.tooltip": "Impossible d’enregistrer les paramètres avancés", + "advancedSettings.callOutCautionDescription": "Soyez prudent, ces paramètres sont destinés aux utilisateurs très avancés uniquement. Toute modification est susceptible d’entraîner des dommages importants à Kibana. Certains de ces paramètres peuvent être non documentés, non pris en charge ou expérimentaux. Lorsqu’un champ dispose d’une valeur par défaut, le laisser vide entraîne l’application de cette valeur par défaut, ce qui peut ne pas être acceptable compte tenu d’autres directives de configuration. Toute suppression d'un paramètre personnalisé de la configuration de Kibana est définitive.", + "advancedSettings.callOutCautionTitle": "Attention : toute action est susceptible de provoquer des dommages.", + "advancedSettings.categoryNames.dashboardLabel": "Tableau de bord", + "advancedSettings.categoryNames.discoverLabel": "Discover", + "advancedSettings.categoryNames.generalLabel": "Général", + "advancedSettings.categoryNames.machineLearningLabel": "Machine Learning", + "advancedSettings.categoryNames.notificationsLabel": "Notifications", + "advancedSettings.categoryNames.observabilityLabel": "Observabilité", + "advancedSettings.categoryNames.reportingLabel": "Reporting", + "advancedSettings.categoryNames.searchLabel": "Recherche", + "advancedSettings.categoryNames.securitySolutionLabel": "Solution de sécurité", + "advancedSettings.categoryNames.timelionLabel": "Timelion", + "advancedSettings.categoryNames.visualizationsLabel": "Visualisations", + "advancedSettings.categorySearchLabel": "Catégorie", + "advancedSettings.featureCatalogueTitle": "Personnalisez votre expérience Kibana : modifiez le format de date, activez le mode sombre, et bien plus encore.", + "advancedSettings.field.changeImageLinkAriaLabel": "Modifier {ariaName}", + "advancedSettings.field.changeImageLinkText": "Modifier l'image", + "advancedSettings.field.codeEditorSyntaxErrorMessage": "Syntaxe JSON non valide", + "advancedSettings.field.customSettingAriaLabel": "Paramètre personnalisé", + "advancedSettings.field.customSettingTooltip": "Paramètre personnalisé", + "advancedSettings.field.defaultValueText": "Valeur par défaut : {value}", + "advancedSettings.field.defaultValueTypeJsonText": "Valeur par défaut : {value}", + "advancedSettings.field.deprecationClickAreaLabel": "Cliquez ici pour afficher la documentation de déclassement pour {settingName}.", + "advancedSettings.field.helpText": "Ce paramètre est défini par le serveur Kibana et ne peut pas être modifié.", + "advancedSettings.field.imageChangeErrorMessage": "Impossible d’enregistrer l'image", + "advancedSettings.field.invalidIconLabel": "Non valide", + "advancedSettings.field.offLabel": "Off", + "advancedSettings.field.onLabel": "On", + "advancedSettings.field.resetToDefaultLinkAriaLabel": "Réinitialiser {ariaName} à la valeur par défaut", + "advancedSettings.field.resetToDefaultLinkText": "Réinitialiser à la valeur par défaut", + "advancedSettings.field.settingIsUnsaved": "Le paramètre n'est actuellement pas enregistré.", + "advancedSettings.field.unsavedIconLabel": "Non enregistré", + "advancedSettings.form.cancelButtonLabel": "Annuler les modifications", + "advancedSettings.form.clearNoSearchResultText": "(effacer la recherche)", + "advancedSettings.form.clearSearchResultText": "(effacer la recherche)", + "advancedSettings.form.countOfSettingsChanged": "{unsavedCount} {unsavedCount, plural, one {paramètre non enregistré} other {paramètres non enregistrés} }{hiddenCount, plural, =0 {} other {, # masqués} }", + "advancedSettings.form.noSearchResultText": "Aucun paramètre trouvé pour {queryText}. {clearSearch}", + "advancedSettings.form.requiresPageReloadToastButtonLabel": "Actualiser la page", + "advancedSettings.form.requiresPageReloadToastDescription": "Un ou plusieurs paramètres nécessitent d’actualiser la page pour pouvoir prendre effet.", + "advancedSettings.form.saveButtonLabel": "Enregistrer les modifications", + "advancedSettings.form.saveButtonTooltipWithInvalidChanges": "Corrigez les paramètres non valides avant d'enregistrer.", + "advancedSettings.form.saveErrorMessage": "Enregistrement impossible", + "advancedSettings.form.searchResultText": "Les termes de la recherche masquent {settingsCount} paramètres {clearSearch}", + "advancedSettings.pageTitle": "Paramètres", + "advancedSettings.searchBar.unableToParseQueryErrorMessage": "Impossible d'analyser la requête", + "advancedSettings.searchBarAriaLabel": "Rechercher dans les paramètres avancés", + "advancedSettings.voiceAnnouncement.ariaLabel": "Informations de résultat des paramètres avancés", + "advancedSettings.voiceAnnouncement.noSearchResultScreenReaderMessage": "Il existe {optionLenght, plural, one {# option} other {# options}} dans {sectionLenght, plural, one {# section} other {# sections}}.", + "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "Vous avez recherché {query}. Il existe {optionLenght, plural, one {# option} other {# options}} dans {sectionLenght, plural, one {# section} other {# sections}}.", + "alerts.documentationTitle": "Afficher la documentation", + "alerts.noPermissionsMessage": "Pour consulter les alertes, vous devez disposer de privilèges pour la fonctionnalité Alertes dans l'espace Kibana. Pour en savoir plus, contactez votre administrateur Kibana.", + "alerts.noPermissionsTitle": "Privilèges de fonctionnalité Kibana requis", + "autocomplete.fieldRequiredError": "Ce champ ne peut pas être vide.", + "autocomplete.invalidDateError": "Date non valide", + "autocomplete.invalidNumberError": "Nombre non valide", + "autocomplete.loadingDescription": "Chargement...", + "autocomplete.selectField": "Veuillez d'abord sélectionner un champ...", + "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.", + "charts.advancedSettings.visualization.colorMappingText": "Mappe des valeurs à des couleurs spécifiques dans les graphiques avec la palette Compatibilité.", + "charts.advancedSettings.visualization.colorMappingTextDeprecation": "Ce paramètre est déclassé et ne sera plus pris en charge à partir de la version 8.0.", + "charts.advancedSettings.visualization.colorMappingTitle": "Mapping des couleurs", + "charts.colormaps.bluesText": "Bleus", + "charts.colormaps.greensText": "Verts", + "charts.colormaps.greenToRedText": "Vert à rouge", + "charts.colormaps.greysText": "Gris", + "charts.colormaps.redsText": "Rouges", + "charts.colormaps.yellowToRedText": "Jaune à rouge", + "charts.colorPicker.clearColor": "Réinitialiser la couleur", + "charts.colorPicker.setColor.screenReaderDescription": "Définir la couleur pour la valeur {legendDataLabel}", + "charts.countText": "Décompte", + "charts.functions.palette.args.colorHelpText": "Les couleurs de la palette. Accepte un nom de couleur {html}, {hex}, {hsl}, {hsla}, {rgb} ou {rgba}.", + "charts.functions.palette.args.gradientHelpText": "Concevoir une palette de dégradés lorsque c'est possible ?", + "charts.functions.palette.args.reverseHelpText": "Inverser la palette ?", + "charts.functions.palette.args.stopHelpText": "La couleur à laquelle s’arrête la palette. Si utilisé, doit être associé à chaque couleur.", + "charts.functions.paletteHelpText": "Crée une palette de couleurs.", + "charts.functions.systemPalette.args.nameHelpText": "Nom de la palette dans la liste des palettes", + "charts.functions.systemPaletteHelpText": "Crée une palette de couleurs dynamique.", + "charts.legend.toggleLegendButtonAriaLabel": "Afficher/Masquer la légende", + "charts.legend.toggleLegendButtonTitle": "Afficher/Masquer la légende", + "charts.palettes.complimentaryLabel": "Gratuite", + "charts.palettes.coolLabel": "Froide", + "charts.palettes.customLabel": "Personnalisée", + "charts.palettes.defaultPaletteLabel": "Par défaut", + "charts.palettes.grayLabel": "Gris", + "charts.palettes.kibanaPaletteLabel": "Compatibilité", + "charts.palettes.negativeLabel": "Négative", + "charts.palettes.positiveLabel": "Positive", + "charts.palettes.statusLabel": "Statut", + "charts.palettes.temperatureLabel": "Température", + "charts.palettes.warmLabel": "Chaude", + "charts.partialData.bucketTooltipText": "La plage temporelle sélectionnée n'inclut pas ce compartiment en entier. Il se peut qu'elle contienne des données partielles.", + "console.autocomplete.addMethodMetaText": "méthode", + "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.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.exampleOutputTextarea": "Outils de développement de la console - Exemple d’éditeur", + "console.helpPage.keyboardCommands.autoIndentDescription": "Appliquer un retrait automatique à la requête en cours", + "console.helpPage.keyboardCommands.closeAutoCompleteMenuDescription": "Fermer le menu de saisie semi-automatique", + "console.helpPage.keyboardCommands.collapseAllScopesDescription": "Réduire tout sauf l’élément actif. Ajouter un décalage pour développer.", + "console.helpPage.keyboardCommands.collapseExpandCurrentScopeDescription": "Réduire/développer l’élément actif", + "console.helpPage.keyboardCommands.jumpToPreviousNextRequestDescription": "Aller au début ou à la fin de la requête précédente/suivante", + "console.helpPage.keyboardCommands.openAutoCompleteDescription": "Ouvrir la saisie semi-automatique (même sans saisie)", + "console.helpPage.keyboardCommands.openDocumentationDescription": "Ouvrir la documentation pour la requête en cours", + "console.helpPage.keyboardCommands.selectCurrentlySelectedInAutoCompleteMenuDescription": "Sélectionner le terme en surbrillance ou le premier terme du menu de saisie semi-automatique", + "console.helpPage.keyboardCommands.submitRequestDescription": "Envoyer la requête", + "console.helpPage.keyboardCommands.switchFocusToAutoCompleteMenuDescription": "Permet d’accéder au menu de saisie semi-automatique. Utilisez les flèches pour sélectionner un terme.", + "console.helpPage.keyboardCommandsTitle": "Commandes du clavier", + "console.helpPage.pageTitle": "Aide", + "console.helpPage.requestFormatDescription": "Vous pouvez saisir une ou plusieurs requêtes dans l'éditeur blanc. La console prend en charge les requêtes dans un format compact :", + "console.helpPage.requestFormatTitle": "Format de la requête", + "console.historyPage.applyHistoryButtonLabel": "Appliquer", + "console.historyPage.clearHistoryButtonLabel": "Effacer", + "console.historyPage.closehistoryButtonLabel": "Fermer", + "console.historyPage.itemOfRequestListAriaLabel": "Requête : {historyItem}", + "console.historyPage.noHistoryTextMessage": "Aucun historique disponible", + "console.historyPage.pageTitle": "Historique", + "console.historyPage.requestListAriaLabel": "Historique des requêtes envoyées", + "console.inputTextarea": "Outils de développement de la console", + "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.notification.error.couldNotSaveRequestTitle": "Impossible d'enregistrer la requête dans l'historique de la console.", + "console.notification.error.historyQuotaReachedMessage": "L'historique des requêtes est arrivé à saturation. Effacez l'historique de la console pour pouvoir enregistrer de nouvelles requêtes.", + "console.notification.error.noRequestSelectedTitle": "Aucune requête sélectionnée. Sélectionnez une requête en positionnant le curseur dessus.", + "console.notification.error.unknownErrorTitle": "Erreur de requête inconnue", + "console.outputTextarea": "Outils de développement de la console - Sortie", + "console.pageHeading": "Console", + "console.requestInProgressBadgeText": "Requête en cours", + "console.requestOptions.autoIndentButtonLabel": "Retrait automatique", + "console.requestOptions.copyAsUrlButtonLabel": "Copier en tant que cURL", + "console.requestOptions.openDocumentationButtonLabel": "Ouvrir la documentation", + "console.requestOptionsButtonAriaLabel": "Options de requête", + "console.requestTimeElapasedBadgeTooltipContent": "Temps écoulé", + "console.sendRequestButtonTooltip": "Cliquer pour envoyer la requête", + "console.settingsPage.autocompleteLabel": "Saisie semi-automatique", + "console.settingsPage.cancelButtonLabel": "Annuler", + "console.settingsPage.fieldsLabelText": "Champs", + "console.settingsPage.fontSizeLabel": "Taille de la police", + "console.settingsPage.indicesAndAliasesLabelText": "Index et alias", + "console.settingsPage.jsonSyntaxLabel": "Syntaxe JSON", + "console.settingsPage.pageTitle": "Paramètres de la console", + "console.settingsPage.refreshButtonLabel": "Actualiser les suggestions de saisie semi-automatique", + "console.settingsPage.refreshingDataDescription": "La console actualise les suggestions de saisie semi-automatique en interrogeant Elasticsearch. L’actualisation automatique peut être un problème en cas de cluster volumineux ou de réseau limité.", + "console.settingsPage.refreshingDataLabel": "Actualisation des suggestions de saisie semi-automatique", + "console.settingsPage.saveButtonLabel": "Enregistrer", + "console.settingsPage.templatesLabelText": "Modèles", + "console.settingsPage.tripleQuotesMessage": "Utiliser des guillemets triples dans le volet de sortie", + "console.settingsPage.wrapLongLinesLabelText": "Renvoyer automatiquement à la ligne", + "console.topNav.helpTabDescription": "Aide", + "console.topNav.helpTabLabel": "Aide", + "console.topNav.historyTabDescription": "Historique", + "console.topNav.historyTabLabel": "Historique", + "console.topNav.settingsTabDescription": "Paramètres", + "console.topNav.settingsTabLabel": "Paramètres", + "console.welcomePage.closeButtonLabel": "Rejeter", + "console.welcomePage.pageTitle": "Bienvenue dans la console", + "console.welcomePage.quickIntroDescription": "L'interface utilisateur de la console est divisée en deux volets : un volet éditeur (à gauche) et un volet de réponse (à droite). L'éditeur permet de saisir des requêtes et de les envoyer à Elasticsearch, tandis que le volet de réponse affiche les résultats.", + "console.welcomePage.quickIntroTitle": "Introduction rapide à l'interface utilisateur", + "console.welcomePage.quickTips.cUrlFormatForRequestsDescription": "Vous pouvez coller des requêtes au format cURL ; elles seront automatiquement traduites dans la syntaxe de la console.", + "console.welcomePage.quickTips.keyboardShortcutsDescription": "N’hésitez pas à jeter un œil aux raccourcis clavier sous le bouton Aide. Vous pourriez y trouver des choses utiles.", + "console.welcomePage.quickTips.resizeEditorDescription": "Vous pouvez redimensionner les volets de l'éditeur et de réponse en faisant glisser le séparateur situé entre les deux.", + "console.welcomePage.quickTips.submitRequestDescription": "Utilisez l’icône de triangle vert pour envoyer vos requêtes à ES.", + "console.welcomePage.quickTips.useWrenchMenuDescription": "Cliquez sur l’icône en forme de clé pour découvrir d'autres éléments utiles.", + "console.welcomePage.quickTipsTitle": "Quelques brèves astuces, pendant que j'ai toute votre attention :", + "console.welcomePage.supportedRequestFormatDescription": "Lors de la saisie d'une requête, la console fera des suggestions que vous pourrez accepter en appuyant sur Entrée/Tab. Ces suggestions sont faites en fonction de la structure de la requête, des index et des types.", + "console.welcomePage.supportedRequestFormatTitle": "La console prend en charge les requêtes dans un format compact, tel que le format cURL :", + "core.application.appContainer.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.browserDeprecationLink": "la matrice de prise en charge sur notre site web", + "core.chrome.browserDeprecationWarning": "La prise en charge d'Internet Explorer sera abandonnée dans les futures versions de ce logiciel. Veuillez consulter le site {link}.", + "core.chrome.legacyBrowserWarning": "Votre navigateur ne satisfait pas aux exigences de sécurité de Kibana.", + "core.euiAccordion.isLoading": "Chargement", + "core.euiBasicTable.selectAllRows": "Sélectionner toutes les lignes", + "core.euiBasicTable.selectThisRow": "Sélectionner cette ligne", + "core.euiBasicTable.tableAutoCaptionWithoutPagination": "Ce tableau contient {itemCount} lignes.", + "core.euiBasicTable.tableAutoCaptionWithPagination": "Ce tableau contient {itemCount} lignes sur {totalItemCount} lignes au total ; page {page} sur {pageCount}.", + "core.euiBasicTable.tableCaptionWithPagination": "{tableCaption} ; page {page} sur {pageCount}.", + "core.euiBasicTable.tablePagination": "Pagination pour le tableau précédent : {tableCaption}", + "core.euiBasicTable.tableSimpleAutoCaptionWithPagination": "Ce tableau contient {itemCount} lignes ; page {page} sur {pageCount}.", + "core.euiBottomBar.customScreenReaderAnnouncement": "Il y a un nouveau repère de région nommé {landmarkHeading} avec des commandes de niveau de page à la fin du document.", + "core.euiBottomBar.screenReaderAnnouncement": "Il y a un nouveau repère de région avec des commandes de niveau de page à la fin du document.", + "core.euiBottomBar.screenReaderHeading": "Commandes de niveau de page", + "core.euiBreadcrumbs.collapsedBadge.ariaLabel": "Voir le fil d’Ariane réduit", + "core.euiBreadcrumbs.nav.ariaLabel": "Fil d’Ariane", + "core.euiCardSelect.select": "Sélectionner", + "core.euiCardSelect.selected": "Sélectionné", + "core.euiCardSelect.unavailable": "Indisponible", + "core.euiCodeBlock.copyButton": "Copier", + "core.euiCodeBlock.fullscreenCollapse": "Réduire", + "core.euiCodeBlock.fullscreenExpand": "Développer", + "core.euiCollapsedItemActions.allActions": "Toutes les actions", + "core.euiColorPicker.alphaLabel": "Valeur (opacité) du canal Alpha", + "core.euiColorPicker.closeLabel": "Appuyez sur la flèche du bas pour ouvrir la fenêtre contextuelle des options de couleur.", + "core.euiColorPicker.colorErrorMessage": "Valeur de couleur non valide", + "core.euiColorPicker.colorLabel": "Valeur de couleur", + "core.euiColorPicker.openLabel": "Appuyez sur Échap pour fermer la fenêtre contextuelle.", + "core.euiColorPicker.popoverLabel": "Boîte de dialogue de sélection de couleur", + "core.euiColorPicker.transparent": "Transparent", + "core.euiColorPickerSwatch.ariaLabel": "Sélection de la couleur {color}", + "core.euiColorStops.screenReaderAnnouncement": "{label} : {readOnly} {disabled} Sélecteur d'arrêt de couleur. Chaque arrêt consiste en un nombre et en une valeur de couleur correspondante. Utilisez les flèches haut et bas pour sélectionner les arrêts. Appuyez sur Entrée pour créer un nouvel arrêt.", + "core.euiColorStopThumb.buttonAriaLabel": "Appuyez sur Entrée pour modifier cet arrêt. Appuyez sur Échap pour revenir au groupe.", + "core.euiColorStopThumb.buttonTitle": "Cliquez pour modifier, faites glisser pour repositionner.", + "core.euiColorStopThumb.removeLabel": "Supprimer cet arrêt", + "core.euiColorStopThumb.screenReaderAnnouncement": "La fenêtre contextuelle qui vient de s’ouvrir contient un formulaire de modification d'arrêt de couleur. Appuyez sur Tab pour parcourir les commandes du formulaire ou sur Échap pour fermer la fenêtre.", + "core.euiColorStopThumb.stopErrorMessage": "Valeur hors limites", + "core.euiColorStopThumb.stopLabel": "Valeur d'arrêt", + "core.euiColumnActions.hideColumn": "Masquer la colonne", + "core.euiColumnActions.moveLeft": "Déplacer vers la gauche", + "core.euiColumnActions.moveRight": "Déplacer vers la droite", + "core.euiColumnActions.sort": "Trier {schemaLabel}", + "core.euiColumnSelector.button": "Colonnes", + "core.euiColumnSelector.buttonActivePlural": "{numberOfHiddenFields} colonnes masquées", + "core.euiColumnSelector.buttonActiveSingular": "{numberOfHiddenFields} colonne masquée", + "core.euiColumnSelector.hideAll": "Tout masquer", + "core.euiColumnSelector.search": "Recherche", + "core.euiColumnSelector.searchcolumns": "Rechercher dans les colonnes", + "core.euiColumnSelector.selectAll": "Afficher tout", + "core.euiColumnSorting.button": "Trier les champs", + "core.euiColumnSorting.clearAll": "Annuler le tri", + "core.euiColumnSorting.emptySorting": "Aucun champ n'est trié actuellement.", + "core.euiColumnSorting.pickFields": "Sélectionner les champs de tri", + "core.euiColumnSorting.sortFieldAriaLabel": "Trier par :", + "core.euiColumnSortingDraggable.defaultSortAsc": "A-Z", + "core.euiColumnSortingDraggable.defaultSortDesc": "Z-A", + "core.euiComboBoxOptionsList.allOptionsSelected": "Vous avez sélectionné toutes les options disponibles.", + "core.euiComboBoxOptionsList.alreadyAdded": "{label} a déjà été ajouté.", + "core.euiComboBoxOptionsList.createCustomOption": "Ajouter {searchValue} en tant qu'option personnalisée", + "core.euiComboBoxOptionsList.delimiterMessage": "Ajouter chaque élément en séparant par {delimiter}", + "core.euiComboBoxOptionsList.loadingOptions": "Options de chargement", + "core.euiComboBoxOptionsList.noAvailableOptions": "Aucune option n’est disponible.", + "core.euiComboBoxOptionsList.noMatchingOptions": "{searchValue} ne correspond à aucune option.", + "core.euiComboBoxPill.removeSelection": "Supprimer {children} de la sélection de ce groupe", + "core.euiCommonlyUsedTimeRanges.legend": "Couramment utilisées", + "core.euiControlBar.customScreenReaderAnnouncement": "Il y a un nouveau repère de région nommé {landmarkHeading} avec des commandes de niveau de page à la fin du document.", + "core.euiControlBar.screenReaderAnnouncement": "Il y a un nouveau repère de région avec des commandes de niveau de page à la fin du document.", + "core.euiControlBar.screenReaderHeading": "Commandes de niveau de page", + "core.euiDataGrid.ariaLabel": "{label} ; page {page} sur {pageCount}.", + "core.euiDataGrid.ariaLabelledBy": "Page {page} sur {pageCount}.", + "core.euiDataGrid.screenReaderNotice": "Cette cellule contient du contenu interactif.", + "core.euiDataGridHeaderCell.headerActions": "Actions d'en-tête", + "core.euiDataGridSchema.booleanSortTextAsc": "Faux-Vrai", + "core.euiDataGridSchema.booleanSortTextDesc": "Vrai-Faux", + "core.euiDataGridSchema.currencySortTextAsc": "Bas-Haut", + "core.euiDataGridSchema.currencySortTextDesc": "Haut-Bas", + "core.euiDataGridSchema.dateSortTextAsc": "Ancien-Nouveau", + "core.euiDataGridSchema.dateSortTextDesc": "Nouveau-Ancien", + "core.euiDataGridSchema.jsonSortTextAsc": "Petit-Grand", + "core.euiDataGridSchema.jsonSortTextDesc": "Grand-Petit", + "core.euiDataGridSchema.numberSortTextAsc": "Bas-Haut", + "core.euiDataGridSchema.numberSortTextDesc": "Haut-Bas", + "core.euiDatePopoverButton.invalidTitle": "Date non valide : {title}", + "core.euiDatePopoverButton.outdatedTitle": "Mise à jour requise : {title}", + "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.euiFilePicker.clearSelectedFiles": "Effacer les fichiers sélectionnés", + "core.euiFilePicker.removeSelected": "Supprimer", + "core.euiFlyout.closeAriaLabel": "Fermer cette boîte de dialogue", + "core.euiForm.addressFormErrors": "Veuillez remédier aux erreurs signalées en surbrillance.", + "core.euiFormControlLayoutClearButton.label": "Effacer l'entrée", + "core.euiHeaderLinks.appNavigation": "Menu de l'application", + "core.euiHeaderLinks.openNavigationMenu": "Ouvrir le menu", + "core.euiHue.label": "Sélectionner la valeur \"hue\" du mode de couleur HSV", + "core.euiImage.closeImage": "Fermer l'image {alt} en plein écran", + "core.euiImage.openImage": "Ouvrir l'image {alt} en plein écran", + "core.euiLink.external.ariaLabel": "Lien externe", + "core.euiLink.newTarget.screenReaderOnlyText": "(s’ouvre dans un nouvel onglet ou une nouvelle fenêtre)", + "core.euiMarkdownEditorFooter.closeButton": "Fermer", + "core.euiMarkdownEditorFooter.errorsTitle": "Erreurs", + "core.euiMarkdownEditorFooter.openUploadModal": "Activer le mode de chargement de fichiers", + "core.euiMarkdownEditorFooter.showMarkdownHelp": "Afficher l'aide de Markdown", + "core.euiMarkdownEditorFooter.showSyntaxErrors": "Afficher les erreurs", + "core.euiMarkdownEditorFooter.supportedFileTypes": "Fichiers pris en charge : {supportedFileTypes}", + "core.euiMarkdownEditorFooter.syntaxTitle": "Aide pour la syntaxe", + "core.euiMarkdownEditorFooter.unsupportedFileType": "Type de fichiers non pris en charge", + "core.euiMarkdownEditorFooter.uploadingFiles": "Cliquer pour charger des fichiers", + "core.euiMarkdownEditorToolbar.editor": "Éditeur", + "core.euiMarkdownEditorToolbar.previewMarkdown": "Aperçu", + "core.euiModal.closeModal": "Ferme cette fenêtre modale.", + "core.euiNotificationEventMessages.accordionAriaLabelButtonText": "+ {messagesLength} messages pour {eventName}", + "core.euiNotificationEventMessages.accordionButtonText": "+ {messagesLength} de plus", + "core.euiNotificationEventMessages.accordionHideText": "masquer", + "core.euiNotificationEventMeta.contextMenuButton": "Menu pour {eventName}", + "core.euiNotificationEventReadButton.markAsRead": "Marquer comme lu", + "core.euiNotificationEventReadButton.markAsReadAria": "Marquer {eventName} comme lu", + "core.euiNotificationEventReadButton.markAsUnread": "Marquer comme non lu", + "core.euiNotificationEventReadButton.markAsUnreadAria": "Marquer {eventName} comme non lu", + "core.euiNotificationEventReadIcon.read": "Lu", + "core.euiNotificationEventReadIcon.readAria": "{eventName} lu", + "core.euiNotificationEventReadIcon.unread": "Non lu", + "core.euiNotificationEventReadIcon.unreadAria": "{eventName} non lu", + "core.euiPagination.firstRangeAriaLabel": "Ignorer les pages 2 à {lastPage}", + "core.euiPagination.lastRangeAriaLabel": "Ignorer les pages {firstPage} à {lastPage}", + "core.euiPagination.pageOfTotalCompressed": "{page} sur {total}", + "core.euiPaginationButton.longPageString": "Page {page} sur {totalPages}", + "core.euiPaginationButton.shortPageString": "Page {page}", + "core.euiPinnableListGroup.pinExtraActionLabel": "Épingler l'élément", + "core.euiPinnableListGroup.pinnedExtraActionLabel": "Désépingler l'élément", + "core.euiPopover.screenReaderAnnouncement": "Il s’agit d’une boîte de dialogue. Appuyez sur Échap pour quitter.", + "core.euiProgress.valueText": "{value} %", + "core.euiQuickSelect.applyButton": "Appliquer", + "core.euiQuickSelect.fullDescription": "Actuellement défini sur {timeTense} {timeValue} {timeUnit}.", + "core.euiQuickSelect.legendText": "Sélection rapide d’une plage temporelle", + "core.euiQuickSelect.nextLabel": "Fenêtre temporelle suivante", + "core.euiQuickSelect.previousLabel": "Fenêtre temporelle précédente", + "core.euiQuickSelect.quickSelectTitle": "Sélection rapide", + "core.euiQuickSelect.tenseLabel": "Durée", + "core.euiQuickSelect.unitLabel": "Unité de temps", + "core.euiQuickSelect.valueLabel": "Valeur de temps", + "core.euiRecentlyUsed.legend": "Plages de dates récemment utilisées", + "core.euiRefreshInterval.legend": "Actualiser toutes les", + "core.euiRelativeTab.fullDescription": "L'unité peut être modifiée. Elle est actuellement définie sur {unit}.", + "core.euiRelativeTab.numberInputError": "Doit être >= 0.", + "core.euiRelativeTab.numberInputLabel": "Nombre d'intervalles", + "core.euiRelativeTab.relativeDate": "Date de {position}", + "core.euiRelativeTab.roundingLabel": "Arrondir à {unit}", + "core.euiRelativeTab.unitInputLabel": "Intervalle relatif", + "core.euiResizableButton.horizontalResizerAriaLabel": "Utilisez les flèches gauche et droite pour ajuster la taille des panneaux.", + "core.euiResizableButton.verticalResizerAriaLabel": "Utilisez les flèches vers le haut et vers le bas pour ajuster la taille des panneaux.", + "core.euiResizablePanel.toggleButtonAriaLabel": "Appuyez pour afficher/masquer ce panneau.", + "core.euiSaturation.ariaLabel": "Curseur à 2 axes de valeur et de saturation du mode de couleur HSV", + "core.euiSaturation.screenReaderInstructions": "Utilisez les touches fléchées pour parcourir le dégradé de couleurs. Les coordonnées seront utilisées pour calculer les chiffres de \"valeur\" et de \"saturation\" du mode de couleur HSV, dans une plage de 0 à 1. Les flèches gauche et droite permettent de modifier la saturation. Les flèches vers le haut et vers le bas permettent de modifier la valeur.", + "core.euiSelectable.loadingOptions": "Options de chargement", + "core.euiSelectable.noAvailableOptions": "Aucune option disponible", + "core.euiSelectable.noMatchingOptions": "{searchValue} ne correspond à aucune option.", + "core.euiSelectable.placeholderName": "Options de filtre", + "core.euiSelectableListItem.excludedOption": "Option exclue.", + "core.euiSelectableListItem.excludedOptionInstructions": "Pour désélectionner cette option, appuyez sur Entrée.", + "core.euiSelectableListItem.includedOption": "Option incluse.", + "core.euiSelectableListItem.includedOptionInstructions": "Pour exclure cette option, appuyez sur Entrée.", + "core.euiSelectableTemplateSitewide.loadingResults": "Chargement des résultats", + "core.euiSelectableTemplateSitewide.noResults": "Aucun résultat disponible", + "core.euiSelectableTemplateSitewide.onFocusBadgeGoTo": "Atteindre", + "core.euiSelectableTemplateSitewide.searchPlaceholder": "Rechercher tout...", + "core.euiStat.loadingText": "Statistiques en cours de chargement", + "core.euiStepStrings.complete": "L'étape {number} : {title} est terminée.", + "core.euiStepStrings.current": "L’étape {number} : {title} est en cours.", + "core.euiStepStrings.disabled": "L'étape {number} : {title} est désactivée.", + "core.euiStepStrings.errors": "L'étape {number} : {title} contient des erreurs.", + "core.euiStepStrings.incomplete": "L'étape {number} : {title} est incomplète.", + "core.euiStepStrings.loading": "L'étape {number} : {title} est en cours de chargement.", + "core.euiStepStrings.simpleComplete": "L'étape {number} est terminée.", + "core.euiStepStrings.simpleCurrent": "L’étape {number} est en cours.", + "core.euiStepStrings.simpleDisabled": "L'étape {number} est désactivée.", + "core.euiStepStrings.simpleErrors": "L'étape {number} contient des erreurs.", + "core.euiStepStrings.simpleIncomplete": "L'étape {number} est incomplète.", + "core.euiStepStrings.simpleLoading": "L'étape {number} est en cours de chargement.", + "core.euiStepStrings.simpleStep": "Étape {number}", + "core.euiStepStrings.simpleWarning": "L'étape {number} contient des avertissements.", + "core.euiStepStrings.step": "Étape {number} : {title}", + "core.euiStepStrings.warning": "L'étape {number} : {title} contient des avertissements.", + "core.euiSuperSelectControl.selectAnOption": "Sélectionner une option : l’option {selectedValue} est sélectionnée.", + "core.euiSuperUpdateButton.cannotUpdateTooltip": "Mise à jour impossible", + "core.euiSuperUpdateButton.clickToApplyTooltip": "Cliquer pour appliquer", + "core.euiSuperUpdateButton.refreshButtonLabel": "Actualiser", + "core.euiSuperUpdateButton.updateButtonLabel": "Mettre à jour", + "core.euiSuperUpdateButton.updatingButtonLabel": "Mise à jour", + "core.euiTableHeaderCell.titleTextWithDesc": "{innerText} ; {description}", + "core.euiTablePagination.rowsPerPage": "Lignes par page", + "core.euiTablePagination.rowsPerPageOption": "{rowsPerPage} lignes", + "core.euiTableSortMobile.sorting": "Tri", + "core.euiToast.dismissToast": "Rejeter le toast", + "core.euiToast.newNotification": "Une nouvelle notification apparaît.", + "core.euiToast.notification": "Notification", + "core.euiTourStep.closeTour": "Fermer la visite", + "core.euiTourStep.endTour": "Terminer la visite", + "core.euiTourStep.skipTour": "Ignorer la visite", + "core.euiTourStepIndicator.ariaLabel": "Étape {number} {status}", + "core.euiTourStepIndicator.isActive": "active", + "core.euiTourStepIndicator.isComplete": "terminée", + "core.euiTourStepIndicator.isIncomplete": "incomplète", + "core.euiTreeView.ariaLabel": "{nodeLabel} enfant de {ariaLabel}", + "core.euiTreeView.listNavigationInstructions": "Utilisez les touches fléchées pour parcourir rapidement cette liste.", + "core.fatalErrors.clearYourSessionButtonLabel": "Effacer votre session", + "core.fatalErrors.goBackButtonLabel": "Retour", + "core.fatalErrors.somethingWentWrongTitle": "Un problème est survenu.", + "core.fatalErrors.tryRefreshingPageDescription": "Essayez d'actualiser la page. Si cela ne fonctionne pas, retournez à la page précédente ou effacez vos données de session.", + "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.status.greenTitle": "Vert", + "core.status.redTitle": "Rouge", + "core.status.yellowTitle": "Jaune", + "core.statusPage.loadStatus.serverIsDownErrorMessage": "Échec de requête du statut du serveur. Votre serveur est peut-être indisponible ?", + "core.statusPage.loadStatus.serverStatusCodeErrorMessage": "Échec de requête du statut du serveur avec le code de statut {responseStatus}.", + "core.statusPage.metricsTiles.columns.heapTotalHeader": "Tas total", + "core.statusPage.metricsTiles.columns.heapUsedHeader": "Tas utilisé", + "core.statusPage.metricsTiles.columns.loadHeader": "Charger", + "core.statusPage.metricsTiles.columns.requestsPerSecHeader": "Requêtes par seconde", + "core.statusPage.metricsTiles.columns.resTimeAvgHeader": "Temps de réponse moyen", + "core.statusPage.metricsTiles.columns.resTimeMaxHeader": "Temps de réponse max.", + "core.statusPage.serverStatus.statusTitle": "Statut Kibana : {kibanaStatus}", + "core.statusPage.statusApp.loadingErrorText": "Une erreur s'est produite lors du chargement du statut.", + "core.statusPage.statusApp.statusActions.buildText": "CRÉER {buildNum}", + "core.statusPage.statusApp.statusActions.commitText": "VALIDER {buildSha}", + "core.statusPage.statusApp.statusTitle": "Statut du plug-in", + "core.statusPage.statusTable.columns.idHeader": "ID", + "core.statusPage.statusTable.columns.statusHeader": "Statut", + "core.toasts.errorToast.seeFullError": "Voir l'erreur en intégralité", + "core.ui_settings.params.darkModeText": "Activez le mode sombre pour l'interface utilisateur Kibana. Vous devez actualiser la page pour que ce paramètre s’applique.", + "core.ui_settings.params.darkModeTitle": "Mode sombre", + "core.ui_settings.params.dateFormat.dayOfWeekText": "Quel est le premier jour de la semaine ?", + "core.ui_settings.params.dateFormat.dayOfWeekTitle": "Jour de la semaine", + "core.ui_settings.params.dateFormat.optionsLinkText": "format", + "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "Intervalles ISO8601", + "core.ui_settings.params.dateFormat.scaledText": "Les valeurs qui définissent le format utilisé lorsque les données temporelles sont rendues dans l'ordre, et lorsque les horodatages formatés doivent s'adapter à l'intervalle entre les mesures. Les clés sont {intervalsLink}.", + "core.ui_settings.params.dateFormat.scaledTitle": "Format de date scalé", + "core.ui_settings.params.dateFormat.timezone.invalidValidationMessage": "Fuseau horaire non valide : {timezone}", + "core.ui_settings.params.dateFormat.timezoneText": "Fuseau horaire à utiliser. L’option {defaultOption} utilise le fuseau horaire détecté par le navigateur.", + "core.ui_settings.params.dateFormat.timezoneTitle": "Fuseau horaire pour le format de date", + "core.ui_settings.params.dateFormatText": "{formatLink} utilisé pour les dates formatées", + "core.ui_settings.params.dateFormatTitle": "Format de date", + "core.ui_settings.params.dateNanosFormatText": "Utilisé pour le type de données {dateNanosLink} d'Elasticsearch", + "core.ui_settings.params.dateNanosFormatTitle": "Date au format nanosecondes", + "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", + "core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "Jour de la semaine non valide : {dayOfWeek}", + "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "Doit être une URL relative.", + "core.ui_settings.params.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.", + "core.ui_settings.params.defaultRoute.defaultRouteTitle": "Chemin par défaut", + "core.ui_settings.params.disableAnimationsText": "Désactivez toutes les animations non nécessaires dans l'interface utilisateur de Kibana. Actualisez la page pour appliquer les modifications.", + "core.ui_settings.params.disableAnimationsTitle": "Désactiver les animations", + "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.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.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.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.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", + "core.ui_settings.params.themeVersionTitle": "Version du thème", + "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "Accueil d'Elastic", + "core.ui.chrome.headerGlobalNav.helpMenuAskElasticTitle": "Questions Elastic", + "core.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "Menu d'aide", + "core.ui.chrome.headerGlobalNav.helpMenuDocumentation": "Documentation", + "core.ui.chrome.headerGlobalNav.helpMenuGiveFeedbackOnApp": "Donner un retour sur {appName}", + "core.ui.chrome.headerGlobalNav.helpMenuGiveFeedbackTitle": "Donner un retour", + "core.ui.chrome.headerGlobalNav.helpMenuKibanaDocumentationTitle": "Documentation Kibana", + "core.ui.chrome.headerGlobalNav.helpMenuOpenGitHubIssueTitle": "Ouvrir un ticket dans GitHub", + "core.ui.chrome.headerGlobalNav.helpMenuTitle": "Aide", + "core.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}", + "core.ui.chrome.headerGlobalNav.logoAriaLabel": "Logo Elastic", + "core.ui.enterpriseSearchNavList.label": "Enterprise Search", + "core.ui.errorUrlOverflow.bigUrlWarningNotificationMessage": "Activez l'option {storeInSessionStorageParam} dans les {advancedSettingsLink} ou simplifiez les visuels à l'écran.", + "core.ui.errorUrlOverflow.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "paramètres avancés", + "core.ui.errorUrlOverflow.bigUrlWarningNotificationTitle": "L'URL est longue et Kibana pourrait cesser de fonctionner.", + "core.ui.errorUrlOverflow.errorTitle": "L'URL pour cet objet est trop longue, et nous ne pouvons pas l'afficher.", + "core.ui.errorUrlOverflow.optionsToFixError.doNotUseIEText": "Veuillez utiliser un navigateur moderne. Tous les autres navigateurs pris en charge connus n'ont pas cette limitation.", + "core.ui.errorUrlOverflow.optionsToFixError.enableOptionText": "Activez l'option {storeInSessionStorageConfig} sous {kibanaSettingsLink}.", + "core.ui.errorUrlOverflow.optionsToFixError.enableOptionText.advancedSettingsLinkText": "Paramètres avancés", + "core.ui.errorUrlOverflow.optionsToFixError.removeStuffFromDashboardText": "Simplifiez l'objet en cours de modification en supprimant du contenu ou des filtres.", + "core.ui.errorUrlOverflow.optionsToFixErrorDescription": "À essayer :", + "core.ui.kibanaNavList.label": "Analytique", + "core.ui.legacyBrowserMessage": "Cette installation Elastic présente des exigences de sécurité strictes auxquelles votre navigateur ne satisfait pas.", + "core.ui.legacyBrowserTitle": "Merci de mettre votre navigateur à niveau.", + "core.ui.loadingIndicatorAriaLabel": "Chargement du contenu", + "core.ui.managementNavList.label": "Gestion", + "core.ui.observabilityNavList.label": "Observabilité", + "core.ui.overlays.banner.attentionTitle": "Attention", + "core.ui.overlays.banner.closeButtonLabel": "Fermer", + "core.ui.primaryNav.pinnedLinksAriaLabel": "Liens épinglés", + "core.ui.primaryNav.screenReaderLabel": "Principale", + "core.ui.primaryNav.toggleNavAriaLabel": "Activer/Désactiver la navigation principale", + "core.ui.primaryNavSection.screenReaderLabel": "Liens de navigation principale, {category}", + "core.ui.publicBaseUrlWarning.muteWarningButtonLabel": "Avertissement de mise sur Muet", + "core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel}, type : {pageType}", + "core.ui.recentlyViewed": "Récemment consulté", + "core.ui.recentlyViewedAriaLabel": "Liens récemment consultés", + "core.ui.securityNavList.label": "Security", + "core.ui.welcomeErrorMessage": "Elastic ne s'est pas chargé correctement. Vérifiez la sortie du serveur pour plus d'informations.", + "core.ui.welcomeMessage": "Chargement d'Elastic", + "dashboard.actions.DownloadCreateDrilldownAction.displayName": "Télécharger au format CSV", + "dashboard.actions.downloadOptionsUnsavedFilename": "sans titre", + "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "Minimiser", + "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "Maximiser le panneau", + "dashboard.addPanel.noMatchingObjectsMessage": "Aucun objet correspondant trouvé.", + "dashboard.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} a été ajouté.", + "dashboard.appLeaveConfirmModal.cancelButtonLabel": "Annuler", + "dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "Quitter le tableau de bord sans enregistrer ?", + "dashboard.appLeaveConfirmModal.unsavedChangesTitle": "Modifications non enregistrées", + "dashboard.badge.readOnly.text": "Lecture seule", + "dashboard.badge.readOnly.tooltip": "Impossible d'enregistrer les tableaux de bord", + "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "Poursuivre les modifications", + "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "Ignorer les modifications", + "dashboard.changeViewModeConfirmModal.description": "Vous pouvez conserver ou ignorer vos modifications lors du retour en mode Affichage. Les modifications ignorées ne peuvent toutefois pas être récupérées.", + "dashboard.changeViewModeConfirmModal.keepUnsavedChangesButtonLabel": "Conserver les modifications", + "dashboard.changeViewModeConfirmModal.leaveEditModeTitle": "Vous avez des modifications non enregistrées.", + "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "Titre du tableau de bord cloné", + "dashboard.createConfirmModal.cancelButtonLabel": "Annuler", + "dashboard.createConfirmModal.confirmButtonLabel": "Redémarrer", + "dashboard.createConfirmModal.continueButtonLabel": "Poursuivre les modifications", + "dashboard.createConfirmModal.unsavedChangesSubtitle": "Vous pouvez poursuivre les modifications ou utiliser un tableau de bord vierge.", + "dashboard.createConfirmModal.unsavedChangesTitle": "Nouveau tableau de bord déjà en cours", + "dashboard.dashboardAppBreadcrumbsTitle": "Tableau de bord", + "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "Impossible de charger le tableau de bord.", + "dashboard.dashboardPageTitle": "Tableaux de bord", + "dashboard.dashboardWasNotSavedDangerMessage": "Le tableau de bord \"{dashTitle}\" n'a pas été enregistré. Erreur : {errorMessage}", + "dashboard.dashboardWasSavedSuccessMessage": "Le tableau de bord \"{dashTitle}\" a été enregistré.", + "dashboard.discardChangesConfirmModal.cancelButtonLabel": "Annuler", + "dashboard.discardChangesConfirmModal.confirmButtonLabel": "Ignorer les modifications", + "dashboard.discardChangesConfirmModal.discardChangesDescription": "Une fois les modifications ignorées, vous ne pourrez pas les récupérer.", + "dashboard.discardChangesConfirmModal.discardChangesTitle": "Ignorer les modifications apportées au tableau de bord ?", + "dashboard.editorMenu.aggBasedGroupTitle": "Basé sur une agrégation", + "dashboard.embedUrlParamExtension.filterBar": "Barre de filtre", + "dashboard.embedUrlParamExtension.include": "Inclure", + "dashboard.embedUrlParamExtension.query": "Requête", + "dashboard.embedUrlParamExtension.timeFilter": "Filtre temporel", + "dashboard.embedUrlParamExtension.topMenu": "Menu supérieur", + "dashboard.emptyDashboardAdditionalPrivilege": "Des privilèges supplémentaires sont requis pour pouvoir modifier ce tableau de bord.", + "dashboard.emptyDashboardTitle": "Ce tableau de bord est vide.", + "dashboard.emptyWidget.addPanelDescription": "Créez du contenu qui raconte une histoire sur vos données.", + "dashboard.emptyWidget.addPanelTitle": "Ajoutez votre première visualisation.", + "dashboard.factory.displayName": "Tableau de bord", + "dashboard.featureCatalogue.dashboardDescription": "Affichez et partagez une collection de visualisations et de recherches enregistrées.", + "dashboard.featureCatalogue.dashboardSubtitle": "Analysez des données à l’aide de tableaux de bord.", + "dashboard.featureCatalogue.dashboardTitle": "Tableau de bord", + "dashboard.fillDashboardTitle": "Ce tableau de bord est vide. Remplissons-le.", + "dashboard.helpMenu.appName": "Tableaux de bord", + "dashboard.howToStartWorkingOnNewDashboardDescription": "Cliquez sur Modifier dans la barre de menu ci-dessus pour commencer à ajouter des panneaux.", + "dashboard.howToStartWorkingOnNewDashboardEditLinkAriaLabel": "Modifier le tableau de bord", + "dashboard.labs.enableLabsDescription": "Cet indicateur détermine si l'observateur a accès au bouton Ateliers, un moyen rapide d'activer et de désactiver les fonctionnalités expérimentales dans le tableau de bord.", + "dashboard.labs.enableUI": "Activer le bouton Ateliers dans le tableau de bord", + "dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription": "Vous pouvez combiner les vues de données de n'importe quelle application Kibana dans un seul tableau de bord afin de tout regrouper.", + "dashboard.listing.createNewDashboard.createButtonLabel": "Créer un nouveau tableau de bord", + "dashboard.listing.createNewDashboard.newToKibanaDescription": "Vous êtes nouveau sur Kibana ? {sampleDataInstallLink} pour découvrir l'application.", + "dashboard.listing.createNewDashboard.sampleDataInstallLinkText": "Installez un exemple de données", + "dashboard.listing.createNewDashboard.title": "Créer votre premier tableau de bord", + "dashboard.listing.readonlyNoItemsBody": "Aucun tableau de bord n'est disponible. Pour modifier vos autorisations afin d’afficher les tableaux de bord dans cet espace, contactez votre administrateur.", + "dashboard.listing.readonlyNoItemsTitle": "Aucun tableau de bord à afficher", + "dashboard.listing.table.descriptionColumnName": "Description", + "dashboard.listing.table.entityName": "tableau de bord", + "dashboard.listing.table.entityNamePlural": "tableaux de bord", + "dashboard.listing.table.titleColumnName": "Titre", + "dashboard.listing.unsaved.discardAria": "Ignorer les modifications apportées à {title}", + "dashboard.listing.unsaved.discardTitle": "Ignorer les modifications", + "dashboard.listing.unsaved.editAria": "Poursuivre les modifications apportées à {title}", + "dashboard.listing.unsaved.editTitle": "Poursuivre les modifications", + "dashboard.listing.unsaved.loading": "Chargement", + "dashboard.listing.unsaved.unsavedChangesTitle": "Vous avez des modifications non enregistrées dans le {dash} suivant.", + "dashboard.migratedChanges": "Certains des panneaux ont été mis à jour vers la version la plus récente.", + "dashboard.noMatchRoute.bannerText": "L'application de tableau de bord ne reconnaît pas ce chemin : {route}.", + "dashboard.noMatchRoute.bannerTitleText": "Page introuvable", + "dashboard.panel.AddToLibrary": "Enregistrer dans la bibliothèque", + "dashboard.panel.addToLibrary.successMessage": "Le panneau {panelTitle} a été ajouté à la bibliothèque Visualize.", + "dashboard.panel.clonedToast": "Panneau cloné", + "dashboard.panel.clonePanel": "Cloner le panneau", + "dashboard.panel.copyToDashboard.cancel": "Annuler", + "dashboard.panel.copyToDashboard.description": "Sélectionnez l'emplacement où copier le panneau. Vous avez été redirigé vers le tableau de bord de destination.", + "dashboard.panel.copyToDashboard.existingDashboardOptionLabel": "Tableau de bord existant", + "dashboard.panel.copyToDashboard.goToDashboard": "Copier et accéder au tableau de bord", + "dashboard.panel.copyToDashboard.newDashboardOptionLabel": "Nouveau tableau de bord", + "dashboard.panel.copyToDashboard.title": "Copier dans le tableau de bord", + "dashboard.panel.invalidData": "Données non valides dans l'url", + "dashboard.panel.LibraryNotification": "Notification de la bibliothèque Visualize", + "dashboard.panel.libraryNotification.ariaLabel": "Afficher les informations de la bibliothèque et dissocier ce panneau", + "dashboard.panel.libraryNotification.toolTip": "La modification de ce panneau pourrait affecter d’autres tableaux de bord. Pour modifier ce panneau uniquement, dissociez-le de la bibliothèque.", + "dashboard.panel.removePanel.replacePanel": "Remplacer le panneau", + "dashboard.panel.title.clonedTag": "copier", + "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "Impossible de migrer les données du panneau pour une rétro-compatibilité \"6.1.0\". Le panneau ne contient pas les champs de colonne et/ou de ligne attendus.", + "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "Impossible de migrer les données du panneau pour une rétro-compatibilité \"6.3.0\". Le panneau ne contient pas le champ attendu : {key}.", + "dashboard.panel.unlinkFromLibrary": "Dissocier de la bibliothèque", + "dashboard.panel.unlinkFromLibrary.successMessage": "Le panneau {panelTitle} n'est plus connecté à la bibliothèque Visualize.", + "dashboard.panelStorageError.clearError": "Une erreur s'est produite lors de la suppression des modifications non enregistrées : {message}.", + "dashboard.panelStorageError.getError": "Une erreur s'est produite lors de la récupération des modifications non enregistrées : {message}.", + "dashboard.panelStorageError.setError": "Une erreur s'est produite lors de la définition des modifications non enregistrées : {message}.", + "dashboard.placeholder.factory.displayName": "paramètre fictif", + "dashboard.savedDashboard.newDashboardTitle": "Nouveau tableau de bord", + "dashboard.solutionToolbar.addPanelButtonLabel": "Créer une visualisation", + "dashboard.solutionToolbar.editorMenuButtonLabel": "Tous les types", + "dashboard.strings.dashboardEditTitle": "Modification de {title}", + "dashboard.topNav.cloneModal.cancelButtonLabel": "Annuler", + "dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "Cloner le tableau de bord", + "dashboard.topNav.cloneModal.confirmButtonLabel": "Confirmer le clonage", + "dashboard.topNav.cloneModal.confirmCloneDescription": "Confirmer le clonage", + "dashboard.topNav.cloneModal.dashboardExistsDescription": "Cliquez sur {confirmClone} pour cloner le tableau de bord avec le titre dupliqué.", + "dashboard.topNav.cloneModal.dashboardExistsTitle": "Un tableau de bord nommé {newDashboardName} existe déjà.", + "dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "Veuillez saisir un autre nom pour votre tableau de bord.", + "dashboard.topNav.labsButtonAriaLabel": "ateliers", + "dashboard.topNav.labsConfigDescription": "Ateliers", + "dashboard.topNav.options.hideAllPanelTitlesSwitchLabel": "Afficher les titres de panneau", + "dashboard.topNav.options.syncColorsBetweenPanelsSwitchLabel": "Synchroniser les palettes de couleur de tous les panneaux", + "dashboard.topNav.options.useMarginsBetweenPanelsSwitchLabel": "Utiliser des marges entre les panneaux", + "dashboard.topNav.saveModal.descriptionFormRowLabel": "Description", + "dashboard.topNav.saveModal.objectType": "tableau de bord", + "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "Le filtre temporel est défini sur l’option sélectionnée chaque fois que ce tableau de bord est chargé.", + "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel": "Enregistrer la plage temporelle avec le tableau de bord", + "dashboard.topNav.showCloneModal.dashboardCopyTitle": "Copie de {title}", + "dashboard.topNave.cancelButtonAriaLabel": "Basculer en mode Affichage", + "dashboard.topNave.cloneButtonAriaLabel": "cloner", + "dashboard.topNave.cloneConfigDescription": "Créer une copie du tableau de bord", + "dashboard.topNave.editButtonAriaLabel": "modifier", + "dashboard.topNave.editConfigDescription": "Basculer en mode Édition", + "dashboard.topNave.fullScreenButtonAriaLabel": "plein écran", + "dashboard.topNave.fullScreenConfigDescription": "Mode Plein écran", + "dashboard.topNave.optionsButtonAriaLabel": "options", + "dashboard.topNave.optionsConfigDescription": "Options", + "dashboard.topNave.saveAsButtonAriaLabel": "enregistrer sous", + "dashboard.topNave.saveAsConfigDescription": "Enregistrer en tant que nouveau tableau de bord", + "dashboard.topNave.saveButtonAriaLabel": "enregistrer", + "dashboard.topNave.saveConfigDescription": "Enregistrer le tableau de bord sans invite de confirmation", + "dashboard.topNave.shareButtonAriaLabel": "partager", + "dashboard.topNave.shareConfigDescription": "Partager le tableau de bord", + "dashboard.topNave.viewConfigDescription": "Basculer en mode Affichage uniquement", + "dashboard.unsavedChangesBadge": "Modifications non enregistrées", + "dashboard.urlWasRemovedInSixZeroWarningMessage": "L'url \"dashboard/create\" a été supprimée dans la version 6.0. Veuillez mettre vos signets à jour.", + "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}", + "data.advancedSettings.autocompleteValueSuggestionMethod": "Méthode de suggestion de saisie semi-automatique", + "data.advancedSettings.autocompleteValueSuggestionMethodLearnMoreLink": "En savoir plus.", + "data.advancedSettings.autocompleteValueSuggestionMethodLink": "En savoir plus.", + "data.advancedSettings.autocompleteValueSuggestionMethodText": "La méthode utilisée pour générer des suggestions de valeur pour la saisie semi-automatique KQL. Sélectionnez terms_enum pour utiliser l'API d'énumération de termes d'Elasticsearch afin d’améliorer les performances de suggestion de saisie semi-automatique. Sélectionnez terms_agg pour utiliser l'agrégation de termes d'Elasticsearch. {learnMoreLink}", + "data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText": "Préférence de requête", + "data.advancedSettings.courier.customRequestPreferenceText": "{requestPreferenceLink} utilisé lorsque {setRequestReferenceSetting} est défini sur {customSettingValue}.", + "data.advancedSettings.courier.customRequestPreferenceTitle": "Préférence de requête personnalisée", + "data.advancedSettings.courier.ignoreFilterText": "Cette configuration améliore la prise en charge des tableaux de bord contenant des visualisations accédant à des index différents. Lorsque ce paramètre est désactivé, tous les filtres sont appliqués à toutes les visualisations. En cas d'activation, le ou les filtres sont ignorés pour une visualisation lorsque l'index de celle-ci ne contient pas le champ de filtrage.", + "data.advancedSettings.courier.ignoreFilterTitle": "Ignorer le ou les filtres", + "data.advancedSettings.courier.maxRequestsText": "Contrôle le paramètre {maxRequestsLink} utilisé pour les requêtes _msearch envoyées par Kibana. Définir ce paramètre sur 0 permet d’utiliser la valeur Elasticsearch par défaut.", + "data.advancedSettings.courier.maxRequestsTitle": "Requêtes de partitions simultanées max.", + "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.requestPreferenceTitle": "Préférence de requête", + "data.advancedSettings.defaultIndexText": "L’index utilisé en l’absence de spécification.", + "data.advancedSettings.defaultIndexTitle": "Index par défaut", + "data.advancedSettings.docTableHighlightText": "Cela permet de mettre les résultats en surbrillance dans le tableau de bord Discover ainsi que dans les recherches enregistrées. À noter que la mise en surbrillance ralentit les requêtes dans le cas de documents volumineux.", + "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.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", + "data.advancedSettings.metaFieldsText": "Champs qui existent en dehors de _source pour fusionner avec le document lors de l'affichage.", + "data.advancedSettings.metaFieldsTitle": "Champs méta", + "data.advancedSettings.pinFiltersText": "Détermine si les filtres doivent avoir un certain état global (être épinglés) par défaut.", + "data.advancedSettings.pinFiltersTitle": "Épingler les filtres par défaut", + "data.advancedSettings.query.allowWildcardsText": "Lorsque ce paramètre est activé, le caractère \"*\" est autorisé en tant que premier caractère dans une clause de requête. Ne s'applique actuellement que lorsque les fonctionnalités de requête expérimentales sont activées dans la barre de requête. Pour ne plus autoriser l’utilisation de caractères génériques au début des requêtes Lucene de base, utilisez {queryStringOptionsPattern}.", + "data.advancedSettings.query.allowWildcardsTitle": "Autoriser les caractères génériques au début des requêtes", + "data.advancedSettings.query.queryStringOptions.optionsLinkText": "Options", + "data.advancedSettings.query.queryStringOptionsText": "{optionsLink} pour l'analyseur de chaînes de requête Lucene. Uniquement utilisé lorsque \"{queryLanguage}\" est défini sur {luceneLanguage}.", + "data.advancedSettings.query.queryStringOptionsTitle": "Options de chaîne de requête", + "data.advancedSettings.searchQueryLanguageKql": "KQL", + "data.advancedSettings.searchQueryLanguageLucene": "Lucene", + "data.advancedSettings.searchQueryLanguageText": "Le langage de requête utilisé par la barre de requête. KQL est un nouveau langage spécialement conçu pour Kibana.", + "data.advancedSettings.searchQueryLanguageTitle": "Langage de requête", + "data.advancedSettings.searchTimeout": "Délai d'expiration de la recherche", + "data.advancedSettings.searchTimeoutDesc": "Permet de définir le délai d'expiration maximal pour une session de recherche. La valeur 0 permet de désactiver le délai d’expiration afin que les requêtes soient exécutées jusqu'au bout.", + "data.advancedSettings.sortOptions.optionsLinkText": "Options", + "data.advancedSettings.sortOptionsText": "{optionsLink} pour le paramètre de tri Elasticsearch", + "data.advancedSettings.sortOptionsTitle": "Options de tri", + "data.advancedSettings.suggestFilterValuesText": "Définir cette propriété sur \"faux\" permet d’empêcher l'éditeur de filtres de suggérer des valeurs pour les champs.", + "data.advancedSettings.suggestFilterValuesTitle": "Suggestions de l'éditeur de filtres", + "data.advancedSettings.timepicker.last15Minutes": "Dernières 15 minutes", + "data.advancedSettings.timepicker.last1Hour": "Dernière heure", + "data.advancedSettings.timepicker.last1Year": "Dernière année", + "data.advancedSettings.timepicker.last24Hours": "Dernières 24 heures", + "data.advancedSettings.timepicker.last30Days": "30 derniers jours", + "data.advancedSettings.timepicker.last30Minutes": "30 dernières minutes", + "data.advancedSettings.timepicker.last7Days": "7 derniers jours", + "data.advancedSettings.timepicker.last90Days": "90 derniers jours", + "data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText": "formats acceptés", + "data.advancedSettings.timepicker.quickRangesText": "La liste des plages à afficher dans la section rapide du filtre temporel. Il s’agit d’un tableau d'objets, avec chaque objet contenant \"de\", \"à\" (voir {acceptedFormatsLink}) et \"afficher\" (le titre à afficher).", + "data.advancedSettings.timepicker.quickRangesTitle": "Plages rapides du filtre temporel", + "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "L'intervalle d'actualisation par défaut du filtre temporel. La valeur doit être spécifiée en millisecondes.", + "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "Intervalle d'actualisation du filtre temporel", + "data.advancedSettings.timepicker.thisWeek": "Cette semaine", + "data.advancedSettings.timepicker.timeDefaultsText": "L’option de filtre temporel à utiliser lorsque Kibana est démarré sans filtre", + "data.advancedSettings.timepicker.timeDefaultsTitle": "Filtre temporel par défaut", + "data.advancedSettings.timepicker.today": "Aujourd'hui", + "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} et {lt} {to}", + "data.aggTypes.buckets.ranges.rangesFormatMessageArrowRight": "{from} → {to}", + "data.errors.fetchError": "Vérifiez votre réseau et la configuration de votre proxy. Si le problème persiste, contactez votre administrateur réseau.", + "data.filter.applyFilterActionTitle": "Appliquer le filtre à la vue en cours", + "data.filter.applyFilters.popupHeader": "Sélectionner les filtres à appliquer", + "data.filter.applyFiltersPopup.cancelButtonLabel": "Annuler", + "data.filter.applyFiltersPopup.saveButtonLabel": "Appliquer", + "data.filter.filterBar.addFilterButtonLabel": "Ajouter un filtre", + "data.filter.filterBar.deleteFilterButtonLabel": "Supprimer", + "data.filter.filterBar.disabledFilterPrefix": "Désactivé", + "data.filter.filterBar.disableFilterButtonLabel": "Désactiver temporairement", + "data.filter.filterBar.editFilterButtonLabel": "Modifier le filtre", + "data.filter.filterBar.enableFilterButtonLabel": "Réactiver", + "data.filter.filterBar.excludeFilterButtonLabel": "Exclure les résultats", + "data.filter.filterBar.fieldNotFound": "Champ {key} introuvable dans le modèle d'indexation {indexPattern}", + "data.filter.filterBar.filterItemBadgeAriaLabel": "Actions de filtrage", + "data.filter.filterBar.filterItemBadgeIconAriaLabel": "Supprimer {filter}", + "data.filter.filterBar.includeFilterButtonLabel": "Inclure les résultats", + "data.filter.filterBar.indexPatternSelectPlaceholder": "Sélectionner un modèle d'indexation", + "data.filter.filterBar.labelErrorInfo": "Modèle d'indexation {indexPattern} introuvable", + "data.filter.filterBar.labelErrorText": "Erreur", + "data.filter.filterBar.labelWarningInfo": "Le champ {fieldName} n'existe pas dans la vue en cours.", + "data.filter.filterBar.labelWarningText": "Avertissement", + "data.filter.filterBar.moreFilterActionsMessage": "Filtre : {innerText}. Sélectionner pour plus d’actions de filtrage.", + "data.filter.filterBar.negatedFilterPrefix": "NON ", + "data.filter.filterBar.pinFilterButtonLabel": "Épingler dans toutes les applications", + "data.filter.filterBar.pinnedFilterPrefix": "Épinglé", + "data.filter.filterBar.unpinFilterButtonLabel": "Désépingler", + "data.filter.filterEditor.cancelButtonLabel": "Annuler", + "data.filter.filterEditor.createCustomLabelInputLabel": "Étiquette personnalisée", + "data.filter.filterEditor.createCustomLabelSwitchLabel": "Créer une étiquette personnalisée ?", + "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "n'existe pas", + "data.filter.filterEditor.editFilterPopupTitle": "Modifier le filtre", + "data.filter.filterEditor.editFilterValuesButtonLabel": "Modifier les valeurs du filtre", + "data.filter.filterEditor.editQueryDslButtonLabel": "Modifier en tant que Query DSL", + "data.filter.filterEditor.existsOperatorOptionLabel": "existe", + "data.filter.filterEditor.falseOptionLabel": "false", + "data.filter.filterEditor.fieldSelectLabel": "Champ", + "data.filter.filterEditor.fieldSelectPlaceholder": "Sélectionner d'abord un champ", + "data.filter.filterEditor.indexPatternSelectLabel": "Modèle d'indexation", + "data.filter.filterEditor.isBetweenOperatorOptionLabel": "est entre", + "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "n'est pas entre", + "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "n'est pas l'une des options suivantes", + "data.filter.filterEditor.isNotOperatorOptionLabel": "n'est pas", + "data.filter.filterEditor.isOneOfOperatorOptionLabel": "est l'une des options suivantes", + "data.filter.filterEditor.isOperatorOptionLabel": "est", + "data.filter.filterEditor.operatorSelectLabel": "Opérateur", + "data.filter.filterEditor.operatorSelectPlaceholderSelect": "Sélectionner", + "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "En attente", + "data.filter.filterEditor.queryDslLabel": "Query DSL d'Elasticsearch", + "data.filter.filterEditor.rangeEndInputPlaceholder": "Fin de la plage", + "data.filter.filterEditor.rangeInputLabel": "Plage", + "data.filter.filterEditor.rangeStartInputPlaceholder": "Début de la plage", + "data.filter.filterEditor.saveButtonLabel": "Enregistrer", + "data.filter.filterEditor.trueOptionLabel": "vrai", + "data.filter.filterEditor.valueInputLabel": "Valeur", + "data.filter.filterEditor.valueInputPlaceholder": "Saisir une valeur", + "data.filter.filterEditor.valueSelectPlaceholder": "Sélectionner une valeur", + "data.filter.filterEditor.valuesSelectLabel": "Valeurs", + "data.filter.filterEditor.valuesSelectPlaceholder": "Sélectionner des valeurs", + "data.filter.options.changeAllFiltersButtonLabel": "Changer tous les filtres", + "data.filter.options.deleteAllFiltersButtonLabel": "Tout supprimer", + "data.filter.options.disableAllFiltersButtonLabel": "Tout désactiver", + "data.filter.options.enableAllFiltersButtonLabel": "Tout activer", + "data.filter.options.invertDisabledFiltersButtonLabel": "Inverser l’activation/désactivation", + "data.filter.options.invertNegatedFiltersButtonLabel": "Inverser l'inclusion", + "data.filter.options.pinAllFiltersButtonLabel": "Tout épingler", + "data.filter.options.unpinAllFiltersButtonLabel": "Tout désépingler", + "data.filter.searchBar.changeAllFiltersTitle": "Changer tous les filtres", + "data.functions.esaggs.help": "Exécuter l'agrégation AggConfig", + "data.functions.esaggs.inspector.dataRequest.description": "Cette requête interroge Elasticsearch pour récupérer les données pour la visualisation.", + "data.functions.esaggs.inspector.dataRequest.title": "Données", + "data.inspector.table..dataDescriptionTooltip": "Afficher les données derrière la visualisation", + "data.inspector.table.dataTitle": "Données", + "data.inspector.table.downloadCSVToggleButtonLabel": "Télécharger CSV", + "data.inspector.table.downloadOptionsUnsavedFilename": "non enregistré", + "data.inspector.table.exportButtonFormulasWarning": "Votre fichier CSV contient des caractères que les applications de feuilles de calcul pourraient considérer comme des formules.", + "data.inspector.table.filterForValueButtonAriaLabel": "Filtrer sur la valeur", + "data.inspector.table.filterForValueButtonTooltip": "Filtrer sur la valeur", + "data.inspector.table.filterOutValueButtonAriaLabel": "Exclure la valeur", + "data.inspector.table.filterOutValueButtonTooltip": "Exclure la valeur", + "data.inspector.table.formattedCSVButtonLabel": "CSV formaté", + "data.inspector.table.formattedCSVButtonTooltip": "Télécharger les données sous forme de tableau", + "data.inspector.table.noDataAvailableDescription": "L'élément n'a fourni aucune donnée.", + "data.inspector.table.noDataAvailableTitle": "Aucune donnée disponible", + "data.inspector.table.rawCSVButtonLabel": "CSV brut", + "data.inspector.table.rawCSVButtonTooltip": "Télécharger les données telles que fournies, par exemple, les dates sous forme d'horodatages", + "data.inspector.table.tableLabel": "Tableau {index}", + "data.inspector.table.tablesDescription": "Il y a {tablesCount, plural, one {# tableau} other {# tableaux} } au total.", + "data.inspector.table.tableSelectorLabel": "Sélectionné :", + "data.kueryAutocomplete.andOperatorDescription": "Nécessite que {bothArguments} soient ''vrai''.", + "data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "les deux arguments", + "data.kueryAutocomplete.equalOperatorDescription": "{equals} une certaine valeur", + "data.kueryAutocomplete.equalOperatorDescription.equalsText": "égale", + "data.kueryAutocomplete.existOperatorDescription": "{exists} sous un certain format", + "data.kueryAutocomplete.existOperatorDescription.existsText": "existe", + "data.kueryAutocomplete.filterResultsDescription": "Filtrer les résultats contenant {fieldName}", + "data.kueryAutocomplete.greaterThanOperatorDescription": "est {greaterThan} une certaine valeur", + "data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "supérieur à", + "data.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "est {greaterThanOrEqualTo} une certaine valeur", + "data.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "supérieur ou égal à", + "data.kueryAutocomplete.lessThanOperatorDescription": "est {lessThan} une certaine valeur", + "data.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "inférieur à", + "data.kueryAutocomplete.lessThanOrEqualOperatorDescription": "est {lessThanOrEqualTo} une certaine valeur", + "data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "inférieur ou égal à", + "data.kueryAutocomplete.orOperatorDescription": "Nécessite qu’{oneOrMoreArguments} soit ''vrai''.", + "data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "au moins un argument", + "data.noDataPopover.content": "Cette plage temporelle ne contient pas de données. Étendez ou ajustez la plage temporelle pour obtenir plus de champs et pouvoir créer des graphiques.", + "data.noDataPopover.dismissAction": "Ne plus afficher", + "data.noDataPopover.subtitle": "Conseil", + "data.noDataPopover.title": "Ensemble de données vide", + "data.painlessError.buttonTxt": "Modifier le script", + "data.painlessError.painlessScriptedFieldErrorMessage": "Erreur d'exécution du champ d'exécution ou du champ scripté sur le modèle d'indexation {indexPatternName}", + "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "Intervalle de calendrier non valide : {interval} ; la valeur doit être 1.", + "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "Format d'intervalle non valide : {interval}", + "data.query.queryBar.clearInputLabel": "Effacer l'entrée", + "data.query.queryBar.comboboxAriaLabel": "Rechercher et filtrer la page {pageType}", + "data.query.queryBar.kqlFullLanguageName": "Langage de requête Kibana", + "data.query.queryBar.kqlLanguageName": "KQL", + "data.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "documents", + "data.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "Ne plus afficher", + "data.query.queryBar.KQLNestedQuerySyntaxInfoText": "Il semblerait que votre requête porte sur un champ imbriqué. Selon le résultat visé, il existe plusieurs façons de construire une syntaxe KQL pour des requêtes imbriquées. Apprenez-en plus avec notre {link}.", + "data.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "Syntaxe de requête imbriquée KQL", + "data.query.queryBar.kqlOffLabel": "Off", + "data.query.queryBar.kqlOnLabel": "On", + "data.query.queryBar.languageSwitcher.toText": "Passer au langage de requête Kibana pour la recherche", + "data.query.queryBar.luceneLanguageName": "Lucene", + "data.query.queryBar.searchInputAriaLabel": "Commencer à taper pour rechercher et filtrer la page {pageType}", + "data.query.queryBar.searchInputPlaceholder": "Recherche", + "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) offre une syntaxe de requête simplifiée et la prise en charge des champs scriptés. KQL offre également une fonctionnalité de saisie semi-automatique. Si vous désactivez KQL, {nonKqlModeHelpText}.", + "data.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText": "Kibana utilise Lucene.", + "data.query.queryBar.syntaxOptionsTitle": "Options de syntaxe", + "data.search.aggs.aggGroups.bucketsText": "Compartiments", + "data.search.aggs.aggGroups.metricsText": "Indicateurs", + "data.search.aggs.aggGroups.noneText": "Aucune", + "data.search.aggs.aggTypesLabel": "plages {fieldName}", + "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.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateHistogram.format.help": "Format à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateHistogram.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.dateHistogram.interval.help": "Intervalle à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateHistogram.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.dateHistogram.minDocCount.help": "Nombre minimal de documents à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateHistogram.scaleMetricValues.help": "Spécifie l'utilisation ou non de scaleMetricValues pour cette agrégation.", + "data.search.aggs.buckets.dateHistogram.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateHistogram.timeRange.help": "Plage temporelle à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateHistogram.timeZone.help": "Fuseau horaire à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateHistogram.useNormalizedEsInterval.help": "Spécifie l'utilisation ou non de useNormalizedEsInterval pour cette agrégation.", + "data.search.aggs.buckets.dateHistogramLabel": "{fieldName} par {intervalDescription}", + "data.search.aggs.buckets.dateHistogramTitle": "Histogramme de date", + "data.search.aggs.buckets.dateRange.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.dateRange.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.dateRange.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateRange.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.dateRange.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.dateRange.ranges.help": "Plages à utiliser pour cette agrégation.", + "data.search.aggs.buckets.dateRange.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.dateRange.timeZone.help": "Fuseau horaire à utiliser pour cette agrégation.", + "data.search.aggs.buckets.dateRangeTitle": "Plage de dates", + "data.search.aggs.buckets.filter.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.filter.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.filter.filter.help": "Pour filtrer les résultats en fonction d’une requête KQL ou Lucene. Ne pas utiliser en association avec geo_bounding_box.", + "data.search.aggs.buckets.filter.geoBoundingBox.help": "Pour filtrer les résultats en fonction d’une localisation au sein d’une zone de délimitation", + "data.search.aggs.buckets.filter.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.filter.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.filter.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.filters.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.filters.filters.help": "Filtres à utiliser pour cette agrégation", + "data.search.aggs.buckets.filters.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.filters.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.filters.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.filtersTitle": "Filtres", + "data.search.aggs.buckets.filterTitle": "Filtre", + "data.search.aggs.buckets.geoHash.autoPrecision.help": "Spécifie l'utilisation ou non de la précision automatique pour cette agrégation.", + "data.search.aggs.buckets.geoHash.boundingBox.help": "Pour filtrer les résultats en fonction d’une localisation au sein d’une zone de délimitation", + "data.search.aggs.buckets.geoHash.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.geoHash.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.geoHash.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.buckets.geoHash.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.geoHash.isFilteredByCollar.help": "Spécifie le filtrage ou non par collier.", + "data.search.aggs.buckets.geoHash.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.geoHash.precision.help": "Précision à utiliser pour cette agrégation.", + "data.search.aggs.buckets.geoHash.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.geoHash.useGeocentroid.help": "Spécifie l'utilisation ou non d’un centroïde géométrique pour cette agrégation.", + "data.search.aggs.buckets.geohashGridTitle": "Geohash", + "data.search.aggs.buckets.geoTile.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.geoTile.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.geoTile.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.buckets.geoTile.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.geoTile.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.geoTile.precision.help": "Précision à utiliser pour cette agrégation.", + "data.search.aggs.buckets.geoTile.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.geoTile.useGeocentroid.help": "Spécifie l'utilisation ou non d’un centroïde géométrique pour cette agrégation.", + "data.search.aggs.buckets.geotileGridTitle": "Geotile", + "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.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", + "data.search.aggs.buckets.histogram.interval.help": "Intervalle à utiliser pour cette agrégation", + "data.search.aggs.buckets.histogram.intervalBase.help": "Intervalle de base à utiliser pour cette agrégation", + "data.search.aggs.buckets.histogram.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.histogram.maxBars.help": "Calcule l'intervalle pour obtenir approximativement le nombre de barres spécifié.", + "data.search.aggs.buckets.histogram.minDocCount.help": "Spécifie l'utilisation ou non de min_doc_count pour cette agrégation.", + "data.search.aggs.buckets.histogram.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.histogramTitle": "Histogramme", + "data.search.aggs.buckets.intervalOptions.autoDisplayName": "Auto", + "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "Jour", + "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "Heure", + "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "Milliseconde", + "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "Minute", + "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "Mois", + "data.search.aggs.buckets.intervalOptions.secondDisplayName": "Seconde", + "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "Semaine", + "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "Année", + "data.search.aggs.buckets.ipRange.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.ipRange.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.ipRange.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.buckets.ipRange.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.ipRange.ipRangeType.help": "Type de plage d'IP à utiliser pour cette agrégation. Doit être l’une des valeurs suivantes : mask, fromTo.", + "data.search.aggs.buckets.ipRange.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.ipRange.ranges.help": "Plages à utiliser pour cette agrégation.", + "data.search.aggs.buckets.ipRange.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.ipRangeLabel": "Plages d'IP de {fieldName}", + "data.search.aggs.buckets.ipRangeTitle": "Plage d'IP", + "data.search.aggs.buckets.range.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.range.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.range.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.buckets.range.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.range.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.range.ranges.help": "Plages en série à utiliser pour cette agrégation.", + "data.search.aggs.buckets.range.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.rangeTitle": "Plage", + "data.search.aggs.buckets.shardDelay.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.shardDelay.delay.help": "Délai entre les partitions à traiter. Exemple : \"5s\".", + "data.search.aggs.buckets.shardDelay.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.shardDelay.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.shardDelay.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.shardDelay.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.significantTerms.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.significantTerms.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.significantTerms.exclude.help": "Valeurs de compartiment spécifiques à exclure des résultats", + "data.search.aggs.buckets.significantTerms.excludeLabel": "Exclure", + "data.search.aggs.buckets.significantTerms.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.buckets.significantTerms.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.significantTerms.include.help": "Valeurs de compartiment spécifiques à inclure dans les résultats", + "data.search.aggs.buckets.significantTerms.includeLabel": "Inclure", + "data.search.aggs.buckets.significantTerms.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.significantTerms.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.significantTerms.size.help": "Nombre maximal de compartiments à extraire", + "data.search.aggs.buckets.significantTermsLabel": "Top {size} des termes les plus inhabituels pour {fieldName}", + "data.search.aggs.buckets.significantTermsTitle": "Termes importants", + "data.search.aggs.buckets.terms.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.buckets.terms.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.buckets.terms.exclude.help": "Valeurs de compartiment spécifiques à exclure des résultats", + "data.search.aggs.buckets.terms.excludeLabel": "Exclure", + "data.search.aggs.buckets.terms.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.buckets.terms.id.help": "ID pour cette agrégation", + "data.search.aggs.buckets.terms.include.help": "Valeurs de compartiment spécifiques à inclure dans les résultats", + "data.search.aggs.buckets.terms.includeLabel": "Inclure", + "data.search.aggs.buckets.terms.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.buckets.terms.missingBucket.help": "Lorsqu'il est défini sur ''vrai'', ce paramètre regroupe tous les compartiments avec des champs manquants.", + "data.search.aggs.buckets.terms.missingBucketLabel": "Manquant", + "data.search.aggs.buckets.terms.missingBucketLabel.help": "Étiquette par défaut utilisée dans les graphiques lorsqu'il manque un champ aux documents.", + "data.search.aggs.buckets.terms.order.help": "Ordre dans lequel renvoyer les résultats : croissant ou décroissant", + "data.search.aggs.buckets.terms.orderAgg.help": "Configuration d'agrégation à utiliser pour ordonner les résultats", + "data.search.aggs.buckets.terms.orderAscendingTitle": "Croissant", + "data.search.aggs.buckets.terms.orderBy.help": "Champ selon lequel ordonner les résultats", + "data.search.aggs.buckets.terms.orderDescendingTitle": "Décroissant", + "data.search.aggs.buckets.terms.otherBucket.help": "Lorsqu'il est défini sur ''vrai'', ce paramètre regroupe tous les compartiments au-delà de la taille autorisée.", + "data.search.aggs.buckets.terms.otherBucketDescription": "Cette requête comptabilise le nombre de documents qui ne répondent pas au critère des compartiments de données.", + "data.search.aggs.buckets.terms.otherBucketLabel": "Autre", + "data.search.aggs.buckets.terms.otherBucketLabel.help": "Étiquette par défaut utilisée dans les graphiques pour les documents du compartiment Autre", + "data.search.aggs.buckets.terms.otherBucketTitle": "Compartiment Autre", + "data.search.aggs.buckets.terms.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.buckets.terms.size.help": "Nombre maximal de compartiments à extraire", + "data.search.aggs.buckets.termsTitle": "Termes", + "data.search.aggs.error.aggNotFound": "Aucun type d'agrégation enregistré pour \"{type}\".", + "data.search.aggs.function.buckets.dateHistogram.help": "Génère une configuration d'agrégation en série pour une agrégation Histogramme.", + "data.search.aggs.function.buckets.dateRange.help": "Génère une configuration d'agrégation en série pour une agrégation Plage de dates.", + "data.search.aggs.function.buckets.filter.help": "Génère une configuration d'agrégation en série pour une agrégation Filtre.", + "data.search.aggs.function.buckets.filters.help": "Génère une configuration d'agrégation en série pour une agrégation Filtre.", + "data.search.aggs.function.buckets.geoHash.help": "Génère une configuration d'agrégation en série pour une agrégation Geohash.", + "data.search.aggs.function.buckets.geoTile.help": "Génère une configuration d'agrégation en série pour une agrégation Geotile.", + "data.search.aggs.function.buckets.histogram.help": "Génère une configuration d'agrégation en série pour une agrégation Histogramme.", + "data.search.aggs.function.buckets.ipRange.help": "Génère une configuration d'agrégation en série pour une agrégation Plage d'IP.", + "data.search.aggs.function.buckets.range.help": "Génère une configuration d'agrégation en série pour une agrégation Plage.", + "data.search.aggs.function.buckets.shardDelay.help": "Génère une configuration d'agrégation en série pour une agrégation Délai de partition.", + "data.search.aggs.function.buckets.significantTerms.help": "Génère une configuration d'agrégation en série pour une agrégation Termes importants.", + "data.search.aggs.function.buckets.terms.help": "Génère une configuration d'agrégation en série pour une agrégation Termes.", + "data.search.aggs.function.metrics.avg.help": "Génère une configuration d'agrégation en série pour une agrégation Moyenne.", + "data.search.aggs.function.metrics.bucket_avg.help": "Génère une configuration d'agrégation en série pour une agrégation Moyenne compartiment.", + "data.search.aggs.function.metrics.bucket_max.help": "Génère une configuration d'agrégation en série pour une agrégation Max. compartiment.", + "data.search.aggs.function.metrics.bucket_min.help": "Génère une configuration d'agrégation en série pour une agrégation Min. compartiment.", + "data.search.aggs.function.metrics.bucket_sum.help": "Génère une configuration d'agrégation en série pour une agrégation Somme compartiment.", + "data.search.aggs.function.metrics.cardinality.help": "Génère une configuration d'agrégation en série pour une agrégation Cardinalité.", + "data.search.aggs.function.metrics.count.help": "Génère une configuration d'agrégation en série pour une agrégation Décompte.", + "data.search.aggs.function.metrics.cumulative_sum.help": "Génère une configuration d'agrégation en série pour une agrégation Somme cumulée.", + "data.search.aggs.function.metrics.derivative.help": "Génère une configuration d'agrégation en série pour une agrégation Dérivée.", + "data.search.aggs.function.metrics.filtered_metric.help": "Génère une configuration d'agrégation en série pour une agrégation Indicateur filtré.", + "data.search.aggs.function.metrics.geo_bounds.help": "Génère une configuration d'agrégation en série pour une agrégation Délimitation géométrique.", + "data.search.aggs.function.metrics.geo_centroid.help": "Génère une configuration d'agrégation en série pour une agrégation Centroïde géométrique.", + "data.search.aggs.function.metrics.max.help": "Génère une configuration d'agrégation en série pour une agrégation Max.", + "data.search.aggs.function.metrics.median.help": "Génère une configuration d'agrégation en série pour une agrégation Médiane.", + "data.search.aggs.function.metrics.min.help": "Génère une configuration d'agrégation en série pour une agrégation Min.", + "data.search.aggs.function.metrics.moving_avg.help": "Génère une configuration d'agrégation en série pour une agrégation Moyenne mobile.", + "data.search.aggs.function.metrics.percentile_ranks.help": "Génère une configuration d'agrégation en série pour une agrégation Rangs centiles.", + "data.search.aggs.function.metrics.percentiles.help": "Génère une configuration d'agrégation en série pour une agrégation Centiles.", + "data.search.aggs.function.metrics.serial_diff.help": "Génère une configuration d'agrégation en série pour une agrégation Différenciation en série.", + "data.search.aggs.function.metrics.singlePercentile.help": "Génère une configuration d'agrégation en série pour une agrégation Centile unique.", + "data.search.aggs.function.metrics.std_deviation.help": "Génère une configuration d'agrégation en série pour une agrégation Écart-type.", + "data.search.aggs.function.metrics.sum.help": "Génère une configuration d'agrégation en série pour une agrégation Somme.", + "data.search.aggs.function.metrics.top_hit.help": "Génère une configuration d'agrégation en série pour une agrégation Meilleur résultat.", + "data.search.aggs.histogram.missingMaxMinValuesWarning": "Impossible d’extraire les valeurs max. et min. pour scaler automatiquement les compartiments de l'histogramme. Cela peut entraîner des performances de visualisation médiocres.", + "data.search.aggs.metrics.averageBucketTitle": "Moyenne compartiment", + "data.search.aggs.metrics.averageLabel": "Moyenne {field}", + "data.search.aggs.metrics.averageTitle": "Moyenne", + "data.search.aggs.metrics.avg.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.avg.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.avg.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.avg.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.avg.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.avg.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.bucket_avg.customBucket.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.bucket_avg.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.bucket_avg.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.bucket_avg.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.bucket_avg.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.bucket_avg.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.bucket_avg.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.bucket_max.customBucket.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.bucket_max.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.bucket_max.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.bucket_max.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.bucket_max.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.bucket_max.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.bucket_max.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.bucket_min.customBucket.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.bucket_min.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.bucket_min.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.bucket_min.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.bucket_min.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.bucket_min.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.bucket_min.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.bucket_sum.customBucket.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.bucket_sum.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.bucket_sum.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.bucket_sum.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.bucket_sum.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.bucket_sum.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.bucket_sum.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.cardinality.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.cardinality.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.cardinality.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.cardinality.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.cardinality.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.cardinality.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.count.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.count.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.count.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.count.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.countLabel": "Décompte", + "data.search.aggs.metrics.countTitle": "Décompte", + "data.search.aggs.metrics.cumulative_sum.buckets_path.help": "Chemin d’accès à l'indicateur d’intérêt", + "data.search.aggs.metrics.cumulative_sum.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.cumulative_sum.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.cumulative_sum.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.cumulative_sum.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.cumulative_sum.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.cumulative_sum.metricAgg.help": "ID correspondant à la configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.cumulative_sum.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.cumulativeSumLabel": "somme cumulée", + "data.search.aggs.metrics.cumulativeSumTitle": "Somme cumulée", + "data.search.aggs.metrics.derivative.buckets_path.help": "Chemin d’accès à l'indicateur d’intérêt", + "data.search.aggs.metrics.derivative.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.derivative.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.derivative.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.derivative.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.derivative.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.derivative.metricAgg.help": "ID correspondant à la configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.derivative.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.derivativeLabel": "dérivée", + "data.search.aggs.metrics.derivativeTitle": "Dérivée", + "data.search.aggs.metrics.filtered_metric.customBucket.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants. Doit être une agrégation de filtres.", + "data.search.aggs.metrics.filtered_metric.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.filtered_metric.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines enfants", + "data.search.aggs.metrics.filtered_metric.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.filtered_metric.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.filtered_metric.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.filteredMetricLabel": "filtré", + "data.search.aggs.metrics.filteredMetricTitle": "Indicateur filtré", + "data.search.aggs.metrics.geo_bounds.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.geo_bounds.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.geo_bounds.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.geo_bounds.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.geo_bounds.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.geo_bounds.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.geo_centroid.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.geo_centroid.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.geo_centroid.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.geo_centroid.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.geo_centroid.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.geo_centroid.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.geoBoundsLabel": "Délimitation géométrique", + "data.search.aggs.metrics.geoBoundsTitle": "Délimitation géométrique", + "data.search.aggs.metrics.geoCentroidLabel": "Centroïde géométrique", + "data.search.aggs.metrics.geoCentroidTitle": "Centroïde géométrique", + "data.search.aggs.metrics.max.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.max.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.max.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.max.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.max.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.max.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.maxBucketTitle": "Max. compartiment", + "data.search.aggs.metrics.maxLabel": "Max. {field}", + "data.search.aggs.metrics.maxTitle": "Max.", + "data.search.aggs.metrics.median.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.median.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.median.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.median.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.median.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.median.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.medianLabel": "Médiane {field}", + "data.search.aggs.metrics.medianTitle": "Médiane", + "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "Agrégations d'indicateurs", + "data.search.aggs.metrics.min.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.min.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.min.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.min.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.min.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.min.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.minBucketTitle": "Min. compartiment", + "data.search.aggs.metrics.minLabel": "Min. {field}", + "data.search.aggs.metrics.minTitle": "Min.", + "data.search.aggs.metrics.moving_avg.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.moving_avg.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.moving_avg.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.moving_avg.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.moving_avg.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.moving_avg.metricAgg.help": "ID correspondant à la configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.moving_avg.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.moving_avg.script.help": "ID correspondant à la configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.moving_avg.window.help": "La taille de la fenêtre à \"faire glisser\" le long de l'histogramme.", + "data.search.aggs.metrics.movingAvgLabel": "moyenne mobile", + "data.search.aggs.metrics.movingAvgTitle": "Moyenne mobile", + "data.search.aggs.metrics.overallAverageLabel": "moyenne générale", + "data.search.aggs.metrics.overallMaxLabel": "max. général", + "data.search.aggs.metrics.overallMinLabel": "min. général", + "data.search.aggs.metrics.overallSumLabel": "somme générale", + "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "Agrégations de pipelines parents", + "data.search.aggs.metrics.percentile_ranks.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.percentile_ranks.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.percentile_ranks.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.percentile_ranks.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.percentile_ranks.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.percentile_ranks.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.percentile_ranks.values.help": "Plage de rangs centiles", + "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "Rang centile {format} de \"{label}\"", + "data.search.aggs.metrics.percentileRanksLabel": "Rangs centiles de {field}", + "data.search.aggs.metrics.percentileRanksTitle": "Rangs centiles", + "data.search.aggs.metrics.percentiles.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.percentiles.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.percentiles.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.percentiles.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.percentiles.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.percentiles.percents.help": "Plage de rangs centiles", + "data.search.aggs.metrics.percentiles.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.percentiles.valuePropsLabel": "{percentile} centile de {label}", + "data.search.aggs.metrics.percentilesLabel": "Centiles de {field}", + "data.search.aggs.metrics.percentilesTitle": "Centiles", + "data.search.aggs.metrics.serial_diff.buckets_path.help": "Chemin d’accès à l'indicateur d’intérêt", + "data.search.aggs.metrics.serial_diff.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.serial_diff.customMetric.help": "Configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.serial_diff.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.serial_diff.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.serial_diff.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.serial_diff.metricAgg.help": "ID correspondant à la configuration d'agrégation à utiliser pour la conception d'agrégations de pipelines parents", + "data.search.aggs.metrics.serial_diff.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.serialDiffLabel": "différenciation en série", + "data.search.aggs.metrics.serialDiffTitle": "Différenciation en série", + "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "Agrégations de pipelines enfants", + "data.search.aggs.metrics.singlePercentile.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.singlePercentile.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.singlePercentile.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.singlePercentile.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.singlePercentile.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.singlePercentile.percentile.help": "Centile à récupérer", + "data.search.aggs.metrics.singlePercentile.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.singlePercentileLabel": "Centile {field}", + "data.search.aggs.metrics.singlePercentileTitle": "Centile", + "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "Écart-type de {fieldDisplayName}", + "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "{label} inférieur", + "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "{label} supérieur", + "data.search.aggs.metrics.standardDeviationLabel": "Écart-type de {field}", + "data.search.aggs.metrics.standardDeviationTitle": "Écart-type", + "data.search.aggs.metrics.std_deviation.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.std_deviation.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.std_deviation.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.std_deviation.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.std_deviation.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.std_deviation.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.sum.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.sum.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.sum.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.sum.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.sum.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.sum.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.sumBucketTitle": "Somme compartiment", + "data.search.aggs.metrics.sumLabel": "Somme de {field}", + "data.search.aggs.metrics.sumTitle": "Somme", + "data.search.aggs.metrics.timeShift.help": "Décalez la plage temporelle de l'indicateur d'une durée définie, par exemple 1 h ou 7 j. \"précédent\" utilisera la plage temporelle la plus proche du filtre d'histogramme de date ou de plage temporelle.", + "data.search.aggs.metrics.top_hit.aggregate.help": "Agréger le type", + "data.search.aggs.metrics.top_hit.customLabel.help": "Représente une étiquette personnalisée pour cette agrégation", + "data.search.aggs.metrics.top_hit.enabled.help": "Spécifie si cette agrégation doit être activée.", + "data.search.aggs.metrics.top_hit.field.help": "Champ à utiliser pour cette agrégation", + "data.search.aggs.metrics.top_hit.id.help": "ID pour cette agrégation", + "data.search.aggs.metrics.top_hit.json.help": "Json avancé à inclure lorsque l'agrégation est envoyée vers Elasticsearch", + "data.search.aggs.metrics.top_hit.schema.help": "Schéma à utiliser pour cette agrégation", + "data.search.aggs.metrics.top_hit.size.help": "Nombre maximal de compartiments à extraire", + "data.search.aggs.metrics.top_hit.sortField.help": "Champ selon lequel ordonner les résultats", + "data.search.aggs.metrics.top_hit.sortOrder.help": "Ordre dans lequel renvoyer les résultats : croissant ou décroissant", + "data.search.aggs.metrics.topHit.ascendingLabel": "Croissant", + "data.search.aggs.metrics.topHit.averageLabel": "Moyenne", + "data.search.aggs.metrics.topHit.concatenateLabel": "Concaténer", + "data.search.aggs.metrics.topHit.descendingLabel": "Décroissant", + "data.search.aggs.metrics.topHit.firstPrefixLabel": "Premier", + "data.search.aggs.metrics.topHit.lastPrefixLabel": "Dernier", + "data.search.aggs.metrics.topHit.maxLabel": "Max.", + "data.search.aggs.metrics.topHit.minLabel": "Min.", + "data.search.aggs.metrics.topHit.sumLabel": "Somme", + "data.search.aggs.metrics.topHitTitle": "Meilleur résultat", + "data.search.aggs.metrics.uniqueCountLabel": "Décompte unique de {field}", + "data.search.aggs.metrics.uniqueCountTitle": "Décompte unique", + "data.search.aggs.otherBucket.labelForMissingValuesLabel": "Étiquette pour des valeurs manquantes", + "data.search.aggs.otherBucket.labelForOtherBucketLabel": "Étiquette pour le compartiment Autre", + "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "Le champ enregistré \"{fieldParameter}\" du modèle d'indexation \"{indexPatternTitle}\" n'est pas valide pour une utilisation avec l'agrégation \"{aggType}\". Veuillez sélectionner un nouveau champ.", + "data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage": "Le champ \"{fieldParameter}\" associé à cet objet n'existe plus dans le modèle d'indexation. Veuillez utiliser un autre champ.", + "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} est un paramètre requis.", + "data.search.aggs.percentageOfLabel": "Pourcentage de {label}", + "data.search.aggs.string.customLabel": "Étiquette personnalisée", + "data.search.dataRequest.title": "Données", + "data.search.es_search.dataRequest.description": "Cette requête interroge Elasticsearch pour récupérer les données pour la visualisation.", + "data.search.es_search.hitsDescription": "Le nombre de documents renvoyés par la requête.", + "data.search.es_search.hitsLabel": "Résultats", + "data.search.es_search.hitsTotalDescription": "Le nombre de documents correspondant à la requête.", + "data.search.es_search.hitsTotalLabel": "Résultats (total)", + "data.search.es_search.indexPatternDescription": "Le modèle d'indexation qui se connecte aux index Elasticsearch.", + "data.search.es_search.queryTimeDescription": "Le temps qu'il a fallu pour traiter la requête. Ne comprend pas le temps nécessaire pour envoyer la requête ni l'analyser dans le navigateur.", + "data.search.es_search.queryTimeLabel": "Durée de la requête", + "data.search.es_search.queryTimeValue": "{queryTime}ms", + "data.search.esaggs.error.kibanaRequest": "Une requête Kibana est nécessaire pour exécuter cette recherche sur le serveur. Veuillez fournir un objet de requête pour les paramètres d'exécution de l'expression.", + "data.search.esdsl.help": "Exécuter une requête Elasticsearch", + "data.search.esdsl.index.help": "Index Elasticsearch à interroger", + "data.search.esdsl.q.help": "Requête DSL", + "data.search.esdsl.size.help": "Paramètre de taille de l’API de recherche d’Elasticsearch", + "data.search.esErrorTitle": "Impossible d’extraire les résultats de recherche", + "data.search.functions.cidr.cidr.help": "Spécifier le bloc CIDR", + "data.search.functions.cidr.help": "Créer une plage CIDR", + "data.search.functions.dateRange.from.help": "Spécifier la date de début", + "data.search.functions.dateRange.help": "Créer une plage de dates", + "data.search.functions.dateRange.to.help": "Spécifier la date de fin", + "data.search.functions.esaggs.aggConfigs.help": "Liste des agrégations configurées avec des fonctions agg_type", + "data.search.functions.esaggs.index.help": "Modèle d'indexation extrait avec indexPatternLoad", + "data.search.functions.esaggs.metricsAtAllLevels.help": "Spécifie l’inclusion ou non des colonnes avec indicateurs pour chaque niveau de compartiment.", + "data.search.functions.esaggs.partialRows.help": "Détermine s'il faut renvoyer ou non les lignes ne contenant que des données partielles.", + "data.search.functions.esaggs.timeFields.help": "Spécifiez des champs temporels afin d’obtenir les plages temporelles résolues pour la requête.", + "data.search.functions.existsFilter.field.help": "Spécifiez le champ que vous souhaitez filtrer. Utilisez la fonction ''field''.", + "data.search.functions.existsFilter.help": "Créer un filtre Kibana existant", + "data.search.functions.existsFilter.negate.help": "Si le filtre doit être inversé.", + "data.search.functions.extendedBounds.help": "Créer des limites étendues", + "data.search.functions.extendedBounds.max.help": "Spécifier la valeur de la limite supérieure", + "data.search.functions.extendedBounds.min.help": "Spécifier la valeur de la limite inférieure", + "data.search.functions.field.help": "Créer un champ Kibana", + "data.search.functions.field.name.help": "Nom du champ", + "data.search.functions.field.script.help": "Script de champ, au cas où le champ serait scripté.", + "data.search.functions.field.type.help": "Type du champ", + "data.search.functions.geoBoundingBox.arguments.error": "Au moins un des groupes de paramètres suivants doit être fourni : {parameters}.", + "data.search.functions.geoBoundingBox.bottom_left.help": "Spécifier l’angle inférieur gauche", + "data.search.functions.geoBoundingBox.bottom_right.help": "Spécifier l’angle inférieur droit", + "data.search.functions.geoBoundingBox.bottom.help": "Spécifier la coordonnée inférieure", + "data.search.functions.geoBoundingBox.help": "Créer une zone de délimitation géométrique", + "data.search.functions.geoBoundingBox.left.help": "Spécifier la coordonnée gauche", + "data.search.functions.geoBoundingBox.right.help": "Spécifier la coordonnée droite", + "data.search.functions.geoBoundingBox.top_left.help": "Spécifier l’angle supérieur gauche", + "data.search.functions.geoBoundingBox.top_right.help": "Spécifier l’angle supérieur droit", + "data.search.functions.geoBoundingBox.top.help": "Spécifier la coordonnée supérieure", + "data.search.functions.geoBoundingBox.wkt.help": "Spécifier le texte bien connu (WKT)", + "data.search.functions.geoPoint.arguments.error": "Les paramètres \"lat\" et \"lon\" ou \"point\" doivent être spécifiés.", + "data.search.functions.geoPoint.help": "Créer un point géographique", + "data.search.functions.geoPoint.lat.help": "Spécifier la latitude", + "data.search.functions.geoPoint.lon.help": "Spécifier la longitude", + "data.search.functions.geoPoint.point.error": "Le paramètre du point doit être une chaîne ou deux valeurs numériques.", + "data.search.functions.geoPoint.point.help": "Spécifiez le point sous la forme d’une chaîne de coordonnées séparées par des virgules ou sous la forme de deux valeurs numériques.", + "data.search.functions.ipRange.from.help": "Spécifier l'adresse de début", + "data.search.functions.ipRange.help": "Créer une plage d'IP", + "data.search.functions.ipRange.to.help": "Spécifier l'adresse de fin", + "data.search.functions.kibana_context.filters.help": "Spécifier des filtres génériques Kibana", + "data.search.functions.kibana_context.help": "Met à jour le contexte général de Kibana.", + "data.search.functions.kibana_context.q.help": "Spécifier une recherche en texte libre Kibana", + "data.search.functions.kibana_context.savedSearchId.help": "Spécifier l'ID de recherche enregistrée à utiliser pour les requêtes et les filtres", + "data.search.functions.kibana_context.timeRange.help": "Spécifier le filtre de plage temporelle Kibana", + "data.search.functions.kibana.help": "Permet d’obtenir le contexte général de Kibana.", + "data.search.functions.kibanaFilter.disabled.help": "Si le filtre doit être désactivé", + "data.search.functions.kibanaFilter.field.help": "Spécifier une recherche en texte libre esdsl", + "data.search.functions.kibanaFilter.help": "Créer un filtre Kibana", + "data.search.functions.kibanaFilter.negate.help": "Si le filtre doit être inversé", + "data.search.functions.kql.help": "Créer une requête KQL Kibana", + "data.search.functions.kql.q.help": "Spécifier une recherche en texte libre KQL Kibana", + "data.search.functions.lucene.help": "Créer une requête Lucene Kibana", + "data.search.functions.lucene.q.help": "Spécifier une recherche en texte libre Lucene", + "data.search.functions.numericalRange.from.help": "Spécifier la valeur de début", + "data.search.functions.numericalRange.help": "Créer une plage numérique", + "data.search.functions.numericalRange.label.help": "Spécifier l'étiquette de la plage", + "data.search.functions.numericalRange.to.help": "Spécifier la valeur de fin", + "data.search.functions.phraseFilter.field.help": "Spécifiez le champ que vous souhaitez filtrer. Utilisez la fonction ''field''.", + "data.search.functions.phraseFilter.help": "Créer un filtre d’expression Kibana", + "data.search.functions.phraseFilter.negate.help": "Si le filtre doit être inversé", + "data.search.functions.phraseFilter.phrase.help": "Spécifier l'expression", + "data.search.functions.queryFilter.help": "Créer un filtre de requête", + "data.search.functions.queryFilter.input.help": "Spécifier le filtre de requête", + "data.search.functions.queryFilter.label.help": "Spécifier l'étiquette du filtre", + "data.search.functions.range.gt.help": "Supérieur à", + "data.search.functions.range.gte.help": "Supérieur ou égal à", + "data.search.functions.range.help": "Créer un filtre de plage Kibana", + "data.search.functions.range.lt.help": "Inférieur à", + "data.search.functions.range.lte.help": "Inférieur ou égal à", + "data.search.functions.rangeFilter.field.help": "Spécifiez le champ que vous souhaitez filtrer. Utilisez la fonction ''field''.", + "data.search.functions.rangeFilter.help": "Créer un filtre de plage Kibana", + "data.search.functions.rangeFilter.negate.help": "Si le filtre doit être inversé", + "data.search.functions.rangeFilter.range.help": "Spécifiez la plage à l’aide de la fonction ''range''.", + "data.search.functions.timerange.from.help": "Spécifier la date de début", + "data.search.functions.timerange.help": "Créer une plage temporelle Kibana", + "data.search.functions.timerange.mode.help": "Spécifier le mode (absolu ou relatif)", + "data.search.functions.timerange.to.help": "Spécifier la date de fin", + "data.search.httpErrorTitle": "Impossible d’extraire vos données", + "data.search.searchBar.savedQueryDescriptionLabelText": "Description", + "data.search.searchBar.savedQueryDescriptionText": "Enregistrez le texte et les filtres de la requête que vous souhaitez réutiliser.", + "data.search.searchBar.savedQueryForm.titleConflictText": "Ce nom est en conflit avec une requête enregistrée existante.", + "data.search.searchBar.savedQueryFormCancelButtonText": "Annuler", + "data.search.searchBar.savedQueryFormSaveButtonText": "Enregistrer", + "data.search.searchBar.savedQueryFormTitle": "Enregistrer la requête", + "data.search.searchBar.savedQueryIncludeFiltersLabelText": "Inclure les filtres", + "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "Inclure le filtre temporel", + "data.search.searchBar.savedQueryNameHelpText": "Un nom est requis. Le nom ne peut pas contenir d'espace vide au début ou à la fin. Le nom doit être unique.", + "data.search.searchBar.savedQueryNameLabelText": "Nom", + "data.search.searchBar.savedQueryNoSavedQueriesText": "Aucune requête enregistrée.", + "data.search.searchBar.savedQueryPopoverButtonText": "Voir les requêtes enregistrées", + "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "Effacer la requête enregistrée en cours", + "data.search.searchBar.savedQueryPopoverClearButtonText": "Effacer", + "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "Annuler", + "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "Supprimer", + "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "Supprimer \"{savedQueryName}\" ?", + "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "Supprimer la requête enregistrée {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "Enregistrer en tant que nouvelle requête enregistrée", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "Enregistrer en tant que nouvelle", + "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "Enregistrer une nouvelle requête enregistrée", + "data.search.searchBar.savedQueryPopoverSaveButtonText": "Enregistrer la requête en cours", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "Enregistrer les modifications apportées à {title}", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "Enregistrer les modifications", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "Bouton de requête enregistrée {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "Description de {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "Bouton de requête enregistrée {savedQueryName} sélectionné. Appuyez pour effacer les modifications.", + "data.search.searchBar.savedQueryPopoverTitleText": "Requêtes enregistrées", + "data.search.searchSource.fetch.requestTimedOutNotificationMessage": "Les données peuvent être incomplètes parce que votre requête est arrivée à échéance.", + "data.search.searchSource.fetch.shardsFailedModal.close": "Fermer", + "data.search.searchSource.fetch.shardsFailedModal.copyToClipboard": "Copier la réponse dans le presse-papiers", + "data.search.searchSource.fetch.shardsFailedModal.failureHeader": "{failureName} à {failureDetails}", + "data.search.searchSource.fetch.shardsFailedModal.showDetails": "Afficher les détails", + "data.search.searchSource.fetch.shardsFailedModal.tabHeaderRequest": "Requête", + "data.search.searchSource.fetch.shardsFailedModal.tabHeaderResponse": "Réponse", + "data.search.searchSource.fetch.shardsFailedModal.tabHeaderShardFailures": "Échecs de partition", + "data.search.searchSource.fetch.shardsFailedModal.tableColIndex": "Index", + "data.search.searchSource.fetch.shardsFailedModal.tableColNode": "Nœud", + "data.search.searchSource.fetch.shardsFailedModal.tableColReason": "Raison", + "data.search.searchSource.fetch.shardsFailedModal.tableColShard": "Partition", + "data.search.searchSource.fetch.shardsFailedModal.tableRowCollapse": "Réduire {rowDescription}", + "data.search.searchSource.fetch.shardsFailedModal.tableRowExpand": "Développer {rowDescription}", + "data.search.searchSource.fetch.shardsFailedNotificationDescription": "Les données que vous consultez peuvent être incomplètes ou erronées.", + "data.search.searchSource.fetch.shardsFailedNotificationMessage": "Échec de {shardsFailed} partitions sur {shardsTotal}", + "data.search.searchSource.hitsDescription": "Le nombre de documents renvoyés par la requête.", + "data.search.searchSource.hitsLabel": "Résultats", + "data.search.searchSource.hitsTotalDescription": "Le nombre de documents correspondant à la requête.", + "data.search.searchSource.hitsTotalLabel": "Résultats (total)", + "data.search.searchSource.indexPatternIdDescription": "L'ID dans l'index {kibanaIndexPattern}.", + "data.search.searchSource.queryTimeDescription": "Le temps qu'il a fallu pour traiter la requête. Ne comprend pas le temps nécessaire pour envoyer la requête ni l'analyser dans le navigateur.", + "data.search.searchSource.queryTimeLabel": "Durée de la requête", + "data.search.searchSource.queryTimeValue": "{queryTime}ms", + "data.search.searchSource.requestTimeDescription": "Durée de la requête depuis le navigateur jusqu’à Elasticsearch et retour. N’inclut pas le temps d’attente de la requête dans la file.", + "data.search.searchSource.requestTimeLabel": "Durée de la requête", + "data.search.searchSource.requestTimeValue": "{requestTime}ms", + "data.search.timeBuckets.dayLabel": "{amount, plural, one {un jour} other {# jours}}", + "data.search.timeBuckets.hourLabel": "{amount, plural, one {une heure} other {# heures}}", + "data.search.timeBuckets.infinityLabel": "Plus d'une année", + "data.search.timeBuckets.millisecondLabel": "{amount, plural, one {une milliseconde} other {# millisecondes}}", + "data.search.timeBuckets.minuteLabel": "{amount, plural, one {une minute} other {# minutes}}", + "data.search.timeBuckets.monthLabel": "un mois", + "data.search.timeBuckets.secondLabel": "{amount, plural, one {une seconde} other {# secondes}}", + "data.search.timeBuckets.yearLabel": "une année", + "data.search.timeoutContactAdmin": "Votre requête a expiré. Contactez l'administrateur système pour accroître le temps d'exécution.", + "data.search.timeoutIncreaseSetting": "Votre requête a expiré. Augmentez le temps d'exécution en utilisant le paramètre avancé de délai d'expiration de la recherche.", + "data.search.timeoutIncreaseSettingActionText": "Modifier le paramètre", + "data.search.unableToGetSavedQueryToastTitle": "Impossible de charger la requête enregistrée {savedQueryId}", + "data.searchSession.warning.readDocs": "En savoir plus", + "data.searchSessionIndicator.noCapability": "Vous n'êtes pas autorisé à créer des sessions de recherche.", + "data.searchSessions.sessionService.sessionEditNameError": "Échec de modification du nom de la session de recherche", + "data.searchSessions.sessionService.sessionObjectFetchError": "Échec de récupération des informations de la session de recherche", + "data.triggers.applyFilterDescription": "Lorsque le filtre Kibana est appliqué. Peut être un filtre simple ou un filtre de plage.", + "data.triggers.applyFilterTitle": "Appliquer le filtre", + "devTools.badge.readOnly.text": "Lecture seule", + "devTools.badge.readOnly.tooltip": "Enregistrement impossible", + "devTools.devToolsTitle": "Outils de développement", + "discover.advancedSettings.context.defaultSizeText": "Le nombre d'entrées connexes à afficher dans la vue contextuelle", + "discover.advancedSettings.context.defaultSizeTitle": "Taille de contexte", + "discover.advancedSettings.context.sizeStepText": "L’incrément duquel augmenter ou diminuer la taille de contexte", + "discover.advancedSettings.context.sizeStepTitle": "Incrément de taille de contexte", + "discover.advancedSettings.context.tieBreakerFieldsText": "Une liste de champs séparés par des virgules à utiliser pour départager les documents présentant la même valeur d'horodatage. Le premier champ de cette liste qui est à la fois présent et triable dans le modèle d'indexation en cours est utilisé.", + "discover.advancedSettings.context.tieBreakerFieldsTitle": "Champs de départage", + "discover.advancedSettings.defaultColumnsText": "Les colonnes affichées par défaut dans l'onglet Discover", + "discover.advancedSettings.defaultColumnsTitle": "Colonnes par défaut", + "discover.advancedSettings.discover.modifyColumnsOnSwitchText": "Supprimez les colonnes qui ne sont pas disponibles dans le nouveau modèle d'indexation.", + "discover.advancedSettings.discover.modifyColumnsOnSwitchTitle": "Modifier les colonnes en cas de changement des modèles d'indexation", + "discover.advancedSettings.discover.multiFieldsLinkText": "champs multiples", + "discover.advancedSettings.discover.readFieldsFromSource": "Lire les champs depuis _source", + "discover.advancedSettings.discover.readFieldsFromSourceDescription": "Lorsque cette option est activée, les documents sont chargés directement depuis ''_source''. Elle sera bientôt déclassée. Lorsqu'elle est désactivée, les champs sont extraits via la nouvelle API de champ du service de recherche de haut niveau.", + "discover.advancedSettings.discover.showMultifields": "Afficher les champs multiples", + "discover.advancedSettings.discover.showMultifieldsDescription": "Détermine si les {multiFields} doivent s'afficher dans la fenêtre de document étendue. Dans la plupart des cas, les champs multiples sont les mêmes que les champs d'origine. Cette option est uniquement disponible lorsque le paramètre ''searchFieldsFromSource'' est désactivé.", + "discover.advancedSettings.docTableHideTimeColumnText": "Permet de masquer la colonne ''Time'' dans Discover et dans toutes les recherches enregistrées des tableaux de bord.", + "discover.advancedSettings.docTableHideTimeColumnTitle": "Masquer la colonne ''Time''", + "discover.advancedSettings.fieldsPopularLimitText": "Les N champs les plus populaires à afficher", + "discover.advancedSettings.fieldsPopularLimitTitle": "Limite de champs populaires", + "discover.advancedSettings.maxDocFieldsDisplayedText": "Le nombre maximal de champs renvoyés dans la colonne de document", + "discover.advancedSettings.maxDocFieldsDisplayedTitle": "Nombre maximal de champs de document affichés", + "discover.advancedSettings.sampleSizeText": "Le nombre de lignes à afficher dans le tableau", + "discover.advancedSettings.sampleSizeTitle": "Nombre de lignes", + "discover.advancedSettings.searchOnPageLoadText": "Détermine si une recherche est exécutée lors du premier chargement de Discover. Ce paramètre n'a pas d'effet lors du chargement d’une recherche enregistrée.", + "discover.advancedSettings.searchOnPageLoadTitle": "Recherche au chargement de la page", + "discover.advancedSettings.sortDefaultOrderText": "Détermine le sens de tri par défaut pour les modèles d'indexation temporelle dans l’application Discover.", + "discover.advancedSettings.sortDefaultOrderTitle": "Sens de tri par défaut", + "discover.advancedSettings.sortOrderAsc": "Croissant", + "discover.advancedSettings.sortOrderDesc": "Décroissant", + "discover.backToTopLinkText": "Revenir en haut de la page.", + "discover.badge.readOnly.text": "Lecture seule", + "discover.badge.readOnly.tooltip": "Impossible d’enregistrer les recherches", + "discover.bucketIntervalTooltip": "Cet intervalle crée {bucketsDescription} pour permettre l’affichage dans la plage temporelle sélectionnée, il a donc été redimensionné vers {bucketIntervalDescription}.", + "discover.bucketIntervalTooltip.tooLargeBucketsText": "des compartiments trop volumineux", + "discover.bucketIntervalTooltip.tooManyBucketsText": "un trop grand nombre de compartiments", + "discover.clearSelection": "Effacer la sélection", + "discover.context.breadcrumb": "Documents relatifs", + "discover.context.contextOfTitle": "Les documents relatifs à #{anchorId}", + "discover.context.failedToLoadAnchorDocumentDescription": "Échec de chargement du document ancré", + "discover.context.failedToLoadAnchorDocumentErrorDescription": "Le document ancré n’a pas pu être chargé.", + "discover.context.invalidTieBreakerFiledSetting": "Paramètre de champ de départage non valide", + "discover.context.loadButtonLabel": "Charger", + "discover.context.loadingDescription": "Chargement...", + "discover.context.newerDocumentsAriaLabel": "Nombre de documents plus récents", + "discover.context.newerDocumentsDescription": "documents plus récents", + "discover.context.newerDocumentsWarning": "Seuls {docCount} documents plus récents que le document ancré ont été trouvés.", + "discover.context.newerDocumentsWarningZero": "Aucun document plus récent que le document ancré n'a été trouvé.", + "discover.context.olderDocumentsAriaLabel": "Nombre de documents plus anciens", + "discover.context.olderDocumentsDescription": "documents plus anciens", + "discover.context.olderDocumentsWarning": "Seuls {docCount} documents plus anciens que le document ancré ont été trouvés.", + "discover.context.olderDocumentsWarningZero": "Aucun document plus ancien que le document ancré n'a été trouvé.", + "discover.context.reloadPageDescription.reloadOrVisitTextMessage": "Veuillez recharger le document ou revenir à la liste pour sélectionner un document ancré valide.", + "discover.context.unableToLoadAnchorDocumentDescription": "Impossible de charger le document ancré", + "discover.context.unableToLoadDocumentDescription": "Impossible de charger les documents", + "discover.controlColumnHeader": "Colonne de commande", + "discover.copyToClipboardJSON": "Copier les documents dans le presse-papiers (JSON)", + "discover.discoverBreadcrumbTitle": "Discover", + "discover.discoverDefaultSearchSessionName": "Discover", + "discover.discoverDescription": "Explorez vos données de manière interactive en interrogeant et en filtrant des documents bruts.", + "discover.discoverSubtitle": "Recherchez et obtenez des informations.", + "discover.discoverTitle": "Discover", + "discover.doc.couldNotFindDocumentsDescription": "Aucun document ne correspond à cet ID.", + "discover.doc.failedToExecuteQueryDescription": "Impossible d'exécuter la recherche", + "discover.doc.failedToLocateDocumentDescription": "Document introuvable", + "discover.doc.loadingDescription": "Chargement…", + "discover.doc.somethingWentWrongDescription": "Index {indexName} manquant.", + "discover.doc.somethingWentWrongDescriptionAddon": "Veuillez vérifier que cet index existe.", + "discover.docTable.documentsNavigation": "Navigation dans les documents", + "discover.docTable.limitedSearchResultLabel": "Limité à {resultCount} résultats. Veuillez affiner votre recherche.", + "discover.docTable.noResultsTitle": "Aucun résultat trouvé.", + "discover.docTable.rows": "lignes", + "discover.docTable.rowsPerPage": "Lignes par page : {pageSize}", + "discover.docTable.tableHeader.documentHeader": "Document", + "discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel": "Déplacer la colonne {columnName} vers la gauche", + "discover.docTable.tableHeader.moveColumnLeftButtonTooltip": "Déplacer la colonne vers la gauche", + "discover.docTable.tableHeader.moveColumnRightButtonAriaLabel": "Déplacer la colonne {columnName} vers la droite", + "discover.docTable.tableHeader.moveColumnRightButtonTooltip": "Déplacer la colonne vers la droite", + "discover.docTable.tableHeader.removeColumnButtonAriaLabel": "Supprimer la colonne {columnName}", + "discover.docTable.tableHeader.removeColumnButtonTooltip": "Supprimer la colonne", + "discover.docTable.tableHeader.sortByColumnAscendingAriaLabel": "Trier la colonne {columnName} par ordre croissant", + "discover.docTable.tableHeader.sortByColumnDescendingAriaLabel": "Trier la colonne {columnName} par ordre décroissant", + "discover.docTable.tableHeader.sortByColumnUnsortedAriaLabel": "Arrêter de trier la colonne {columnName}", + "discover.docTable.tableRow.detailHeading": "Document développé", + "discover.docTable.tableRow.filterForValueButtonAriaLabel": "Filtrer sur la valeur", + "discover.docTable.tableRow.filterForValueButtonTooltip": "Filtrer sur la valeur", + "discover.docTable.tableRow.filterOutValueButtonAriaLabel": "Exclure la valeur", + "discover.docTable.tableRow.filterOutValueButtonTooltip": "Exclure la valeur", + "discover.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "Afficher/Masquer les détails de la ligne", + "discover.docTable.tableRow.viewSingleDocumentLinkText": "Afficher un seul document", + "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "Afficher les documents alentour", + "discover.docTable.totalDocuments": "{totalDocuments} documents", + "discover.documentsAriaLabel": "Documents", + "discover.docViews.json.jsonTitle": "JSON", + "discover.docViews.table.filterForFieldPresentButtonAriaLabel": "Filtrer sur le champ", + "discover.docViews.table.filterForFieldPresentButtonTooltip": "Filtrer sur le champ", + "discover.docViews.table.filterForValueButtonAriaLabel": "Filtrer sur la valeur", + "discover.docViews.table.filterForValueButtonTooltip": "Filtrer sur la valeur", + "discover.docViews.table.filterOutValueButtonAriaLabel": "Exclure la valeur", + "discover.docViews.table.filterOutValueButtonTooltip": "Exclure la valeur", + "discover.docViews.table.scoreSortWarningTooltip": "Filtrez sur _score pour pouvoir récupérer les valeurs correspondantes.", + "discover.docViews.table.tableTitle": "Tableau", + "discover.docViews.table.toggleColumnInTableButtonAriaLabel": "Afficher/Masquer la colonne dans le tableau", + "discover.docViews.table.toggleColumnInTableButtonTooltip": "Afficher/Masquer la colonne dans le tableau", + "discover.docViews.table.toggleFieldDetails": "Afficher/Masquer les détails du champ", + "discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "Impossible de filtrer sur les champs méta", + "discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "Impossible de filtrer sur les champs scriptés", + "discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "Il est impossible d’effectuer une recherche sur des champs non indexés.", + "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.displayName": "recherche", + "discover.field.mappingConflict": "Ce champ est défini avec plusieurs types (chaîne, entier, etc.) dans les différents index qui correspondent à ce modèle. Vous pouvez toujours utiliser ce champ conflictuel, mais il sera indisponible pour les fonctions qui nécessitent que Kibana en connaisse le type. Pour corriger ce problème, vous devrez réindexer vos données.", + "discover.field.mappingConflict.title": "Conflit de mapping", + "discover.field.title": "{fieldName} ({fieldDisplayName})", + "discover.fieldChooser.detailViews.emptyStringText": "Chaîne vide", + "discover.fieldChooser.detailViews.existsInRecordsText": "Existe dans {value} / {totalValue} enregistrements", + "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "Exclure le {field} : \"{value}\"", + "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "Filtrer sur le {field} : \"{value}\"", + "discover.fieldChooser.detailViews.valueOfRecordsText": "{value}/{totalValue} enregistrements", + "discover.fieldChooser.discoverField.actions": "Actions", + "discover.fieldChooser.discoverField.addButtonAriaLabel": "Ajouter {field} au tableau", + "discover.fieldChooser.discoverField.addFieldTooltip": "Ajouter le champ en tant que colonne", + "discover.fieldChooser.discoverField.deleteFieldLabel": "Supprimer le champ du modèle d'indexation", + "discover.fieldChooser.discoverField.editFieldLabel": "Modifier le champ du modèle d'indexation", + "discover.fieldChooser.discoverField.fieldTopValuesLabel": "Top 5 des valeurs", + "discover.fieldChooser.discoverField.multiField": "champ multiple", + "discover.fieldChooser.discoverField.multiFields": "Champs multiples", + "discover.fieldChooser.discoverField.multiFieldTooltipContent": "Les champs multiples peuvent avoir plusieurs valeurs.", + "discover.fieldChooser.discoverField.name": "Champ", + "discover.fieldChooser.discoverField.removeButtonAriaLabel": "Supprimer {field} du tableau", + "discover.fieldChooser.discoverField.removeFieldTooltip": "Supprimer le champ du tableau", + "discover.fieldChooser.discoverField.value": "Valeur", + "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "L'analyse n'est pas disponible pour les champs géométriques.", + "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "L'analyse n'est pas disponible pour les champs d'objet.", + "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "Ce champ est présent dans votre mapping Elasticsearch, mais pas dans les {hitsLength} documents affichés dans le tableau des documents. Cependant, vous pouvez toujours le consulter ou effectuer une recherche dessus.", + "discover.fieldChooser.fieldFilterButtonLabel": "Filtrer par type", + "discover.fieldChooser.fieldsMobileButtonLabel": "Champs", + "discover.fieldChooser.filter.aggregatableLabel": "Regroupable", + "discover.fieldChooser.filter.availableFieldsTitle": "Champs disponibles", + "discover.fieldChooser.filter.fieldSelectorLabel": "Sélection des options du filtre {id}", + "discover.fieldChooser.filter.filterByTypeLabel": "Filtrer par type", + "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "Index et champs", + "discover.fieldChooser.filter.popularTitle": "Populaire", + "discover.fieldChooser.filter.searchableLabel": "Interrogeable", + "discover.fieldChooser.filter.selectedFieldsTitle": "Champs sélectionnés", + "discover.fieldChooser.filter.toggleButton.any": "tout", + "discover.fieldChooser.filter.toggleButton.no": "non", + "discover.fieldChooser.filter.toggleButton.yes": "oui", + "discover.fieldChooser.filter.typeLabel": "Type", + "discover.fieldChooser.indexPatterns.actionsPopoverLabel": "Paramètres du modèle d'indexation", + "discover.fieldChooser.indexPatterns.addFieldButton": "Ajouter un champ au modèle d'indexation", + "discover.fieldChooser.indexPatterns.manageFieldButton": "Gérer les champs du modèle d'indexation", + "discover.fieldChooser.searchPlaceHolder": "Rechercher les noms de champs", + "discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "Masquer les paramètres de filtre de champs", + "discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "Afficher les paramètres de filtre de champs", + "discover.fieldChooser.visualizeButton.label": "Visualiser", + "discover.fieldList.flyoutBackIcon": "Retour", + "discover.fieldList.flyoutHeading": "Liste des champs", + "discover.fieldNameIcons.booleanAriaLabel": "Champ booléen", + "discover.fieldNameIcons.conflictFieldAriaLabel": "Champ conflictuel", + "discover.fieldNameIcons.dateFieldAriaLabel": "Champ de date", + "discover.fieldNameIcons.geoPointFieldAriaLabel": "Champ de point géographique", + "discover.fieldNameIcons.geoShapeFieldAriaLabel": "Champ de forme géométrique", + "discover.fieldNameIcons.ipAddressFieldAriaLabel": "Champ d'adresse IP", + "discover.fieldNameIcons.murmur3FieldAriaLabel": "Champ Murmur3", + "discover.fieldNameIcons.nestedFieldAriaLabel": "Champ imbriqué", + "discover.fieldNameIcons.numberFieldAriaLabel": "Champ numérique", + "discover.fieldNameIcons.sourceFieldAriaLabel": "Champ source", + "discover.fieldNameIcons.stringFieldAriaLabel": "Champ de chaîne", + "discover.fieldNameIcons.unknownFieldAriaLabel": "Champ inconnu", + "discover.grid.documentHeader": "Document", + "discover.grid.filterFor": "Filtrer sur", + "discover.grid.filterForAria": "Filtrer sur cette {value}", + "discover.grid.filterOut": "Exclure", + "discover.grid.filterOutAria": "Exclure cette {value}", + "discover.grid.flyout.documentNavigation": "Navigation dans le document", + "discover.grid.flyout.toastColumnAdded": "La colonne \"{columnName}\" a été ajoutée.", + "discover.grid.flyout.toastColumnRemoved": "La colonne \"{columnName}\" a été supprimée.", + "discover.grid.flyout.toastFilterAdded": "Le filtre a été ajouté.", + "discover.grid.tableRow.detailHeading": "Document développé", + "discover.grid.tableRow.viewSingleDocumentLinkTextSimple": "Document unique", + "discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple": "Documents relatifs", + "discover.grid.tableRow.viewText": "Afficher :", + "discover.grid.viewDoc": "Afficher/Masquer les détails de la boîte de dialogue", + "discover.helpMenu.appName": "Discover", + "discover.hideChart": "Masquer le graphique", + "discover.histogramOfFoundDocumentsAriaLabel": "Histogramme des documents détectés", + "discover.hitCountSpinnerAriaLabel": "Nombre final de résultats toujours en chargement", + "discover.hitsPluralTitle": "{formattedHits} {hits, plural, one {résultat} other {résultats}}", + "discover.howToSeeOtherMatchingDocumentsDescription": "Voici les {sampleSize} premiers documents correspondant à votre recherche. Veuillez affiner celle-ci pour en voir plus.", + "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "Voici les {sampleSize} premiers documents correspondant à votre recherche. Veuillez affiner celle-ci pour en voir plus.", + "discover.inspectorRequestDataTitleChart": "Données du graphique", + "discover.inspectorRequestDataTitleDocuments": "Documents", + "discover.inspectorRequestDataTitleTotalHits": "Nombre total de résultats", + "discover.inspectorRequestDescriptionChart": "Cette requête interroge Elasticsearch afin de récupérer les données d'agrégation pour le graphique.", + "discover.inspectorRequestDescriptionDocument": "Cette requête interroge Elasticsearch afin de récupérer les documents.", + "discover.inspectorRequestDescriptionTotalHits": "Cette requête interroge Elasticsearch afin de récupérer le nombre total de résultats.", + "discover.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", + "discover.json.copyToClipboardLabel": "Copier dans le presse-papiers", + "discover.loadingChartResults": "Chargement du graphique", + "discover.loadingDocuments": "Chargement des documents", + "discover.loadingJSON": "Chargement de JSON", + "discover.loadingResults": "Chargement des résultats", + "discover.localMenu.inspectTitle": "Inspecter", + "discover.localMenu.localMenu.newSearchTitle": "Nouveau", + "discover.localMenu.localMenu.optionsTitle": "Options", + "discover.localMenu.newSearchDescription": "Nouvelle recherche", + "discover.localMenu.openInspectorForSearchDescription": "Ouvrir l'inspecteur de recherche", + "discover.localMenu.openSavedSearchDescription": "Ouvrir une recherche enregistrée", + "discover.localMenu.openTitle": "Ouvrir", + "discover.localMenu.optionsDescription": "Options", + "discover.localMenu.saveSaveSearchObjectType": "recherche", + "discover.localMenu.saveSearchDescription": "Enregistrer la recherche", + "discover.localMenu.saveTitle": "Enregistrer", + "discover.localMenu.shareSearchDescription": "Partager la recherche", + "discover.localMenu.shareTitle": "Partager", + "discover.noResults.adjustFilters": "Modifiez les filtres.", + "discover.noResults.adjustSearch": "Modifiez la requête.", + "discover.noResults.expandYourTimeRangeTitle": "Étendre la plage temporelle", + "discover.noResults.queryMayNotMatchTitle": "Essayez de rechercher sur une période plus longue.", + "discover.noResults.searchExamples.noResultsBecauseOfError": "Une erreur s’est produite lors de la récupération des résultats de recherche.", + "discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle": "Aucun résultat ne correspond à vos critères de recherche.", + "discover.noResultsFound": "Aucun résultat trouvé.", + "discover.notifications.invalidTimeRangeText": "La plage temporelle spécifiée n'est pas valide (de : \"{from}\" à \"{to}\").", + "discover.notifications.invalidTimeRangeTitle": "Plage temporelle non valide", + "discover.notifications.notSavedSearchTitle": "La recherche \"{savedSearchTitle}\" n'a pas été enregistrée.", + "discover.notifications.savedSearchTitle": "La recherche \"{savedSearchTitle}\" a été enregistrée.", + "discover.partialHits": "≥ {formattedHits} {hits, plural, one {résultat} other {résultats}}", + "discover.reloadSavedSearchButton": "Réinitialiser la recherche", + "discover.removeColumnLabel": "Supprimer la colonne", + "discover.rootBreadcrumb": "Discover", + "discover.savedSearch.savedObjectName": "Recherche enregistrée", + "discover.searchGenerationWithDescription": "Tableau généré par la recherche {searchTitle}", + "discover.searchGenerationWithDescriptionGrid": "Tableau généré par la recherche {searchTitle} ({searchDescription})", + "discover.searchingTitle": "Recherche", + "discover.selectColumnHeader": "Sélectionner la colonne", + "discover.selectedDocumentsNumber": "{nr} documents sélectionnés", + "discover.showAllDocuments": "Afficher tous les documents", + "discover.showChart": "Afficher le graphique", + "discover.showErrorMessageAgain": "Afficher le message d'erreur", + "discover.showSelectedDocumentsOnly": "Afficher uniquement les documents sélectionnés", + "discover.skipToBottomButtonLabel": "Atteindre la fin du tableau", + "discover.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.", + "discover.sourceViewer.errorMessageTitle": "Une erreur s'est produite.", + "discover.sourceViewer.refresh": "Actualiser", + "discover.toggleSidebarAriaLabel": "Afficher/Masquer la barre latérale", + "discover.topNav.openSearchPanel.manageSearchesButtonLabel": "Gérer les recherches", + "discover.topNav.openSearchPanel.noSearchesFoundDescription": "Aucune recherche correspondante trouvée.", + "discover.topNav.openSearchPanel.openSearchTitle": "Ouvrir une recherche", + "discover.topNav.optionsPopover.currentViewMode": "{viewModeLabel} : {currentViewMode}", + "discover.uninitializedRefreshButtonText": "Actualiser les données", + "discover.uninitializedText": "Saisissez une requête, ajoutez quelques filtres, ou cliquez simplement sur Actualiser afin d’extraire les résultats pour la requête en cours.", + "discover.uninitializedTitle": "Commencer la recherche", + "embeddableApi.addPanel.createNewDefaultOption": "Créer", + "embeddableApi.addPanel.displayName": "Ajouter un panneau", + "embeddableApi.addPanel.noMatchingObjectsMessage": "Aucun objet correspondant trouvé.", + "embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} a été ajouté.", + "embeddableApi.addPanel.Title": "Ajouter depuis la bibliothèque", + "embeddableApi.attributeService.saveToLibraryError": "Une erreur s'est produite lors de l'enregistrement. Erreur : {errorMessage}", + "embeddableApi.contextMenuTrigger.description": "Un menu contextuel cliquable dans l’angle supérieur droit du panneau.", + "embeddableApi.contextMenuTrigger.title": "Menu contextuel", + "embeddableApi.customizePanel.action.displayName": "Modifier le titre du panneau", + "embeddableApi.customizePanel.modal.cancel": "Annuler", + "embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel": "Titre du panneau", + "embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel": "Entrez un titre personnalisé pour le panneau.", + "embeddableApi.customizePanel.modal.optionsMenuForm.resetCustomDashboardButtonLabel": "Réinitialiser", + "embeddableApi.customizePanel.modal.saveButtonTitle": "Enregistrer", + "embeddableApi.customizePanel.modal.showTitle": "Afficher le titre du panneau", + "embeddableApi.customizeTitle.optionsMenuForm.panelTitleFormRowLabel": "Titre du panneau", + "embeddableApi.customizeTitle.optionsMenuForm.panelTitleInputAriaLabel": "Les modifications apportées à cette entrée sont appliquées immédiatement. Appuyez sur Entrée pour quitter.", + "embeddableApi.customizeTitle.optionsMenuForm.resetCustomDashboardButtonLabel": "Réinitialiser le titre", + "embeddableApi.errors.embeddableFactoryNotFound": "Impossible de charger {type}. Veuillez effectuer une mise à niveau vers la distribution par défaut d'Elasticsearch et de Kibana avec la licence appropriée.", + "embeddableApi.errors.paneldoesNotExist": "Panneau introuvable", + "embeddableApi.helloworld.displayName": "bonjour", + "embeddableApi.panel.dashboardPanelAriaLabel": "Panneau du tableau de bord", + "embeddableApi.panel.editPanel.displayName": "Modifier {value}", + "embeddableApi.panel.editTitleAriaLabel": "Cliquez pour modifier le titre : {title}", + "embeddableApi.panel.enhancedDashboardPanelAriaLabel": "Panneau du tableau de bord : {title}", + "embeddableApi.panel.inspectPanel.displayName": "Inspecter", + "embeddableApi.panel.inspectPanel.untitledEmbeddableFilename": "sans titre", + "embeddableApi.panel.labelAborted": "Annulé", + "embeddableApi.panel.labelError": "Erreur", + "embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel": "Options de panneau", + "embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel": "Options de panneau pour {title}", + "embeddableApi.panel.placeholderTitle": "[Aucun titre]", + "embeddableApi.panel.removePanel.displayName": "Supprimer du tableau de bord", + "embeddableApi.panelBadgeTrigger.description": "Des actions apparaissent dans la barre de titre lorsqu'un élément pouvant être intégré est chargé dans un panneau.", + "embeddableApi.panelBadgeTrigger.title": "Badges du panneau", + "embeddableApi.panelNotificationTrigger.description": "Les actions apparaissent dans l’angle supérieur droit des panneaux.", + "embeddableApi.panelNotificationTrigger.title": "Notifications du panneau", + "embeddableApi.samples.contactCard.displayName": "carte de visite", + "embeddableApi.samples.filterableContainer.displayName": "tableau de bord filtrable", + "embeddableApi.samples.filterableEmbeddable.displayName": "filtrable", + "embeddableApi.selectRangeTrigger.description": "Une plage de valeurs sur la visualisation", + "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", + "esQuery.kql.errors.endOfInputText": "fin de l'entrée", + "esQuery.kql.errors.fieldNameText": "nom du champ", + "esQuery.kql.errors.literalText": "littéral", + "esQuery.kql.errors.syntaxError": "{expectedList} attendu, mais {foundInput} détecté.", + "esQuery.kql.errors.valueText": "valeur", + "esQuery.kql.errors.whitespaceText": "whitespace", + "esUi.cronEditor.cronDaily.fieldHour.textAtLabel": "À", + "esUi.cronEditor.cronDaily.fieldTimeLabel": "Heure", + "esUi.cronEditor.cronDaily.hourSelectLabel": "Heure", + "esUi.cronEditor.cronDaily.minuteSelectLabel": "Minute", + "esUi.cronEditor.cronHourly.fieldMinute.textAtLabel": "À", + "esUi.cronEditor.cronHourly.fieldTimeLabel": "Minute", + "esUi.cronEditor.cronMonthly.fieldDateLabel": "Date", + "esUi.cronEditor.cronMonthly.fieldHour.textAtLabel": "À", + "esUi.cronEditor.cronMonthly.fieldTimeLabel": "Heure", + "esUi.cronEditor.cronMonthly.hourSelectLabel": "Heure", + "esUi.cronEditor.cronMonthly.minuteSelectLabel": "Minute", + "esUi.cronEditor.cronMonthly.textOnTheLabel": "Le", + "esUi.cronEditor.cronWeekly.fieldDateLabel": "Jour", + "esUi.cronEditor.cronWeekly.fieldHour.textAtLabel": "À", + "esUi.cronEditor.cronWeekly.fieldTimeLabel": "Heure", + "esUi.cronEditor.cronWeekly.hourSelectLabel": "Heure", + "esUi.cronEditor.cronWeekly.minuteSelectLabel": "Minute", + "esUi.cronEditor.cronWeekly.textOnLabel": "Le", + "esUi.cronEditor.cronYearly.fieldDate.textOnTheLabel": "Le", + "esUi.cronEditor.cronYearly.fieldDateLabel": "Date", + "esUi.cronEditor.cronYearly.fieldHour.textAtLabel": "À", + "esUi.cronEditor.cronYearly.fieldMonth.textInLabel": "En", + "esUi.cronEditor.cronYearly.fieldMonthLabel": "Mois", + "esUi.cronEditor.cronYearly.fieldTimeLabel": "Heure", + "esUi.cronEditor.cronYearly.hourSelectLabel": "Heure", + "esUi.cronEditor.cronYearly.minuteSelectLabel": "Minute", + "esUi.cronEditor.day.friday": "vendredi", + "esUi.cronEditor.day.monday": "lundi", + "esUi.cronEditor.day.saturday": "samedi", + "esUi.cronEditor.day.sunday": "dimanche", + "esUi.cronEditor.day.thursday": "jeudi", + "esUi.cronEditor.day.tuesday": "mardi", + "esUi.cronEditor.day.wednesday": "mercredi", + "esUi.cronEditor.fieldFrequencyLabel": "Fréquence", + "esUi.cronEditor.month.april": "avril", + "esUi.cronEditor.month.august": "août", + "esUi.cronEditor.month.december": "décembre", + "esUi.cronEditor.month.february": "février", + "esUi.cronEditor.month.january": "janvier", + "esUi.cronEditor.month.july": "juillet", + "esUi.cronEditor.month.june": "juin", + "esUi.cronEditor.month.march": "mars", + "esUi.cronEditor.month.may": "mai", + "esUi.cronEditor.month.november": "novembre", + "esUi.cronEditor.month.october": "octobre", + "esUi.cronEditor.month.september": "septembre", + "esUi.cronEditor.textEveryLabel": "Chaque", + "esUi.forms.comboBoxField.placeHolderText": "Saisir, puis appuyer sur \"ENTRÉE\"", + "esUi.forms.fieldValidation.indexNameInvalidCharactersError": "Le nom de l'index contient {characterListLength, plural, one {le caractère non valide} other {les caractères non valides}} {characterList}.", + "esUi.forms.fieldValidation.indexNameSpacesError": "Le nom de l'index ne peut pas contenir d'espaces.", + "esUi.forms.fieldValidation.indexNameStartsWithDotError": "Le nom de l'index ne peut pas commencer par un point (.).", + "esUi.forms.fieldValidation.indexPatternInvalidCharactersError": "Le modèle d'indexation contient {characterListLength, plural, one {le caractère non valide} other {les caractères non valides}} {characterList}.", + "esUi.forms.fieldValidation.indexPatternSpacesError": "Le modèle d'indexation ne peut pas contenir d'espaces.", + "esUi.formWizard.backButtonLabel": "Retour", + "esUi.formWizard.nextButtonLabel": "Suivant", + "esUi.formWizard.saveButtonLabel": "Enregistrer", + "esUi.formWizard.savingButtonLabel": "Enregistrement en cours...", + "esUi.validation.string.invalidJSONError": "JSON non valide", + "expressionError.errorComponent.description": "Échec de l'expression avec le message :", + "expressionError.errorComponent.title": "Oups ! Échec de l'expression", + "expressionError.renderer.debug.displayName": "Débogage", + "expressionError.renderer.debug.helpDescription": "Présenter une sortie de débogage formatée {JSON}", + "expressionError.renderer.error.displayName": "Informations sur l'erreur", + "expressionError.renderer.error.helpDescription": "Présenter les données de l'erreur d'une manière utile pour les utilisateurs", + "expressionImage.functions.image.args.dataurlHelpText": "L'{URL} {https} ou l'{URL} de données {BASE64} d'une image.", + "expressionImage.functions.image.args.modeHelpText": "{contain} affiche l'image entière, mise à l’échelle. {cover} remplit le conteneur avec l'image, en rognant les côtés ou le bas si besoin. {stretch} redimensionne la hauteur et la largeur de l'image pour correspondre à 100 % du conteneur.", + "expressionImage.functions.image.invalidImageModeErrorMessage": "\"mode\" doit être défini sur \"{contain}\", \"{cover}\" ou \"{stretch}\".", + "expressionImage.functions.imageHelpText": "Affiche une image. Spécifiez une ressource d'image sous la forme d'une {URL} de données {BASE64}, ou saisissez une sous-expression.", + "expressionImage.renderer.image.displayName": "Image", + "expressionImage.renderer.image.helpDescription": "Présenter une image", + "expressionMetric.functions.metric.args.labelFontHelpText": "Les propriétés de la police {CSS} pour l'étiquette. Par exemple, {FONT_FAMILY} ou {FONT_WEIGHT}.", + "expressionMetric.functions.metric.args.labelHelpText": "Le texte décrivant l'indicateur.", + "expressionMetric.functions.metric.args.metricFontHelpText": "Les propriétés de la police {CSS} pour l'indicateur. Par exemple, {FONT_FAMILY} ou {FONT_WEIGHT}.", + "expressionMetric.functions.metric.args.metricFormatHelpText": "Une chaîne de format {NUMERALJS}. Par exemple, {example1} ou {example2}.", + "expressionMetric.functions.metricHelpText": "Affiche un nombre sur une étiquette.", + "expressionMetric.renderer.metric.displayName": "Indicateur", + "expressionMetric.renderer.metric.helpDescription": "Présenter un nombre sur une étiquette", + "expressionRepeatImage.error.repeatImage.missingMaxArgument": "{maxArgument} doit être défini si un {emptyImageArgument} est fourni.", + "expressionRepeatImage.functions.repeatImage.args.emptyImageHelpText": "Comble la différence entre les paramètres {CONTEXT} et {maxArg} pour l'élément avec cette image. Spécifiez une ressource d'image sous la forme d'une {URL} de données {BASE64}, ou saisissez une sous-expression.", + "expressionRepeatImage.functions.repeatImage.args.imageHelpText": "L'image à répéter. Spécifiez une ressource d'image sous la forme d'une {URL} de données {BASE64}, ou saisissez une sous-expression.", + "expressionRepeatImage.functions.repeatImage.args.maxHelpText": "Le nombre maximal de fois que l'image peut être répétée.", + "expressionRepeatImage.functions.repeatImage.args.sizeHelpText": "La hauteur ou largeur maximale de l'image, en pixels. Lorsque l'image est plus haute que large, cette fonction limite la hauteur.", + "expressionRepeatImage.functions.repeatImageHelpText": "Configure un élément de répétition d’image.", + "expressionRepeatImage.renderer.repeatImage.displayName": "Répétition d’image", + "expressionRepeatImage.renderer.repeatImage.helpDescription": "Présenter une répétition d’image basique", + "expressionRevealImage.functions.revealImage.args.emptyImageHelpText": "Une image d'arrière-plan facultative à révéler. Spécifiez une ressource d'image sous la forme d’une {URL} de données \"{BASE64}\", ou saisissez une sous-expression.", + "expressionRevealImage.functions.revealImage.args.imageHelpText": "L'image à révéler. Spécifiez une ressource d'image sous la forme d'une {URL} de données {BASE64}, ou saisissez une sous-expression.", + "expressionRevealImage.functions.revealImage.args.originHelpText": "La position à laquelle démarrer le remplissage de l'image. Par exemple, {list} ou {end}.", + "expressionRevealImage.functions.revealImage.invalidImageUrl": "URL d'image non valide : \"{imageUrl}\".", + "expressionRevealImage.functions.revealImage.invalidPercentErrorMessage": "Valeur non valide : \"{percent}\". Le pourcentage doit être compris entre 0 et 1.", + "expressionRevealImage.functions.revealImageHelpText": "Configure un élément de révélation d'image.", + "expressionRevealImage.renderer.revealImage.displayName": "Révélation d'image", + "expressionRevealImage.renderer.revealImage.helpDescription": "Révèle un pourcentage d'une image pour concevoir un graphique à jauge personnalisé.", + "expressions.defaultErrorRenderer.errorTitle": "Erreur dans la visualisation", + "expressions.execution.functionDisabled": "Fonction {fnName} désactivée.", + "expressions.execution.functionNotFound": "Fonction {fnName} introuvable.", + "expressions.functions.createTable.args.idsHelpText": "ID de colonne à générer dans l'ordre de position. L'ID représente la clé dans la ligne.", + "expressions.functions.createTable.args.nameHelpText": "Noms de colonne à générer dans l'ordre de position. Ces noms n'ont pas besoin d'être uniques et, en l’absence de noms, les ID sont utilisés par défaut.", + "expressions.functions.createTable.args.rowCountText": "Le nombre de lignes vides à ajouter au tableau, pour y attribuer une valeur plus tard", + "expressions.functions.createTableHelpText": "Crée une table de données avec une liste de colonnes, et une ou plusieurs lignes vides. Pour générer les lignes, utilisez {mapColumnFn} ou {mathColumnFn}.", + "expressions.functions.cumulativeSum.args.byHelpText": "Colonne par laquelle diviser le calcul de la somme cumulée", + "expressions.functions.cumulativeSum.args.inputColumnIdHelpText": "Colonne pour laquelle calculer la somme cumulée", + "expressions.functions.cumulativeSum.args.outputColumnIdHelpText": "Colonne dans laquelle stocker le résultat de la somme cumulée", + "expressions.functions.cumulativeSum.args.outputColumnNameHelpText": "Nom de la colonne dans laquelle stocker le résultat de la somme cumulée", + "expressions.functions.cumulativeSum.help": "Calcule la somme cumulée d'une colonne dans un tableau de données.", + "expressions.functions.derivative.args.byHelpText": "Colonne par laquelle diviser le calcul de la dérivée", + "expressions.functions.derivative.args.inputColumnIdHelpText": "Colonne pour laquelle calculer la dérivée", + "expressions.functions.derivative.args.outputColumnIdHelpText": "Colonne dans laquelle stocker le résultat de la dérivée", + "expressions.functions.derivative.args.outputColumnNameHelpText": "Nom de la colonne dans laquelle stocker le résultat de la dérivée", + "expressions.functions.derivative.help": "Calcule la dérivée d'une colonne dans un tableau de données.", + "expressions.functions.font.args.alignHelpText": "L'alignement horizontal du texte.", + "expressions.functions.font.args.colorHelpText": "La couleur du texte.", + "expressions.functions.font.args.familyHelpText": "Une chaîne de police Internet {css} acceptable", + "expressions.functions.font.args.italicHelpText": "Mettre le texte en italique ?", + "expressions.functions.font.args.lHeightHelpText": "La hauteur de la ligne en pixels", + "expressions.functions.font.args.sizeHelpText": "La taille de la police en pixels", + "expressions.functions.font.args.underlineHelpText": "Souligner le texte ?", + "expressions.functions.font.args.weightHelpText": "L’épaisseur de la police. Par exemple, {list} ou {end}.", + "expressions.functions.font.invalidFontWeightErrorMessage": "Épaisseur de police non valide : \"{weight}\"", + "expressions.functions.font.invalidTextAlignmentErrorMessage": "Alignement du texte non valide : \"{align}\"", + "expressions.functions.fontHelpText": "Créez un style de police.", + "expressions.functions.mapColumn.args.copyMetaFromHelpText": "Si défini, l'objet méta de l'ID de colonne spécifié est copié dans la colonne cible spécifiée. Si la colonne n'existe pas, un échec silencieux se produit.", + "expressions.functions.mapColumn.args.expressionHelpText": "Une expression qui est exécutée sur chaque ligne, fournie avec un contexte {DATATABLE} de ligne unique et retournant la valeur de la cellule.", + "expressions.functions.mapColumn.args.idHelpText": "Un ID facultatif de la colonne de résultat. Si aucun ID n'est fourni, l'ID est récupéré de la colonne existante par l'argument de nom fourni. S'il n'existe pas encore de colonne à ce nom, une nouvelle colonne avec ce nom et un ID identique est ajoutée au tableau.", + "expressions.functions.mapColumn.args.nameHelpText": "Le nom de la colonne produite. Les noms n'ont pas besoin d'être uniques.", + "expressions.functions.mapColumnHelpText": "Ajoute une colonne calculée comme le résultat d'autres colonnes. Des modifications ne sont apportées que si des arguments sont fournis. Voir également {alterColumnFn} et {staticColumnFn}.", + "expressions.functions.math.args.expressionHelpText": "Une expression {TINYMATH} évaluée. Voir {TINYMATH_URL}.", + "expressions.functions.math.args.onErrorHelpText": "Si l’évaluation {TINYMATH} échoue ou renvoie NaN, la valeur de retour est spécifiée par onError. Lors de la ''génération'', une exception est levée, terminant l'exécution de l'expression (par défaut).", + "expressions.functions.math.emptyDatatableErrorMessage": "Table de données vide", + "expressions.functions.math.emptyExpressionErrorMessage": "Expression vide", + "expressions.functions.math.executionFailedErrorMessage": "Échec d'exécution de l'expression mathématique. Vérifiez les noms des colonnes.", + "expressions.functions.math.tooManyResultsErrorMessage": "Les expressions doivent retourner un nombre unique. Essayez d'englober votre expression dans {mean} ou {sum}.", + "expressions.functions.mathColumn.args.copyMetaFromHelpText": "Si défini, l'objet méta de l'ID de colonne spécifié est copié dans la colonne cible spécifiée. Si la colonne n'existe pas, un échec silencieux se produit.", + "expressions.functions.mathColumn.args.idHelpText": "ID de la colonne produite. Doit être unique.", + "expressions.functions.mathColumn.args.nameHelpText": "Le nom de la colonne produite. Les noms n'ont pas besoin d'être uniques.", + "expressions.functions.mathColumn.arrayValueError": "Impossible de réaliser le calcul sur les valeurs du tableau à {name}", + "expressions.functions.mathColumn.uniqueIdError": "L'ID doit être unique.", + "expressions.functions.mathHelpText": "Interprète une expression mathématique {TINYMATH} à l'aide d'un {TYPE_NUMBER} ou d'une {DATATABLE} en tant que {CONTEXT}. Les colonnes {DATATABLE} peuvent être recherchées d’après leur nom. Si {CONTEXT} est un nombre, il est disponible en tant que {value}.", + "expressions.functions.movingAverage.args.byHelpText": "Colonne par laquelle diviser le calcul de la moyenne mobile", + "expressions.functions.movingAverage.args.inputColumnIdHelpText": "Colonne pour laquelle calculer la moyenne mobile", + "expressions.functions.movingAverage.args.outputColumnIdHelpText": "Colonne dans laquelle stocker le résultat de la moyenne mobile", + "expressions.functions.movingAverage.args.outputColumnNameHelpText": "Nom de la colonne dans laquelle stocker le résultat de la moyenne mobile", + "expressions.functions.movingAverage.args.windowHelpText": "La taille de la fenêtre à \"faire glisser\" le long de l'histogramme.", + "expressions.functions.movingAverage.help": "Calcule la moyenne mobile d'une colonne dans un tableau de données.", + "expressions.functions.overallMetric.args.byHelpText": "Colonne par laquelle diviser le calcul général", + "expressions.functions.overallMetric.args.inputColumnIdHelpText": "Colonne pour laquelle calculer l’indicateur général", + "expressions.functions.overallMetric.args.outputColumnIdHelpText": "Colonne dans laquelle stocker le résultat de l'indicateur général", + "expressions.functions.overallMetric.args.outputColumnNameHelpText": "Nom de la colonne dans laquelle stocker le résultat de l’indicateur général", + "expressions.functions.overallMetric.help": "Calcule la somme, le minimum, le maximum ou la moyenne générale d'une colonne dans un tableau de données.", + "expressions.functions.overallMetric.metricHelpText": "Indicateur à calculer", + "expressions.functions.seriesCalculations.columnConflictMessage": "L'ID de colonne de sortie {columnId} existe déjà. Veuillez choisir un autre ID de colonne.", + "expressions.functions.theme.args.defaultHelpText": "La valeur par défaut lorsqu’aucune information de thème n’est disponible.", + "expressions.functions.theme.args.variableHelpText": "Nom de la variable de thème à lire.", + "expressions.functions.themeHelpText": "Lit un paramètre de thème.", + "expressions.functions.uiSetting.args.default": "La valeur par défaut utilisée lorsque le paramètre n’est pas défini.", + "expressions.functions.uiSetting.args.parameter": "Le nom du paramètre.", + "expressions.functions.uiSetting.error.kibanaRequest": "Une requête Kibana est nécessaire pour obtenir les paramètres de l'interface utilisateur sur le serveur. Veuillez fournir un objet de requête pour les paramètres d'exécution de l'expression.", + "expressions.functions.uiSetting.error.parameter": "Paramètre \"{parameter}\" non valide.", + "expressions.functions.uiSetting.help": "Renvoie une valeur de paramètre de l'interface utilisateur.", + "expressions.functions.var.help": "Met à jour le contexte général de Kibana.", + "expressions.functions.var.name.help": "Spécifiez le nom de la variable.", + "expressions.functions.varset.help": "Met à jour le contexte général de Kibana.", + "expressions.functions.varset.name.help": "Spécifiez le nom de la variable.", + "expressions.functions.varset.val.help": "Spécifiez la valeur de la variable. Sinon, le contexte d'entrée est utilisé.", + "expressions.types.number.fromStringConversionErrorMessage": "Impossible de cataloguer la chaîne \"{string}\" en nombre", + "expressionShape.functions.progress.args.barColorHelpText": "La couleur de la barre d'arrière-plan.", + "expressionShape.functions.progress.args.barWeightHelpText": "L'épaisseur de la barre d'arrière-plan.", + "expressionShape.functions.progress.args.fontHelpText": "Les propriétés de la police {CSS} pour l'étiquette. Par exemple, {FONT_FAMILY} ou {FONT_WEIGHT}.", + "expressionShape.functions.progress.args.labelHelpText": "Pour afficher ou masquer l'étiquette, utilisez {BOOLEAN_TRUE} ou {BOOLEAN_FALSE}. Vous pouvez également spécifier une chaîne à afficher en tant qu'étiquette.", + "expressionShape.functions.progress.args.maxHelpText": "La valeur maximale de l'élément de progression.", + "expressionShape.functions.progress.args.shapeHelpText": "Sélectionnez {list} ou {end}.", + "expressionShape.functions.progress.args.valueColorHelpText": "La couleur de la barre de progression.", + "expressionShape.functions.progress.args.valueWeightHelpText": "L'épaisseur de la barre de progression.", + "expressionShape.functions.progress.invalidMaxValueErrorMessage": "Valeur {arg} non valide : \"{max, number}\" ; \"{arg}\" doit être supérieur à 0.", + "expressionShape.functions.progress.invalidValueErrorMessage": "Valeur non valide : \"{value, number}\". La valeur doit être comprise entre 0 et {max, number}.", + "expressionShape.functions.progressHelpText": "Configure un élément de progression.", + "expressionShape.functions.shape.args.borderHelpText": "Une couleur {SVG} pour la bordure de la forme.", + "expressionShape.functions.shape.args.borderWidthHelpText": "L'épaisseur de la bordure.", + "expressionShape.functions.shape.args.fillHelpText": "Une couleur {SVG} de remplissage de la forme.", + "expressionShape.functions.shape.args.maintainAspectHelpText": "Conserver le rapport d'origine de la forme ?", + "expressionShape.functions.shape.args.shapeHelpText": "Choisissez une forme.", + "expressionShape.functions.shape.invalidShapeErrorMessage": "Valeur non valide : \"{shape}\". Cette forme n'existe pas.", + "expressionShape.functions.shapeHelpText": "Crée une forme.", + "expressionShape.renderer.progress.displayName": "Progression", + "expressionShape.renderer.progress.helpDescription": "Présenter une progression basique", + "expressionShape.renderer.shape.displayName": "Forme", + "expressionShape.renderer.shape.helpDescription": "Présenter une forme basique", + "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "Format numérique", + "fieldFormats.advancedSettings.format.bytesFormatText": "{numeralFormatLink} par défaut pour le format \"octets\"", + "fieldFormats.advancedSettings.format.bytesFormatTitle": "Format octets", + "fieldFormats.advancedSettings.format.currencyFormat.numeralFormatLinkText": "Format numérique", + "fieldFormats.advancedSettings.format.currencyFormatText": "{numeralFormatLink} par défaut pour le format \"devise\"", + "fieldFormats.advancedSettings.format.currencyFormatTitle": "Format devise", + "fieldFormats.advancedSettings.format.defaultTypeMapText": "Mapping du nom du format à utiliser par défaut pour chaque type de champ. Le format {defaultFormat} est utilisé lorsque le type de champ n'est pas mentionné explicitement.", + "fieldFormats.advancedSettings.format.defaultTypeMapTitle": "Nom du format du type de champ", + "fieldFormats.advancedSettings.format.formattingLocale.numeralLanguageLinkText": "Langage numérique", + "fieldFormats.advancedSettings.format.formattingLocaleText": "Paramètre régional {numeralLanguageLink}", + "fieldFormats.advancedSettings.format.formattingLocaleTitle": "Paramètre régional de format", + "fieldFormats.advancedSettings.format.numberFormat.numeralFormatLinkText": "Format numérique", + "fieldFormats.advancedSettings.format.numberFormatText": "{numeralFormatLink} par défaut pour le format \"nombre\"", + "fieldFormats.advancedSettings.format.numberFormatTitle": "Format nombre", + "fieldFormats.advancedSettings.format.percentFormat.numeralFormatLinkText": "Format numérique", + "fieldFormats.advancedSettings.format.percentFormatText": "{numeralFormatLink} par défaut pour le format \"pourcentage\"", + "fieldFormats.advancedSettings.format.percentFormatTitle": "Format pourcentage", + "fieldFormats.advancedSettings.shortenFieldsText": "Raccourcir les champs longs, par exemple f.b.baz plutôt que foo.bar.baz", + "fieldFormats.advancedSettings.shortenFieldsTitle": "Raccourcir les champs", + "fieldFormats.boolean.title": "Booléen", + "fieldFormats.bytes.title": "Octets", + "fieldFormats.color.title": "Couleur", + "fieldFormats.date_nanos.title": "Date nanos", + "fieldFormats.date.title": "Date", + "fieldFormats.duration.inputFormats.days": "Jours", + "fieldFormats.duration.inputFormats.hours": "Heures", + "fieldFormats.duration.inputFormats.microseconds": "Microsecondes", + "fieldFormats.duration.inputFormats.milliseconds": "Millisecondes", + "fieldFormats.duration.inputFormats.minutes": "Minutes", + "fieldFormats.duration.inputFormats.months": "Mois", + "fieldFormats.duration.inputFormats.nanoseconds": "Nanosecondes", + "fieldFormats.duration.inputFormats.picoseconds": "Picosecondes", + "fieldFormats.duration.inputFormats.seconds": "Secondes", + "fieldFormats.duration.inputFormats.weeks": "Semaines", + "fieldFormats.duration.inputFormats.years": "Années", + "fieldFormats.duration.negativeLabel": "moins", + "fieldFormats.duration.outputFormats.asDays": "Jours", + "fieldFormats.duration.outputFormats.asDays.short": "j", + "fieldFormats.duration.outputFormats.asHours": "Heures", + "fieldFormats.duration.outputFormats.asHours.short": "h", + "fieldFormats.duration.outputFormats.asMilliseconds": "Millisecondes", + "fieldFormats.duration.outputFormats.asMilliseconds.short": "ms", + "fieldFormats.duration.outputFormats.asMinutes": "Minutes", + "fieldFormats.duration.outputFormats.asMinutes.short": "min", + "fieldFormats.duration.outputFormats.asMonths": "Mois", + "fieldFormats.duration.outputFormats.asMonths.short": "mois", + "fieldFormats.duration.outputFormats.asSeconds": "Secondes", + "fieldFormats.duration.outputFormats.asSeconds.short": "s", + "fieldFormats.duration.outputFormats.asWeeks": "Semaines", + "fieldFormats.duration.outputFormats.asWeeks.short": "w", + "fieldFormats.duration.outputFormats.asYears": "Années", + "fieldFormats.duration.outputFormats.asYears.short": "y", + "fieldFormats.duration.outputFormats.humanize.approximate": "Lisible par l'humain (approximatif)", + "fieldFormats.duration.outputFormats.humanize.precise": "Lisible par l'humain (précis)", + "fieldFormats.duration.title": "Durée", + "fieldFormats.histogram.title": "Histogramme", + "fieldFormats.ip.title": "Adresse IP", + "fieldFormats.number.title": "Nombre", + "fieldFormats.percent.title": "Pourcentage", + "fieldFormats.relative_date.title": "Date relative", + "fieldFormats.static_lookup.title": "Recherche statique", + "fieldFormats.string.emptyLabel": "(vide)", + "fieldFormats.string.title": "Chaîne", + "fieldFormats.string.transformOptions.base64": "Décodage Base64", + "fieldFormats.string.transformOptions.lower": "Minuscule", + "fieldFormats.string.transformOptions.none": "- Aucune -", + "fieldFormats.string.transformOptions.short": "Points courts", + "fieldFormats.string.transformOptions.title": "Initiale majuscule", + "fieldFormats.string.transformOptions.upper": "Majuscule", + "fieldFormats.string.transformOptions.url": "Décodage paramètre URL", + "fieldFormats.truncated_string.title": "Chaîne tronquée", + "fieldFormats.url.title": "Url", + "fieldFormats.url.types.audio": "Audio", + "fieldFormats.url.types.img": "Image", + "fieldFormats.url.types.link": "Lien", + "flot.pie.unableToDrawLabelsInsideCanvasErrorMessage": "Impossible de dessiner un graphique avec les étiquettes contenues dans la toile", + "flot.time.aprLabel": "Avr", + "flot.time.augLabel": "Août", + "flot.time.decLabel": "Déc", + "flot.time.febLabel": "Févr", + "flot.time.friLabel": "Ven", + "flot.time.janLabel": "Jan", + "flot.time.julLabel": "Juil", + "flot.time.junLabel": "Juin", + "flot.time.marLabel": "Mars", + "flot.time.mayLabel": "Mai", + "flot.time.monLabel": "Lun", + "flot.time.novLabel": "Nov", + "flot.time.octLabel": "Oct", + "flot.time.satLabel": "Sam", + "flot.time.sepLabel": "Sept", + "flot.time.sunLabel": "Dim", + "flot.time.thuLabel": "Jeu", + "flot.time.tueLabel": "Mar", + "flot.time.wedLabel": "Mer", + "home.addData.addDataButtonLabel": "Ajouter vos données", + "home.addData.sampleDataButtonLabel": "Utiliser un exemple de données", + "home.addData.sectionTitle": "Ajoutez vos données pour commencer", + "home.addData.text": "Vous avez plusieurs options pour commencer à exploiter vos données. Vous pouvez collecter des données à partir d'une application ou d'un service, ou bien charger un fichier. Et si vous n'êtes pas encore prêt à utiliser vos propres données, utilisez notre exemple d’ensemble de données.", + "home.breadcrumbs.homeTitle": "Accueil", + "home.dataManagementDisableCollection": " Pour mettre fin à la collecte, ", + "home.dataManagementDisableCollectionLink": "désactivez les données d'utilisation ici.", + "home.dataManagementDisclaimerPrivacy": "Pour en savoir plus sur la manière dont les données d'utilisation nous aident à gérer et à améliorer nos produits et nos services, consultez notre ", + "home.dataManagementDisclaimerPrivacyLink": "Déclaration de confidentialité.", + "home.dataManagementEnableCollection": " Pour démarrer la collecte, ", + "home.dataManagementEnableCollectionLink": "activez les données d'utilisation ici.", + "home.exploreButtonLabel": "Explorer par moi-même", + "home.exploreYourDataDescription": "Une fois toutes les étapes terminées, vous êtes prêt à explorer vos données.", + "home.header.title": "Bienvenue chez vous", + "home.letsStartDescription": "Ajoutez des données à votre cluster depuis n’importe quelle source, puis analysez-les et visualisez-les en temps réel. Utilisez nos solutions pour définir des recherches, observer votre écosystème et vous protéger contre les menaces de sécurité.", + "home.letsStartTitle": "Commencez par ajouter vos données", + "home.loadTutorials.requestFailedErrorMessage": "Échec de la requête avec le code de statut : {status}", + "home.loadTutorials.unableToLoadErrorMessage": "Impossible de charger les tutoriels", + "home.manageData.devToolsButtonLabel": "Outils de développement", + "home.manageData.sectionTitle": "Gestion", + "home.manageData.stackManagementButtonLabel": "Gestion de la suite", + "home.pageTitle": "Accueil", + "home.recentlyAccessed.recentlyViewedTitle": "Récemment consulté", + "home.sampleData.ecommerceSpec.ordersTitle": "[e-commerce] Commandes", + "home.sampleData.ecommerceSpec.promotionTrackingTitle": "[e-commerce] Suivi des promotions", + "home.sampleData.ecommerceSpec.revenueDashboardDescription": "Analyser des commandes et revenus e-commerce", + "home.sampleData.ecommerceSpec.revenueDashboardTitle": "[e-commerce] Tableau de bord des revenus", + "home.sampleData.ecommerceSpec.soldProductsPerDayTitle": "[e-commerce] Produits vendus par jour", + "home.sampleData.ecommerceSpecDescription": "Exemple de données, visualisations et tableaux de bord pour le suivi des commandes d’e-commerce.", + "home.sampleData.ecommerceSpecTitle": "Exemple de commandes d’e-commerce", + "home.sampleData.flightsSpec.airportConnectionsTitle": "[Vols] Connexions aéroportuaires (passage au-dessus d'un aéroport)", + "home.sampleData.flightsSpec.delayBucketsTitle": "[Vols] Compartiments retard", + "home.sampleData.flightsSpec.delaysAndCancellationsTitle": "[Vols] Retards et annulations", + "home.sampleData.flightsSpec.departuresCountMapTitle": "[Vols] Mappage du nombre de départs", + "home.sampleData.flightsSpec.destinationWeatherTitle": "[Vols] Météo à la destination", + "home.sampleData.flightsSpec.flightLogTitle": "[Vols] Journal de vol", + "home.sampleData.flightsSpec.globalFlightDashboardDescription": "Analyser des données aéroportuaires factices pour ES-Air, Logstash Airways, Kibana Airlines et JetBeats", + "home.sampleData.flightsSpec.globalFlightDashboardTitle": "[Vols] Tableau de bord des vols internationaux", + "home.sampleData.flightsSpecDescription": "Exemple de données, de visualisations et de tableaux de bord pour le monitoring des itinéraires de vol.", + "home.sampleData.flightsSpecTitle": "Exemple de données aéroportuaires", + "home.sampleData.logsSpec.bytesDistributionTitle": "[Logs] Distribution des octets", + "home.sampleData.logsSpec.discoverTitle": "[Logs] Visites", + "home.sampleData.logsSpec.goalsTitle": "[Logs] Objectifs", + "home.sampleData.logsSpec.heatmapTitle": "[Logs] Carte thermique des visiteurs uniques", + "home.sampleData.logsSpec.hostVisitsBytesTableTitle": "[Logs] Tableau des hôtes, visites et octets", + "home.sampleData.logsSpec.responseCodesOverTimeTitle": "[Logs] Codes de réponse sur la durée + annotations", + "home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle": "[Logs] Diagramme de Sankey source-destination", + "home.sampleData.logsSpec.visitorsMapTitle": "[Logs] Mappage des visiteurs", + "home.sampleData.logsSpec.webTrafficDescription": "Analyser des données de log factices relatives au trafic Internet du site d'Elastic", + "home.sampleData.logsSpec.webTrafficTitle": "[Logs] Trafic Internet", + "home.sampleData.logsSpecDescription": "Exemple de données, de visualisations et de tableaux de bord pour le monitoring des logs Internet.", + "home.sampleData.logsSpecTitle": "Exemple de logs Internet", + "home.sampleDataSet.installedLabel": "{name} installé", + "home.sampleDataSet.unableToInstallErrorMessage": "Impossible d'installer l'exemple d’ensemble de données : {name}.", + "home.sampleDataSet.unableToLoadListErrorMessage": "Impossible de charger la liste des exemples d’ensemble de données", + "home.sampleDataSet.unableToUninstallErrorMessage": "Impossible de désinstaller l'exemple d’ensemble de données : {name}.", + "home.sampleDataSet.uninstalledLabel": "{name} désinstallé", + "home.sampleDataSetCard.addButtonAriaLabel": "Ajouter {datasetName}", + "home.sampleDataSetCard.addButtonLabel": "Ajouter des données", + "home.sampleDataSetCard.addingButtonAriaLabel": "Ajout de {datasetName}", + "home.sampleDataSetCard.addingButtonLabel": "Ajout", + "home.sampleDataSetCard.dashboardLinkLabel": "Tableau de bord", + "home.sampleDataSetCard.default.addButtonAriaLabel": "Ajouter {datasetName}", + "home.sampleDataSetCard.default.addButtonLabel": "Ajouter des données", + "home.sampleDataSetCard.default.unableToVerifyErrorMessage": "Impossible de vérifier le statut de l'ensemble de données. Erreur : {statusMsg}.", + "home.sampleDataSetCard.removeButtonAriaLabel": "Supprimer {datasetName}", + "home.sampleDataSetCard.removeButtonLabel": "Supprimer", + "home.sampleDataSetCard.removingButtonAriaLabel": "Suppression de {datasetName}", + "home.sampleDataSetCard.removingButtonLabel": "Suppression", + "home.sampleDataSetCard.viewDataButtonAriaLabel": "Consulter {datasetName}", + "home.sampleDataSetCard.viewDataButtonLabel": "Consulter les données", + "home.solutionsSection.sectionTitle": "Choisir votre solution", + "home.tryButtonLabel": "Ajouter des données", + "home.tutorial.addDataToKibanaTitle": "Ajouter des données", + "home.tutorial.card.sampleDataDescription": "Commencez votre exploration de Kibana avec ces ensembles de données \"en un clic\".", + "home.tutorial.card.sampleDataTitle": "Exemple de données", + "home.tutorial.elasticCloudButtonLabel": "Elastic Cloud", + "home.tutorial.instruction_variant.fleet": "Elastic APM (bêta) dans Fleet", + "home.tutorial.instructionSet.checkStatusButtonLabel": "Vérifier le statut", + "home.tutorial.instructionSet.customizeLabel": "Personnaliser les extraits de code", + "home.tutorial.instructionSet.noDataLabel": "Aucune donnée trouvée", + "home.tutorial.instructionSet.statusCheckTitle": "Vérification du statut", + "home.tutorial.instructionSet.successLabel": "Réussite", + "home.tutorial.introduction.betaLabel": "Version bêta", + "home.tutorial.introduction.imageAltDescription": "Capture d'écran du tableau de bord principal.", + "home.tutorial.introduction.viewButtonLabel": "Consulter les champs exportés", + "home.tutorial.noTutorialLabel": "Tutoriel {tutorialId} introuvable", + "home.tutorial.savedObject.addedLabel": "Les objets enregistrés {savedObjectsLength} ont bien été ajoutés.", + "home.tutorial.savedObject.confirmButtonLabel": "Confirmer l'écrasement", + "home.tutorial.savedObject.defaultButtonLabel": "Charger des objets Kibana", + "home.tutorial.savedObject.installLabel": "Importe un modèle d'indexation, des visualisations et des tableaux de bord prédéfinis.", + "home.tutorial.savedObject.installStatusLabel": "{overwriteErrorsLength} sur {savedObjectsLength} objets existent déjà. Cliquez sur \"Confirmer l'écrasement\" pour importer et écraser les objets existants. Toute modification apportée aux objets sera perdue.", + "home.tutorial.savedObject.loadTitle": "Charger des objets Kibana", + "home.tutorial.savedObject.requestFailedErrorMessage": "Échec de la requête. Erreur : {message}.", + "home.tutorial.savedObject.unableToAddErrorMessage": "Impossible d'ajouter {errorsLength} sur {savedObjectsLength} objets Kibana. Erreur : {errorMessage}.", + "home.tutorial.selectionLegend": "Type de déploiement", + "home.tutorial.selfManagedButtonLabel": "Autogéré", + "home.tutorial.tabs.sampleDataTitle": "Exemple de données", + "home.tutorial.unexpectedStatusCheckStateErrorDescription": "État de vérification du statut {statusCheckState} inattendu", + "home.tutorial.unhandledInstructionTypeErrorDescription": "Type d'instructions {visibleInstructions} non pris en charge", + "home.tutorialDirectory.featureCatalogueDescription": "Importez des données à partir d'applications et de services populaires.", + "home.tutorialDirectory.featureCatalogueTitle": "Ajouter des données", + "home.tutorials.activemqLogs.artifacts.dashboards.linkLabel": "Événements d'audit ActiveMQ", + "home.tutorials.activemqLogs.longDescription": "Collectez les logs ActiveMQ avec Filebeat. [En savoir plus]({learnMoreLink}).", + "home.tutorials.activemqLogs.nameTitle": "Logs ActiveMQ", + "home.tutorials.activemqLogs.shortDescription": "Collectez les logs ActiveMQ avec Filebeat.", + "home.tutorials.activemqMetrics.artifacts.application.label": "Discover", + "home.tutorials.activemqMetrics.longDescription": "Le module Metricbeat ''activemq'' récupère les indicateurs de monitoring depuis les instances ActiveMQ. [En savoir plus]({learnMoreLink}).", + "home.tutorials.activemqMetrics.nameTitle": "Indicateurs ActiveMQ", + "home.tutorials.activemqMetrics.shortDescription": "Récupérez les indicateurs de monitoring depuis les instances ActiveMQ.", + "home.tutorials.aerospikeMetrics.artifacts.application.label": "Discover", + "home.tutorials.aerospikeMetrics.longDescription": "Le module Metricbeat ''aerospike'' récupère les indicateurs internes d’Aerospike. [En savoir plus]({learnMoreLink}).", + "home.tutorials.aerospikeMetrics.nameTitle": "Indicateurs Aerospike", + "home.tutorials.aerospikeMetrics.shortDescription": "Récupérez les indicateurs internes depuis le serveur Aerospike.", + "home.tutorials.apacheLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs Apache", + "home.tutorials.apacheLogs.longDescription": "Le module Filebeat ''apache'' analyse les logs d'accès et d'erreurs créés par le serveur HTTP Apache. [En savoir plus]({learnMoreLink}).", + "home.tutorials.apacheLogs.nameTitle": "Logs Apache", + "home.tutorials.apacheLogs.shortDescription": "Collectez et analysez les logs d'accès et d'erreurs créés par le serveur HTTP Apache.", + "home.tutorials.apacheMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Apache", + "home.tutorials.apacheMetrics.longDescription": "Le module Metricbeat ''apache'' récupère les indicateurs internes depuis le serveur HTTP Apache 2. [En savoir plus]({learnMoreLink}).", + "home.tutorials.apacheMetrics.nameTitle": "Indicateurs Apache", + "home.tutorials.apacheMetrics.shortDescription": "Récupérez les indicateurs internes depuis le serveur HTTP Apache 2.", + "home.tutorials.auditbeat.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.auditbeat.longDescription": "Utilisez Auditbeat pour collecter les données d'audit de vos hôtes. Ces données incluent les processus, utilisateurs, connexions, informations de socket, accès aux fichiers et bien plus encore. [En savoir plus]({learnMoreLink}).", + "home.tutorials.auditbeat.nameTitle": "Auditbeat", + "home.tutorials.auditbeat.shortDescription": "Collectez des données d'audit de vos hôtes.", + "home.tutorials.auditdLogs.artifacts.dashboards.linkLabel": "Événements d'audit", + "home.tutorials.auditdLogs.longDescription": "Le module collecte et analyse les logs du démon d'audit (''auditd'') [En savoir plus]({learnMoreLink}).", + "home.tutorials.auditdLogs.nameTitle": "Logs auditd", + "home.tutorials.auditdLogs.shortDescription": "Collectez les logs du démon Linux auditd.", + "home.tutorials.awsLogs.artifacts.dashboards.linkLabel": "Tableau de bord du log d'accès au serveur AWS S3", + "home.tutorials.awsLogs.longDescription": "Collectez des logs AWS en les exportant vers un compartiment S3 configuré avec la notification SQS [En savoir plus]({learnMoreLink}).", + "home.tutorials.awsLogs.nameTitle": "Logs AWS S3", + "home.tutorials.awsLogs.shortDescription": "Collectez des logs AWS à partir du compartiment S3 avec Filebeat.", + "home.tutorials.awsMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs AWS", + "home.tutorials.awsMetrics.longDescription": "Le module Metricbeat ''aws'' récupère les indicateurs de monitoring depuis les API AWS et Cloudwatch. [En savoir plus]({learnMoreLink}).", + "home.tutorials.awsMetrics.nameTitle": "Indicateurs AWS", + "home.tutorials.awsMetrics.shortDescription": "Récupérez les indicateurs de monitoring pour les instances EC2 depuis les API AWS et Cloudwatch.", + "home.tutorials.azureLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs Azure", + "home.tutorials.azureLogs.longDescription": "Le module Filebeat ''azure'' collecte les logs d’activité et d’audit Azure. [Learn more]({learnMoreLink}).", + "home.tutorials.azureLogs.nameTitle": "Logs Azure", + "home.tutorials.azureLogs.shortDescription": "Collectez les logs d’activité et d’audit Azure.", + "home.tutorials.azureMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Azure", + "home.tutorials.azureMetrics.longDescription": "Le module Metricbeat ''azure'' récupère les indicateurs de monitoring Azure. [En savoir plus]({learnMoreLink}).", + "home.tutorials.azureMetrics.nameTitle": "Indicateurs Azure", + "home.tutorials.azureMetrics.shortDescription": "Récupérez les indicateurs de monitoring Azure.", + "home.tutorials.barracudaLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.barracudaLogs.longDescription": "Ce module permet de recevoir les logs Barracuda Web Application Firewall par le biais de Syslog ou d’un fichier. [Learn more]({learnMoreLink}).", + "home.tutorials.barracudaLogs.nameTitle": "Logs Barracuda", + "home.tutorials.barracudaLogs.shortDescription": "Collectez les logs Barracuda Web Application Firewall par le biais de Syslog ou d’un fichier.", + "home.tutorials.bluecoatLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.bluecoatLogs.longDescription": "Ce module permet de recevoir les logs Blue Coat Director par le biais de Syslog ou d’un fichier. [Learn more]({learnMoreLink}).", + "home.tutorials.bluecoatLogs.nameTitle": "Logs Blue Coat Director", + "home.tutorials.bluecoatLogs.shortDescription": "Collectez les logs Blue Coat Director par le biais de Syslog ou d'un fichier.", + "home.tutorials.cefLogs.artifacts.dashboards.linkLabel": "Tableau de bord d'aperçu du réseau CEF", + "home.tutorials.cefLogs.longDescription": "Ce module permet de recevoir des données Common Event Format (CEF) par le biais de Syslog. Lorsque des messages sont reçus par le biais du protocole Syslog, l'entrée Syslog analyse l'en-tête et définit la valeur d'horodatage. Puis le processeur est appliqué pour analyser les données CEF. Les données décodées sont alors écrites dans un champ objet ''cef''. Enfin, tous les champs Elastic Common Schema (ECS) ayant des correspondances CEF sont renseignés. [En savoir plus]({learnMoreLink}).", + "home.tutorials.cefLogs.nameTitle": "Logs CEF", + "home.tutorials.cefLogs.shortDescription": "Collectez des logs Common Event Format (CEF) par le biais de Syslog.", + "home.tutorials.cephMetrics.artifacts.application.label": "Discover", + "home.tutorials.cephMetrics.longDescription": "Le module Metricbeat ''ceph'' récupère les indicateurs internes depuis Ceph. [En savoir plus]({learnMoreLink}).", + "home.tutorials.cephMetrics.nameTitle": "Indicateurs Ceph", + "home.tutorials.cephMetrics.shortDescription": "Récupérez les indicateurs internes depuis le serveur Ceph.", + "home.tutorials.checkpointLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.checkpointLogs.longDescription": "Il s'agit d'un module pour les logs de pare-feu Check Point. Il prend en charge les logs de l’exportateur de journaux au format Syslog. [Learn more]({learnMoreLink}).", + "home.tutorials.checkpointLogs.nameTitle": "Logs Check Point", + "home.tutorials.checkpointLogs.shortDescription": "Collectez des logs de pare-feu Check Point.", + "home.tutorials.ciscoLogs.artifacts.dashboards.linkLabel": "Tableau de bord de pare-feu ASA", + "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 les logs de dispositifs réseau Cisco par le biais de Syslog ou d'un fichier.", + "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.nameTitle": "Logs Cloudwatch AWS", + "home.tutorials.cloudwatchLogs.shortDescription": "Collectez les logs Cloudwatch avec Functionbeat.", + "home.tutorials.cockroachdbMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs CockroachDB", + "home.tutorials.cockroachdbMetrics.longDescription": "Le module Metricbeat ''cockroachbd'' récupère les indicateurs de monitoring depuis CockroachDB. [En savoir plus]({learnMoreLink}).", + "home.tutorials.cockroachdbMetrics.nameTitle": "Indicateurs CockroachDB", + "home.tutorials.cockroachdbMetrics.shortDescription": "Récupérez les indicateurs de monitoring depuis le serveur CockroachDB.", + "home.tutorials.common.auditbeat.cloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.auditbeat.premCloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.auditbeat.premInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.auditbeatCloudInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.auditbeatCloudInstructions.config.debTitle": "Modifier la configuration", + "home.tutorials.common.auditbeatCloudInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.auditbeatCloudInstructions.config.osxTitle": "Modifier la configuration", + "home.tutorials.common.auditbeatCloudInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "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.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.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.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.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}).", + "home.tutorials.common.auditbeatInstructions.install.debTextPre": "Vous utilisez Auditbeat pour la première fois ? Consultez le [guide de démarrage rapide]({linkUrl}).", + "home.tutorials.common.auditbeatInstructions.install.debTitle": "Télécharger et installer Auditbeat", + "home.tutorials.common.auditbeatInstructions.install.osxTextPre": "Vous utilisez Auditbeat pour la première fois ? Consultez le [guide de démarrage rapide]({linkUrl}).", + "home.tutorials.common.auditbeatInstructions.install.osxTitle": "Télécharger et installer Auditbeat", + "home.tutorials.common.auditbeatInstructions.install.rpmTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({linkUrl}).", + "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.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", + "home.tutorials.common.auditbeatInstructions.start.osxTextPre": "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.osxTitle": "Lancer Auditbeat", + "home.tutorials.common.auditbeatInstructions.start.rpmTextPre": "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.rpmTitle": "Lancer Auditbeat", + "home.tutorials.common.auditbeatInstructions.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.auditbeatInstructions.start.windowsTitle": "Lancer Auditbeat", + "home.tutorials.common.auditbeatStatusCheck.buttonLabel": "Vérifier les données", + "home.tutorials.common.auditbeatStatusCheck.errorText": "Aucune donnée n'a encore été reçue.", + "home.tutorials.common.auditbeatStatusCheck.successText": "Des données ont été reçues.", + "home.tutorials.common.auditbeatStatusCheck.text": "Vérifier que des données sont reçues d'Auditbeat", + "home.tutorials.common.auditbeatStatusCheck.title": "Statut", + "home.tutorials.common.cloudInstructions.passwordAndResetLink": "Où {passwordTemplate} est le mot de passe de l'utilisateur ''elastic''.\\{#config.cloud.profileUrl\\}\n Mot de passe oublié ? [Réinitialiser dans Elastic Cloud](\\{config.cloud.baseUrl\\}\\{config.cloud.profileUrl\\}).\n \\{/config.cloud.profileUrl\\}", + "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.filebeatCloudInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.filebeatCloudInstructions.config.debTitle": "Modifier la configuration", + "home.tutorials.common.filebeatCloudInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.filebeatCloudInstructions.config.osxTitle": "Modifier la configuration", + "home.tutorials.common.filebeatCloudInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.filebeatCloudInstructions.config.rpmTitle": "Modifier la configuration", + "home.tutorials.common.filebeatCloudInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.filebeatCloudInstructions.config.windowsTitle": "Modifier la configuration", + "home.tutorials.common.filebeatEnableInstructions.debTextPost": "Modifiez les paramètres dans le fichier ''/etc/filebeat/modules.d/{moduleName}.yml''.", + "home.tutorials.common.filebeatEnableInstructions.debTitle": "Activer et configurer le module {moduleName}", + "home.tutorials.common.filebeatEnableInstructions.osxTextPost": "Modifiez les paramètres dans le fichier ''modules.d/{moduleName}.yml''.", + "home.tutorials.common.filebeatEnableInstructions.osxTextPre": "Dans le répertoire d'installation, exécutez la commande suivante :", + "home.tutorials.common.filebeatEnableInstructions.osxTitle": "Activer et configurer le module {moduleName}", + "home.tutorials.common.filebeatEnableInstructions.rpmTextPost": "Modifiez les paramètres dans le fichier ''/etc/filebeat/modules.d/{moduleName}.yml''.", + "home.tutorials.common.filebeatEnableInstructions.rpmTitle": "Activer et configurer le module {moduleName}", + "home.tutorials.common.filebeatEnableInstructions.windowsTextPost": "Modifiez les paramètres dans le fichier ''modules.d/{moduleName}.yml''.", + "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.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.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.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.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}).", + "home.tutorials.common.filebeatInstructions.install.debTextPre": "Vous utilisez Filebeat pour la première fois ? Consultez le [guide de démarrage rapide]({linkUrl}).", + "home.tutorials.common.filebeatInstructions.install.debTitle": "Télécharger et installer Filebeat", + "home.tutorials.common.filebeatInstructions.install.osxTextPre": "Vous utilisez Filebeat pour la première fois ? Consultez le [guide de démarrage rapide]({linkUrl}).", + "home.tutorials.common.filebeatInstructions.install.osxTitle": "Télécharger et installer Filebeat", + "home.tutorials.common.filebeatInstructions.install.rpmTextPost": "Vous cherchez les packages 32 bits ? Consultez la [page de téléchargement]({linkUrl}).", + "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.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", + "home.tutorials.common.filebeatInstructions.start.osxTextPre": "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.osxTitle": "Lancer Filebeat", + "home.tutorials.common.filebeatInstructions.start.rpmTextPre": "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.rpmTitle": "Lancer Filebeat", + "home.tutorials.common.filebeatInstructions.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.filebeatInstructions.start.windowsTitle": "Lancer Filebeat", + "home.tutorials.common.filebeatStatusCheck.buttonLabel": "Vérifier les données", + "home.tutorials.common.filebeatStatusCheck.errorText": "Aucune donnée n'a encore été reçue de ce module.", + "home.tutorials.common.filebeatStatusCheck.successText": "Des données ont été reçues de ce module.", + "home.tutorials.common.filebeatStatusCheck.text": "Vérifier que des données sont reçues du module Filebeat \"{moduleName}\"", + "home.tutorials.common.filebeatStatusCheck.title": "Statut du module", + "home.tutorials.common.functionbeat.cloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.functionbeat.premCloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.functionbeat.premInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.functionbeatAWSInstructions.textPost": "Où '''' et '''' sont vos informations d'identification et ''us-east-1'' est la région désirée.", + "home.tutorials.common.functionbeatAWSInstructions.textPre": "Définissez vos informations d'identification AWS dans l'environnement :", + "home.tutorials.common.functionbeatAWSInstructions.title": "Définir des informations d'identification AWS", + "home.tutorials.common.functionbeatCloudInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.functionbeatCloudInstructions.config.osxTitle": "Modifier la configuration", + "home.tutorials.common.functionbeatCloudInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.functionbeatCloudInstructions.config.windowsTitle": "Modifier la configuration", + "home.tutorials.common.functionbeatEnableOnPremInstructions.defaultTextPost": "Où '''' est le nom du groupe de logs à importer et '''' un nom de compartiment S3 valide pour la mise en œuvre du déploiement de Functionbeat.", + "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.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.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.", + "home.tutorials.common.functionbeatInstructions.deploy.osxTitle": "Déployer Functionbeat en tant que fonction AWS Lambda", + "home.tutorials.common.functionbeatInstructions.deploy.windowsTextPre": "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.", + "home.tutorials.common.functionbeatInstructions.deploy.windowsTitle": "Déployer Functionbeat en tant que fonction AWS Lambda", + "home.tutorials.common.functionbeatInstructions.install.linuxTextPre": "Vous utilisez Functionbeat pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", + "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.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.", + "home.tutorials.common.functionbeatStatusCheck.successText": "Des données ont été reçues de Functionbeat.", + "home.tutorials.common.functionbeatStatusCheck.text": "Vérifier que des données sont reçues de Functionbeat", + "home.tutorials.common.functionbeatStatusCheck.title": "Statut de Functionbeat", + "home.tutorials.common.heartbeat.cloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.heartbeat.premCloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.heartbeat.premInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.heartbeatCloudInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.heartbeatCloudInstructions.config.debTitle": "Modifier la configuration", + "home.tutorials.common.heartbeatCloudInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.heartbeatCloudInstructions.config.osxTitle": "Modifier la configuration", + "home.tutorials.common.heartbeatCloudInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.heartbeatCloudInstructions.config.rpmTitle": "Modifier la configuration", + "home.tutorials.common.heartbeatCloudInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.heartbeatCloudInstructions.config.windowsTitle": "Modifier la configuration", + "home.tutorials.common.heartbeatEnableCloudInstructions.debTextPre": "Modifiez le paramètre ''heartbeat.monitors'' dans le fichier ''heartbeat.yml''.", + "home.tutorials.common.heartbeatEnableCloudInstructions.defaultTextPost": "Pour plus d’informations sur comment configurer des moniteurs dans Heartbeat, consultez les [documents de configuration de Heartbeat.]({configureLink})", + "home.tutorials.common.heartbeatEnableCloudInstructions.defaultTitle": "Modifier la configuration – Ajouter des moniteurs", + "home.tutorials.common.heartbeatEnableCloudInstructions.osxTextPre": "Modifiez le paramètre ''heartbeat.monitors'' dans le fichier ''heartbeat.yml''.", + "home.tutorials.common.heartbeatEnableCloudInstructions.rpmTextPre": "Modifiez le paramètre ''heartbeat.monitors'' dans le fichier ''heartbeat.yml''.", + "home.tutorials.common.heartbeatEnableCloudInstructions.windowsTextPre": "Modifiez le paramètre ''heartbeat.monitors'' dans le fichier ''heartbeat.yml''.", + "home.tutorials.common.heartbeatEnableOnPremInstructions.debTextPre": "Modifiez le paramètre ''heartbeat.monitors'' dans le fichier ''heartbeat.yml''.", + "home.tutorials.common.heartbeatEnableOnPremInstructions.defaultTextPost": "Où {hostTemplate} est l’URL monitorée. Pour plus d’informations sur comment configurer des moniteurs dans Heartbeat, consultez les [documents de configuration de Heartbeat.]({configureLink})", + "home.tutorials.common.heartbeatEnableOnPremInstructions.defaultTitle": "Modifier la configuration – Ajouter des moniteurs", + "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.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.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.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.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}).", + "home.tutorials.common.heartbeatInstructions.install.debTextPre": "Vous utilisez Heartbeat pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", + "home.tutorials.common.heartbeatInstructions.install.debTitle": "Télécharger et installer Heartbeat", + "home.tutorials.common.heartbeatInstructions.install.osxTextPre": "Vous utilisez Heartbeat pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", + "home.tutorials.common.heartbeatInstructions.install.osxTitle": "Télécharger et installer Heartbeat", + "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.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", + "home.tutorials.common.heartbeatInstructions.start.osxTextPre": "La commande ''setup'' charge le modèle d'indexation Kibana.", + "home.tutorials.common.heartbeatInstructions.start.osxTitle": "Lancer Heartbeat", + "home.tutorials.common.heartbeatInstructions.start.rpmTextPre": "La commande ''setup'' charge le modèle d'indexation Kibana.", + "home.tutorials.common.heartbeatInstructions.start.rpmTitle": "Lancer Heartbeat", + "home.tutorials.common.heartbeatInstructions.start.windowsTextPre": "La commande ''setup'' charge le modèle d'indexation Kibana.", + "home.tutorials.common.heartbeatInstructions.start.windowsTitle": "Lancer Heartbeat", + "home.tutorials.common.heartbeatStatusCheck.buttonLabel": "Vérifier les données", + "home.tutorials.common.heartbeatStatusCheck.errorText": "Aucune donnée n'a encore été reçue de Heartbeat.", + "home.tutorials.common.heartbeatStatusCheck.successText": "Des données ont été reçues de Heartbeat.", + "home.tutorials.common.heartbeatStatusCheck.text": "Vérifier que des données sont reçues de Heartbeat", + "home.tutorials.common.heartbeatStatusCheck.title": "Statut de Heartbeat", + "home.tutorials.common.logstashInstructions.install.java.osxTextPre": "Suivez les instructions d'installation [ici]({link}).", + "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.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.windowsTitle": "Télécharger et installer Logstash", + "home.tutorials.common.metricbeat.cloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.metricbeat.premCloudInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.metricbeat.premInstructions.gettingStarted.title": "Commencer", + "home.tutorials.common.metricbeatCloudInstructions.config.debTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.metricbeatCloudInstructions.config.debTitle": "Modifier la configuration", + "home.tutorials.common.metricbeatCloudInstructions.config.osxTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.metricbeatCloudInstructions.config.osxTitle": "Modifier la configuration", + "home.tutorials.common.metricbeatCloudInstructions.config.rpmTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.metricbeatCloudInstructions.config.rpmTitle": "Modifier la configuration", + "home.tutorials.common.metricbeatCloudInstructions.config.windowsTextPre": "Modifiez {path} afin de définir les informations de connexion pour Elastic Cloud :", + "home.tutorials.common.metricbeatCloudInstructions.config.windowsTitle": "Modifier la configuration", + "home.tutorials.common.metricbeatEnableInstructions.debTextPost": "Modifiez les paramètres dans le fichier ''/etc/metricbeat/modules.d/{moduleName}.yml''.", + "home.tutorials.common.metricbeatEnableInstructions.debTitle": "Activer et configurer le module {moduleName}", + "home.tutorials.common.metricbeatEnableInstructions.osxTextPost": "Modifiez les paramètres dans le fichier ''modules.d/{moduleName}.yml''.", + "home.tutorials.common.metricbeatEnableInstructions.osxTextPre": "Dans le répertoire d'installation, exécutez la commande suivante :", + "home.tutorials.common.metricbeatEnableInstructions.osxTitle": "Activer et configurer le module {moduleName}", + "home.tutorials.common.metricbeatEnableInstructions.rpmTextPost": "Modifiez les paramètres dans le fichier ''/etc/metricbeat/modules.d/{moduleName}.yml''.", + "home.tutorials.common.metricbeatEnableInstructions.rpmTitle": "Activer et configurer le module {moduleName}", + "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.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.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.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.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}).", + "home.tutorials.common.metricbeatInstructions.install.debTextPre": "Vous utilisez Metricbeat pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", + "home.tutorials.common.metricbeatInstructions.install.debTitle": "Télécharger et installer Metricbeat", + "home.tutorials.common.metricbeatInstructions.install.osxTextPre": "Vous utilisez Metricbeat pour la première fois ? Consultez le [guide de démarrage rapide]({link}).", + "home.tutorials.common.metricbeatInstructions.install.osxTitle": "Télécharger et installer Metricbeat", + "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.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", + "home.tutorials.common.metricbeatInstructions.start.osxTextPre": "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.osxTitle": "Lancer Metricbeat", + "home.tutorials.common.metricbeatInstructions.start.rpmTextPre": "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.rpmTitle": "Lancer Metricbeat", + "home.tutorials.common.metricbeatInstructions.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.metricbeatInstructions.start.windowsTitle": "Lancer Metricbeat", + "home.tutorials.common.metricbeatStatusCheck.buttonLabel": "Vérifier les données", + "home.tutorials.common.metricbeatStatusCheck.errorText": "Aucune donnée n'a encore été reçue de ce module.", + "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.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.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": "Premiers pas", + "home.tutorials.common.winlogbeat.premInstructions.gettingStarted.title": "Premiers pas", + "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.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.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", + "home.tutorials.common.winlogbeatStatusCheck.buttonLabel": "Vérifier les données", + "home.tutorials.common.winlogbeatStatusCheck.errorText": "Aucune donnée n'a encore été reçue.", + "home.tutorials.common.winlogbeatStatusCheck.successText": "Des données ont été reçues.", + "home.tutorials.common.winlogbeatStatusCheck.text": "Vérifier que des données sont reçues de Winlogbeat", + "home.tutorials.common.winlogbeatStatusCheck.title": "Statut du module", + "home.tutorials.consulMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Consul", + "home.tutorials.consulMetrics.longDescription": "Le module Metricbeat \"consul\" récupère des indicateurs de monitoring depuis Consul. [En savoir plus]({learnMoreLink}).", + "home.tutorials.consulMetrics.nameTitle": "Indicateurs Consul", + "home.tutorials.consulMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis le serveur Consul.", + "home.tutorials.corednsLogs.artifacts.dashboards.linkLabel": "Aperçu de [Filebeat CoreDNS]", + "home.tutorials.corednsLogs.longDescription": "Il s'agit d'un module Filebeat pour CoreDNS. Celui-ci prend en charge les déploiements CoreDNS autonomes et les déploiements CoreDNS dans Kubernetes. [En savoir plus]({learnMoreLink}).", + "home.tutorials.corednsLogs.nameTitle": "Logs CoreDNS", + "home.tutorials.corednsLogs.shortDescription": "Collectez les logs CoreDNS.", + "home.tutorials.corednsMetrics.artifacts.application.label": "Discover", + "home.tutorials.corednsMetrics.longDescription": "Le module Metricbeat \"coredns\" récupère des indicateurs de monitoring depuis CoreDNS. [En savoir plus]({learnMoreLink}).", + "home.tutorials.corednsMetrics.nameTitle": "Indicateurs CoreDNS", + "home.tutorials.corednsMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis le serveur CoreDNS.", + "home.tutorials.couchbaseMetrics.artifacts.application.label": "Discover", + "home.tutorials.couchbaseMetrics.longDescription": "Le module Metricbeat \"couchbase\" récupère des indicateurs internes depuis Couchbase. [En savoir plus]({learnMoreLink}).", + "home.tutorials.couchbaseMetrics.nameTitle": "Indicateurs Couchbase", + "home.tutorials.couchbaseMetrics.shortDescription": "Récupérez des indicateurs internes depuis Couchbase.", + "home.tutorials.couchdbMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs CouchDB", + "home.tutorials.couchdbMetrics.longDescription": "Le module Metricbeat \"couchdb\" récupère des indicateurs de monitoring depuis CouchDB. [En savoir plus]({learnMoreLink}).", + "home.tutorials.couchdbMetrics.nameTitle": "Indicateurs CouchDB", + "home.tutorials.couchdbMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis le serveur CouchdB.", + "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.nameTitle": "Logs CrowdStrike", + "home.tutorials.crowdstrikeLogs.shortDescription": "Collectez des logs CrowdStrike Falcon à l'aide du connecteur SIEM Falcon.", + "home.tutorials.cylanceLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.cylanceLogs.longDescription": "Ce module permet de recevoir des logs CylancePROTECT par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.cylanceLogs.nameTitle": "Logs CylancePROTECT", + "home.tutorials.cylanceLogs.shortDescription": "Collectez des logs CylancePROTECT par le biais de Syslog ou d’un fichier.", + "home.tutorials.dockerMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Docker", + "home.tutorials.dockerMetrics.longDescription": "Le module Metricbeat \"docker\" récupère des indicateurs depuis le serveur Docker. [En savoir plus]({learnMoreLink}).", + "home.tutorials.dockerMetrics.nameTitle": "Indicateurs Docker", + "home.tutorials.dockerMetrics.shortDescription": "Récupérez des indicateurs concernant vos conteneurs Docker.", + "home.tutorials.dropwizardMetrics.artifacts.application.label": "Discover", + "home.tutorials.dropwizardMetrics.longDescription": "Le module Metricbeat \"dropwizard\" récupère des indicateurs internes depuis l'application Java Dropwizard. [En savoir plus]({learnMoreLink}).", + "home.tutorials.dropwizardMetrics.nameTitle": "Indicateurs Dropwizard", + "home.tutorials.dropwizardMetrics.shortDescription": "Récupérez des indicateurs internes depuis l'application Java Dropwizard.", + "home.tutorials.elasticsearchLogs.artifacts.application.label": "Discover", + "home.tutorials.elasticsearchLogs.longDescription": "Le module Filebeat \"elasticsearch\" analyse les logs créés par Elasticsearch. [En savoir plus]({learnMoreLink}).", + "home.tutorials.elasticsearchLogs.nameTitle": "Logs Elasticsearch", + "home.tutorials.elasticsearchLogs.shortDescription": "Collectez et analysez les logs créés par Elasticsearch.", + "home.tutorials.elasticsearchMetrics.artifacts.application.label": "Discover", + "home.tutorials.elasticsearchMetrics.longDescription": "Le module Metricbeat \"elasticsearch\" récupère des indicateurs internes depuis Elasticsearch. [En savoir plus]({learnMoreLink}).", + "home.tutorials.elasticsearchMetrics.nameTitle": "Indicateurs Elasticsearch", + "home.tutorials.elasticsearchMetrics.shortDescription": "Récupérez des indicateurs internes depuis Elasticsearch.", + "home.tutorials.envoyproxyLogs.artifacts.dashboards.linkLabel": "Aperçu d'Envoy Proxy", + "home.tutorials.envoyproxyLogs.longDescription": "Il s'agit d'un module Filebeat pour le log d'accès à Envoy Proxy (https://www.envoyproxy.io/docs/envoy/v1.10.0/configuration/access_log). Celui-ci prend en charge les déploiements autonomes et les déploiements Envoy Proxy dans Kubernetes. [Learn more]({learnMoreLink}).", + "home.tutorials.envoyproxyLogs.nameTitle": "Logs Envoy Proxy", + "home.tutorials.envoyproxyLogs.shortDescription": "Collectez des logs Envoy Proxy.", + "home.tutorials.envoyproxyMetrics.longDescription": "Le module Metricbeat \"envoyproxy\" récupère des indicateurs de monitoring depuis Envoy Proxy. [En savoir plus]({learnMoreLink}).", + "home.tutorials.envoyproxyMetrics.nameTitle": "Indicateurs Envoy Proxy", + "home.tutorials.envoyproxyMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis Envoy Proxy.", + "home.tutorials.etcdMetrics.artifacts.application.label": "Discover", + "home.tutorials.etcdMetrics.longDescription": "Le module Metricbeat \"etcd\" récupère des indicateurs internes depuis Etcd. [En savoir plus]({learnMoreLink}).", + "home.tutorials.etcdMetrics.nameTitle": "Indicateurs Etcd", + "home.tutorials.etcdMetrics.shortDescription": "Récupérez des indicateurs internes depuis le serveur Etcd.", + "home.tutorials.f5Logs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.f5Logs.longDescription": "Ce module permet de recevoir des logs Big-IP Access Policy Manager par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.f5Logs.nameTitle": "Logs F5", + "home.tutorials.f5Logs.shortDescription": "Collectez des logs F5 Big-IP Access Policy Manager par le biais de Syslog ou d’un fichier.", + "home.tutorials.fortinetLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.fortinetLogs.longDescription": "Il s'agit d'un module pour les logs Fortinet FortiOS envoyés au format Syslog. [En savoir plus]({learnMoreLink}).", + "home.tutorials.fortinetLogs.nameTitle": "Logs Fortinet", + "home.tutorials.fortinetLogs.shortDescription": "Collectez des logs Fortinet FortiOS par le biais de Syslog.", + "home.tutorials.gcpLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs d'audit", + "home.tutorials.gcpLogs.longDescription": "Il s'agit d'un module pour les logs Google Cloud. Il prend en charge la lecture des logs d'audit, de flux VPC et de pare-feu qui ont été exportés depuis Stackdriver dans un récepteur de rubriques Google Pub/Sub. [En savoir plus]({learnMoreLink}).", + "home.tutorials.gcpLogs.nameTitle": "Logs Google Cloud", + "home.tutorials.gcpLogs.shortDescription": "Collectez des logs d'audit, de pare-feu et de flux VPC Google Cloud.", + "home.tutorials.gcpMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Google Cloud", + "home.tutorials.gcpMetrics.longDescription": "Le module Metricbeat \"gcp\" récupère des indicateurs de monitoring depuis Google Cloud Platform à l'aide de l'API de monitoring Stackdriver. [En savoir plus]({learnMoreLink}).", + "home.tutorials.gcpMetrics.nameTitle": "Indicateurs Google Cloud", + "home.tutorials.gcpMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis Google Cloud Platform à l'aide de l'API de monitoring Stackdriver.", + "home.tutorials.golangMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Golang", + "home.tutorials.golangMetrics.longDescription": "Le module Metricbeat \"{moduleName}\" récupère des indicateurs internes depuis une application Golang. [En savoir plus]({learnMoreLink}).", + "home.tutorials.golangMetrics.nameTitle": "Indicateurs Golang", + "home.tutorials.golangMetrics.shortDescription": "Récupérez des indicateurs internes depuis une application Golang.", + "home.tutorials.gsuiteLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.gsuiteLogs.longDescription": "Il s'agit d'un module pour l'ingestion de données depuis les différentes API de rapports d'audit GSuite. [En savoir plus]({learnMoreLink}).", + "home.tutorials.gsuiteLogs.nameTitle": "Logs GSuite", + "home.tutorials.gsuiteLogs.shortDescription": "Collectez des rapports d'activité GSuite.", + "home.tutorials.haproxyLogs.artifacts.dashboards.linkLabel": "Aperçu de HAProxy", + "home.tutorials.haproxyLogs.longDescription": "Le module collecte et analyse les logs d'un processus (\"haproxy\") [En savoir plus]({learnMoreLink}).", + "home.tutorials.haproxyLogs.nameTitle": "Logs HAProxy", + "home.tutorials.haproxyLogs.shortDescription": "Collectez des logs HAProxy.", + "home.tutorials.haproxyMetrics.artifacts.application.label": "Discover", + "home.tutorials.haproxyMetrics.longDescription": "Le module Metricbeat \"haproxy\" récupère des indicateurs internes depuis HAProxy. [En savoir plus]({learnMoreLink}).", + "home.tutorials.haproxyMetrics.nameTitle": "Indicateurs HAProxy", + "home.tutorials.haproxyMetrics.shortDescription": "Récupérez des indicateurs internes depuis le serveur HAProxy.", + "home.tutorials.ibmmqLogs.artifacts.dashboards.linkLabel": "Événements IBM MQ", + "home.tutorials.ibmmqLogs.longDescription": "Collectez des logs IBM MQ avec Filebeat. [En savoir plus]({learnMoreLink}).", + "home.tutorials.ibmmqLogs.nameTitle": "Logs IBM MQ", + "home.tutorials.ibmmqLogs.shortDescription": "Collectez des logs IBM MQ avec Filebeat.", + "home.tutorials.ibmmqMetrics.artifacts.application.label": "Discover", + "home.tutorials.ibmmqMetrics.longDescription": "Le module Metricbeat \"ibmmq\" récupère des indicateurs de monitoring depuis les instances IBM MQ. [En savoir plus]({learnMoreLink}).", + "home.tutorials.ibmmqMetrics.nameTitle": "Indicateurs IBM MQ", + "home.tutorials.ibmmqMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis les instances IBM MQ.", + "home.tutorials.icingaLogs.artifacts.dashboards.linkLabel": "Log principal Icinga", + "home.tutorials.icingaLogs.longDescription": "Le module analyse le log principal et les logs de débogage et de démarrage d'[Icinga](https://www.icinga.com/products/icinga-2/). [En savoir plus]({learnMoreLink}).", + "home.tutorials.icingaLogs.nameTitle": "Logs Icinga", + "home.tutorials.icingaLogs.shortDescription": "Collectez le log principal et les logs de débogage et de démarrage d'Icinga.", + "home.tutorials.iisLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs IIS", + "home.tutorials.iisLogs.longDescription": "Le module Filebeat \"iis\" analyse les logs d'accès et d'erreurs créés par le serveur HTTP IIS. [En savoir plus]({learnMoreLink}).", + "home.tutorials.iisLogs.nameTitle": "Logs IIS", + "home.tutorials.iisLogs.shortDescription": "Collectez et analysez les logs d'accès et d'erreurs créés par le serveur HTTP IIS.", + "home.tutorials.iisMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs IIS", + "home.tutorials.iisMetrics.longDescription": "Le module Metricbeat \"iis\" collecte les indicateurs du serveur IIS ainsi que des sites web et des pools d'applications en cours d'exécution. [En savoir plus]({learnMoreLink}).", + "home.tutorials.iisMetrics.nameTitle": "Indicateurs IIS", + "home.tutorials.iisMetrics.shortDescription": "Collectez les indicateurs en lien avec le serveur IIS.", + "home.tutorials.impervaLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.impervaLogs.longDescription": "Ce module permet de recevoir des logs Imperva SecureSphere par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.impervaLogs.nameTitle": "Logs Imperva", + "home.tutorials.impervaLogs.shortDescription": "Collectez des logs Imperva SecureSphere par le biais de Syslog ou d’un fichier.", + "home.tutorials.infobloxLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.infobloxLogs.longDescription": "Ce module permet de recevoir des logs Infoblox NIOS par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.infobloxLogs.nameTitle": "Logs Infoblox", + "home.tutorials.infobloxLogs.shortDescription": "Collectez des logs Infoblox NIOS par le biais de Syslog ou d’un fichier.", + "home.tutorials.iptablesLogs.artifacts.dashboards.linkLabel": "Aperçu d'Iptables", + "home.tutorials.iptablesLogs.longDescription": "Il s'agit d'un module pour les logs iptables et ip6tables. Il analyse les logs reçus via le réseau par le biais de Syslog ou d’un fichier. En outre, il comprend le préfixe ajouté par certains pare-feux Ubiquiti qui contient le nom de l'ensemble de règles, le numéro de règle et l'action effectuée sur le trafic (autoriser/refuser). [En savoir plus]({learnMoreLink}).", + "home.tutorials.iptablesLogs.nameTitle": "Logs Iptables", + "home.tutorials.iptablesLogs.shortDescription": "Collectez des logs iptables et ip6tables.", + "home.tutorials.juniperLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.juniperLogs.longDescription": "Ce module permet de recevoir des logs Juniper JUNOS par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.juniperLogs.nameTitle": "Logs Juniper", + "home.tutorials.juniperLogs.shortDescription": "Collectez des logs Juniper JUNOS par le biais de Syslog ou d’un fichier.", + "home.tutorials.kafkaLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs Kafka", + "home.tutorials.kafkaLogs.longDescription": "Le module Filebeat \"kafka\" analyse les logs créés par Kafka. [En savoir plus]({learnMoreLink}).", + "home.tutorials.kafkaLogs.nameTitle": "Logs Kafka", + "home.tutorials.kafkaLogs.shortDescription": "Collectez et analysez les logs créés par Kafka.", + "home.tutorials.kafkaMetrics.artifacts.application.label": "Discover", + "home.tutorials.kafkaMetrics.longDescription": "Le module Metricbeat \"kafka\" récupère des indicateurs internes depuis Kafka. [En savoir plus]({learnMoreLink}).", + "home.tutorials.kafkaMetrics.nameTitle": "Indicateurs Kafka", + "home.tutorials.kafkaMetrics.shortDescription": "Récupérez des indicateurs internes depuis le serveur Kafka.", + "home.tutorials.kibanaLogs.artifacts.application.label": "Discover", + "home.tutorials.kibanaLogs.longDescription": "Il s'agit du module Kibana. [En savoir plus]({learnMoreLink}).", + "home.tutorials.kibanaLogs.nameTitle": "Logs Kibana", + "home.tutorials.kibanaLogs.shortDescription": "Collectez des logs Kibana.", + "home.tutorials.kibanaMetrics.artifacts.application.label": "Discover", + "home.tutorials.kibanaMetrics.longDescription": "Le module Metricbeat \"kibana\" récupère des indicateurs internes depuis Kibana. [En savoir plus]({learnMoreLink}).", + "home.tutorials.kibanaMetrics.nameTitle": "Indicateurs Kibana", + "home.tutorials.kibanaMetrics.shortDescription": "Récupérez des indicateurs internes depuis Kibana.", + "home.tutorials.kubernetesMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Kubernetes", + "home.tutorials.kubernetesMetrics.longDescription": "Le module Metricbeat \"kubernetes\" récupère des indicateurs depuis les API Kubernetes. [En savoir plus]({learnMoreLink}).", + "home.tutorials.kubernetesMetrics.nameTitle": "Indicateurs Kubernetes", + "home.tutorials.kubernetesMetrics.shortDescription": "Récupérez des indicateurs depuis votre installation Kubernetes.", + "home.tutorials.logstashLogs.artifacts.dashboards.linkLabel": "Logs Logstash", + "home.tutorials.logstashLogs.longDescription": "Le module analyse les logs standard et le log de requêtes lentes Logstash. Il prend en charge les formats texte brut et JSON. [En savoir plus]({learnMoreLink}).", + "home.tutorials.logstashLogs.nameTitle": "Logs Logstash", + "home.tutorials.logstashLogs.shortDescription": "Collectez le log principal et le log de requêtes lentes Logstash.", + "home.tutorials.logstashMetrics.artifacts.application.label": "Discover", + "home.tutorials.logstashMetrics.longDescription": "Le module Metricbeat \"{moduleName}\" récupère des indicateurs internes depuis un serveur Logstash. [En savoir plus]({learnMoreLink}).", + "home.tutorials.logstashMetrics.nameTitle": "Indicateurs Logstash", + "home.tutorials.logstashMetrics.shortDescription": "Récupérez des indicateurs internes depuis un serveur Logstash.", + "home.tutorials.memcachedMetrics.artifacts.application.label": "Discover", + "home.tutorials.memcachedMetrics.longDescription": "Le module Metricbeat \"memcached\" récupère des indicateurs internes depuis Memcached. [En savoir plus]({learnMoreLink}).", + "home.tutorials.memcachedMetrics.nameTitle": "Indicateurs Memcached", + "home.tutorials.memcachedMetrics.shortDescription": "Récupérez des indicateurs internes depuis le serveur Memcached.", + "home.tutorials.microsoftLogs.artifacts.dashboards.linkLabel": "Aperçu de Microsoft ATP", + "home.tutorials.microsoftLogs.longDescription": "Collectez des alertes Microsoft Defender ATP pour les utiliser avec Elastic Security [En savoir plus]({learnMoreLink}).", + "home.tutorials.microsoftLogs.nameTitle": "Logs Microsoft Defender ATP", + "home.tutorials.microsoftLogs.shortDescription": "Collectez des alertes Microsoft Defender ATP.", + "home.tutorials.mispLogs.artifacts.dashboards.linkLabel": "Aperçu de MISP", + "home.tutorials.mispLogs.longDescription": "Il s'agit d'un module Filebeat pour la lecture des informations de Threat Intelligence depuis la plateforme MISP (https://www.circl.lu/doc/misp/). Il utilise l'entrée httpjson pour accéder à l'interface d'API REST MISP. [En savoir plus]({learnMoreLink}).", + "home.tutorials.mispLogs.nameTitle": "Logs de Threat Intelligence MISP", + "home.tutorials.mispLogs.shortDescription": "Collectez des données de Threat Intelligence MISP avec Filebeat.", + "home.tutorials.mongodbLogs.artifacts.dashboards.linkLabel": "Aperçu de MongoDB", + "home.tutorials.mongodbLogs.longDescription": "Le module collecte et analyse les logs créés par [MongoDB](https://www.mongodb.com/). [En savoir plus]({learnMoreLink}).", + "home.tutorials.mongodbLogs.nameTitle": "Logs MongoDB", + "home.tutorials.mongodbLogs.shortDescription": "Collectez des logs MongoDB.", + "home.tutorials.mongodbMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs MongoDB", + "home.tutorials.mongodbMetrics.longDescription": "Le module Metricbeat \"mongodb\" récupère des indicateurs internes depuis le serveur MongoDB. [En savoir plus]({learnMoreLink}).", + "home.tutorials.mongodbMetrics.nameTitle": "Indicateurs MongoDB", + "home.tutorials.mongodbMetrics.shortDescription": "Récupérez des indicateurs internes depuis MongoDB.", + "home.tutorials.mssqlLogs.artifacts.application.label": "Discover", + "home.tutorials.mssqlLogs.longDescription": "Le module analyse les logs d'erreurs créés par MSSQL. [En savoir plus]({learnMoreLink}).", + "home.tutorials.mssqlLogs.nameTitle": "Logs MSSQL", + "home.tutorials.mssqlLogs.shortDescription": "Collectez des logs MSSQL.", + "home.tutorials.mssqlMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Microsoft SQL Server", + "home.tutorials.mssqlMetrics.longDescription": "Le module Metricbeat \"mssql\" récupère des indicateurs de monitoring, de logs et de performances depuis une instance Microsoft SQL Server. [En savoir plus]({learnMoreLink}).", + "home.tutorials.mssqlMetrics.nameTitle": "Indicateurs Microsoft SQL Server", + "home.tutorials.mssqlMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis une instance Microsoft SQL Server.", + "home.tutorials.muninMetrics.artifacts.application.label": "Discover", + "home.tutorials.muninMetrics.longDescription": "Le module Metricbeat \"munin\" récupère des indicateurs internes depuis Munin. [En savoir plus]({learnMoreLink}).", + "home.tutorials.muninMetrics.nameTitle": "Indicateurs Munin", + "home.tutorials.muninMetrics.shortDescription": "Récupérez des indicateurs internes depuis le serveur Munin.", + "home.tutorials.mysqlLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs MySQL", + "home.tutorials.mysqlLogs.longDescription": "Le module Filebeat \"mysql\" analyse les logs d'erreurs et de requêtes lentes créés par MySQL. [En savoir plus]({learnMoreLink}).", + "home.tutorials.mysqlLogs.nameTitle": "Logs MySQL", + "home.tutorials.mysqlLogs.shortDescription": "Collectez et analysez les logs d'erreurs et de requêtes lentes créés par MySQL.", + "home.tutorials.mysqlMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs MySQL", + "home.tutorials.mysqlMetrics.longDescription": "Le module Metricbeat \"mysql\" récupère des indicateurs internes depuis le serveur MySQL. [En savoir plus]({learnMoreLink}).", + "home.tutorials.mysqlMetrics.nameTitle": "Indicateurs MySQL", + "home.tutorials.mysqlMetrics.shortDescription": "Récupérez des indicateurs internes depuis MySQL.", + "home.tutorials.natsLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs NATS", + "home.tutorials.natsLogs.longDescription": "Le module Filebeat \"nats\" analyse les logs créés par NATS. [En savoir plus]({learnMoreLink}).", + "home.tutorials.natsLogs.nameTitle": "Logs NATS", + "home.tutorials.natsLogs.shortDescription": "Collectez et analysez les logs créés par NATS.", + "home.tutorials.natsMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs NATS", + "home.tutorials.natsMetrics.longDescription": "Le module Metricbeat \"nats\" récupère des indicateurs de monitoring depuis NATS. [En savoir plus]({learnMoreLink}).", + "home.tutorials.natsMetrics.nameTitle": "Indicateurs NATS", + "home.tutorials.natsMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis le serveur NATS.", + "home.tutorials.netflowLogs.artifacts.dashboards.linkLabel": "Aperçu de Netflow", + "home.tutorials.netflowLogs.longDescription": "Ce module permet de recevoir des enregistrements de flux NetFlow et IPFIX via UDP. Cette entrée prend en charge les versions 1, 5, 6, 7, 8 et 9 de NetFlow ainsi qu'IPFIX. Pour les versions de NetFlow antérieures à la version 9, les champs sont automatiquement mappés vers NetFlow v9. [En savoir plus]({learnMoreLink})", + "home.tutorials.netflowLogs.nameTitle": "Collecteur IPFIX/NetFlow", + "home.tutorials.netflowLogs.shortDescription": "Collectez des enregistrements de flux NetFlow et IPFIX.", + "home.tutorials.netscoutLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.netscoutLogs.longDescription": "Ce module permet de recevoir des logs Arbor Peakflow SP par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.netscoutLogs.nameTitle": "Logs Arbor Peakflow", + "home.tutorials.netscoutLogs.shortDescription": "Collectez des logs Netscout Arbor Peakflow SP par le biais de Syslog ou d’un fichier.", + "home.tutorials.nginxLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs Nginx", + "home.tutorials.nginxLogs.longDescription": "Le module Filebeat \"nginx\" analyse les logs d'accès et d'erreurs créés par le serveur HTTP Nginx. [En savoir plus]({learnMoreLink}).", + "home.tutorials.nginxLogs.nameTitle": "Logs Nginx", + "home.tutorials.nginxLogs.shortDescription": "Collectez et analysez les logs d'accès et d'erreurs créés par le serveur HTTP Nginx.", + "home.tutorials.nginxMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Nginx", + "home.tutorials.nginxMetrics.longDescription": "Le module Metricbeat \"nginx\" récupère des indicateurs internes depuis le serveur HTTP Nginx. Le module récupère les données de statut du serveur depuis la page web générée par {statusModuleLink}, qui doit être activé dans votre installation Nginx. [En savoir plus]({learnMoreLink}).", + "home.tutorials.nginxMetrics.nameTitle": "Indicateurs Nginx", + "home.tutorials.nginxMetrics.shortDescription": "Récupérez des indicateurs internes depuis le serveur HTTP Nginx.", + "home.tutorials.o365Logs.artifacts.dashboards.linkLabel": "Tableau de bord des audits O365", + "home.tutorials.o365Logs.longDescription": "Il s'agit d'un module pour les logs Office 365 reçus via l'un des points de terminaison d'API Office 365. Actuellement, il prend en charge les actions et les événements utilisateur, administrateur, système et de politique depuis les logs d’activité Office 365 et Azure AD exposés par l'API d’activité de gestion Office 365. [En savoir plus]({learnMoreLink}).", + "home.tutorials.o365Logs.nameTitle": "Logs Office 365", + "home.tutorials.o365Logs.shortDescription": "Collectez les logs d'activité Office 365 via l'API Office 365.", + "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.nameTitle": "Logs Okta", + "home.tutorials.oktaLogs.shortDescription": "Collectez le log système Okta via l'API Okta.", + "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}).", + "home.tutorials.openmetricsMetrics.nameTitle": "Indicateurs OpenMetrics", + "home.tutorials.openmetricsMetrics.shortDescription": "Récupérez des indicateurs depuis un point de terminaison fournissant des indicateurs au format OpenMetrics.", + "home.tutorials.oracleMetrics.artifacts.application.label": "Discover", + "home.tutorials.oracleMetrics.longDescription": "Le module Metricbeat \"{moduleName}\" récupère des indicateurs internes depuis un serveur Oracle. [En savoir plus]({learnMoreLink}).", + "home.tutorials.oracleMetrics.nameTitle": "Indicateurs Oracle", + "home.tutorials.oracleMetrics.shortDescription": "Récupérez des indicateurs internes depuis un serveur Oracle.", + "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.nameTitle": "Logs osquery", + "home.tutorials.osqueryLogs.shortDescription": "Collectez des logs osquery au format JSON.", + "home.tutorials.panwLogs.artifacts.dashboards.linkLabel": "Flux de réseau PANW", + "home.tutorials.panwLogs.longDescription": "Il s'agit d'un module pour les logs de monitoring des pare-feux Palo Alto Networks PAN-OS reçus par le biais de Syslog ou lus depuis un fichier. Actuellement, il prend en charge les messages de type Trafic et Menaces. [En savoir plus]({learnMoreLink}).", + "home.tutorials.panwLogs.nameTitle": "Logs Palo Alto Networks PAN-OS", + "home.tutorials.panwLogs.shortDescription": "Collectez des logs Palo Alto Networks PAN-OS relatifs aux menaces et au trafic par le biais de Syslog ou d’un fichier log.", + "home.tutorials.phpFpmMetrics.longDescription": "Le module Metricbeat \"php_fpm\" récupère des indicateurs internes depuis le serveur PHP-FPM. [En savoir plus]({learnMoreLink}).", + "home.tutorials.phpFpmMetrics.nameTitle": "Indicateurs PHP-FPM", + "home.tutorials.phpFpmMetrics.shortDescription": "Récupérez des indicateurs internes depuis PHP-FPM.", + "home.tutorials.postgresqlLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs PostgreSQL", + "home.tutorials.postgresqlLogs.longDescription": "Le module Filebeat \"postgresql\" analyse les logs d'erreurs et de requêtes lentes créés par PostgreSQL. [En savoir plus]({learnMoreLink}).", + "home.tutorials.postgresqlLogs.nameTitle": "Logs PostgreSQL", + "home.tutorials.postgresqlLogs.shortDescription": "Collectez et analysez les logs d'erreurs et de requêtes lentes créés par PostgreSQL.", + "home.tutorials.postgresqlMetrics.longDescription": "Le module Metricbeat \"postgresql\" récupère des indicateurs internes depuis le serveur PostgreSQL. [En savoir plus]({learnMoreLink}).", + "home.tutorials.postgresqlMetrics.nameTitle": "Indicateurs PostgreSQL", + "home.tutorials.postgresqlMetrics.shortDescription": "Récupérez des indicateurs internes depuis PostgreSQL.", + "home.tutorials.prometheusMetrics.artifacts.application.label": "Discover", + "home.tutorials.prometheusMetrics.longDescription": "Le module Metricbeat \"{moduleName}\" récupère des indicateurs depuis le point de terminaison Prometheus. [En savoir plus]({learnMoreLink}).", + "home.tutorials.prometheusMetrics.nameTitle": "Indicateurs Prometheus", + "home.tutorials.prometheusMetrics.shortDescription": "Récupérez des indicateurs depuis un exportateur Prometheus.", + "home.tutorials.rabbitmqLogs.artifacts.application.label": "Discover", + "home.tutorials.rabbitmqLogs.longDescription": "Ce module permet d'analyser les [fichiers log RabbitMQ](https://www.rabbitmq.com/logging.html). [En savoir plus]({learnMoreLink}).", + "home.tutorials.rabbitmqLogs.nameTitle": "Logs RabbitMQ", + "home.tutorials.rabbitmqLogs.shortDescription": "Collectez des logs RabbitMQ.", + "home.tutorials.rabbitmqMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs RabbitMQ", + "home.tutorials.rabbitmqMetrics.longDescription": "Le module Metricbeat \"rabbitmq\" récupère des indicateurs internes depuis le serveur RabbitMQ. [En savoir plus]({learnMoreLink}).", + "home.tutorials.rabbitmqMetrics.nameTitle": "Indicateurs RabbitMQ", + "home.tutorials.rabbitmqMetrics.shortDescription": "Récupérez des indicateurs internes depuis le serveur RabbitMQ.", + "home.tutorials.radwareLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.radwareLogs.longDescription": "Ce module permet de recevoir des logs Radware DefensePro par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.radwareLogs.nameTitle": "Logs Radware DefensePro", + "home.tutorials.radwareLogs.shortDescription": "Collectez des logs Radware DefensePro par le biais de Syslog ou d’un fichier.", + "home.tutorials.redisenterpriseMetrics.artifacts.application.label": "Discover", + "home.tutorials.redisenterpriseMetrics.longDescription": "Le module Metricbeat \"redisenterprise\" récupère des indicateurs de monitoring depuis le serveur Redis Enterprise. [En savoir plus]({learnMoreLink}).", + "home.tutorials.redisenterpriseMetrics.nameTitle": "Indicateurs Redis Enterprise", + "home.tutorials.redisenterpriseMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis le serveur Redis Enterprise.", + "home.tutorials.redisLogs.artifacts.dashboards.linkLabel": "Tableau de bord des logs Redis", + "home.tutorials.redisLogs.longDescription": "Le module Filebeat \"redis\" analyse les logs d'erreurs et de requêtes lentes créés par Redis. Pour que Redis écrive des logs d'erreurs, assurez-vous que l'option \"logfile\" est définie sur \"redis-server.log\" dans le fichier de configuration Redis. Les logs de requêtes lentes sont lus directement depuis Redis via la commande \"SLOWLOG\". Pour que Redis enregistre des logs de requêtes lentes, assurez-vous que l'option \"slowlog-log-slower-than\" est activée. Notez que l'ensemble de fichiers \"slowlog\" est expérimental. [En savoir plus]({learnMoreLink}).", + "home.tutorials.redisLogs.nameTitle": "Logs Redis", + "home.tutorials.redisLogs.shortDescription": "Collectez et analysez les logs d'erreurs et de requêtes lentes créés par Redis.", + "home.tutorials.redisMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Redis", + "home.tutorials.redisMetrics.longDescription": "Le module Metricbeat \"redis\" récupère des indicateurs internes depuis le serveur Redis. [En savoir plus]({learnMoreLink}).", + "home.tutorials.redisMetrics.nameTitle": "Indicateurs Redis", + "home.tutorials.redisMetrics.shortDescription": "Récupérez des indicateurs internes depuis Redis.", + "home.tutorials.santaLogs.artifacts.dashboards.linkLabel": "Aperçu de Santa", + "home.tutorials.santaLogs.longDescription": "Le module collecte et analyse les logs de [Google Santa](https://github.com/google/santa), un outil de sécurité pour macOS qui monitore les exécutions de processus et est capable de mettre en liste noire/blanche des fichiers binaires. [En savoir plus]({learnMoreLink}).", + "home.tutorials.santaLogs.nameTitle": "Logs Google Santa", + "home.tutorials.santaLogs.shortDescription": "Collectez des logs Google Santa relatifs aux exécutions de processus sur MacOS.", + "home.tutorials.sonicwallLogs.longDescription": "Ce module permet de recevoir des logs Sonicwall FW par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.sonicwallLogs.nameTitle": "Logs Sonicwall FW", + "home.tutorials.sonicwallLogs.shortDescription": "Collectez des logs Sonicwall FW par le biais de Syslog ou d’un fichier.", + "home.tutorials.sophosLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.sophosLogs.longDescription": "Il s'agit d'un module pour les produits Sophos. Actuellement, il prend en charge les logs XG SFOS envoyés au format Syslog. [En savoir plus]({learnMoreLink}).", + "home.tutorials.sophosLogs.nameTitle": "Logs Sophos", + "home.tutorials.sophosLogs.shortDescription": "Collectez des logs Sophos XG SFOS par le biais de Syslog.", + "home.tutorials.squidLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.squidLogs.longDescription": "Ce module permet de recevoir des logs Squid par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.squidLogs.nameTitle": "Logs Squid", + "home.tutorials.squidLogs.shortDescription": "Collectez des logs Squid par le biais de Syslog ou d’un fichier.", + "home.tutorials.stanMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs Stan", + "home.tutorials.stanMetrics.longDescription": "Le module Metricbeat \"stan\" récupère des indicateurs de monitoring depuis STAN. [En savoir plus]({learnMoreLink}).", + "home.tutorials.stanMetrics.nameTitle": "Indicateurs STAN", + "home.tutorials.stanMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis le serveur STAN.", + "home.tutorials.statsdMetrics.longDescription": "Le module Metricbeat \"statsd\" récupère des indicateurs de monitoring depuis statsd. [En savoir plus]({learnMoreLink}).", + "home.tutorials.statsdMetrics.nameTitle": "Indicateurs statsd", + "home.tutorials.statsdMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis statsd.", + "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.nameTitle": "Logs Suricata", + "home.tutorials.suricataLogs.shortDescription": "Collectez des logs IDS/IPS/NSM Suricata.", + "home.tutorials.systemLogs.artifacts.dashboards.linkLabel": "Tableau de bord Syslog système", + "home.tutorials.systemLogs.longDescription": "Le module collecte et analyse les logs créés par le service de logging système des distributions basées sur Unix/Linux communes. [En savoir plus]({learnMoreLink}).", + "home.tutorials.systemLogs.nameTitle": "Logs système", + "home.tutorials.systemLogs.shortDescription": "Collectez des logs système des distributions basées sur Unix/Linux communes.", + "home.tutorials.systemMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs système", + "home.tutorials.systemMetrics.longDescription": "Le module Metricbeat \"system\" collecte des statistiques relatives au CPU, à la mémoire, au réseau et au disque depuis l'hôte. Il collecte des statistiques au niveau du système et des statistiques par processus et système de fichiers. [En savoir plus]({learnMoreLink}).", + "home.tutorials.systemMetrics.nameTitle": "Indicateurs système", + "home.tutorials.systemMetrics.shortDescription": "Collectez des statistiques relatives au CPU, à la mémoire, au réseau et au disque depuis l'hôte.", + "home.tutorials.tomcatLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.tomcatLogs.longDescription": "Ce module permet de recevoir des logs Apache Tomcat par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.tomcatLogs.nameTitle": "Logs Tomcat", + "home.tutorials.tomcatLogs.shortDescription": "Collectez des logs Apache Tomcat par le biais de Syslog ou d’un fichier.", + "home.tutorials.traefikLogs.artifacts.dashboards.linkLabel": "Logs d'accès Traefik", + "home.tutorials.traefikLogs.longDescription": "Le module analyse les logs d'accès créés par [Traefik](https://traefik.io/). [En savoir plus]({learnMoreLink}).", + "home.tutorials.traefikLogs.nameTitle": "Logs Traefik", + "home.tutorials.traefikLogs.shortDescription": "Collectez des logs d'accès Traefik.", + "home.tutorials.traefikMetrics.longDescription": "Le module Metricbeat \"traefik\" récupère des indicateurs de monitoring depuis Traefik. [En savoir plus]({learnMoreLink}).", + "home.tutorials.traefikMetrics.nameTitle": "Indicateurs Traefik", + "home.tutorials.traefikMetrics.shortDescription": "Récupérez des indicateurs de monitoring depuis Traefik.", + "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.nameTitle": "Monitorings Uptime", + "home.tutorials.uptimeMonitors.shortDescription": "Monitorer la disponibilité des services", + "home.tutorials.uwsgiMetrics.artifacts.dashboards.linkLabel": "Tableau de bord des indicateurs uWSGI", + "home.tutorials.uwsgiMetrics.longDescription": "Le module Metricbeat \"uwsgi\" récupère des indicateurs internes depuis le serveur uWSGI. [En savoir plus]({learnMoreLink}).", + "home.tutorials.uwsgiMetrics.nameTitle": "Indicateurs uWSGI", + "home.tutorials.uwsgiMetrics.shortDescription": "Récupérez des indicateurs internes depuis le serveur uWSGI.", + "home.tutorials.vsphereMetrics.artifacts.application.label": "Discover", + "home.tutorials.vsphereMetrics.longDescription": "Le module Metricbeat \"vsphere\" récupère des indicateurs internes depuis un cluster vSphere. [En savoir plus]({learnMoreLink}).", + "home.tutorials.vsphereMetrics.nameTitle": "Indicateurs vSphere", + "home.tutorials.vsphereMetrics.shortDescription": "Récupérez des indicateurs internes depuis vSphere.", + "home.tutorials.windowsEventLogs.artifacts.application.label": "Application SIEM", + "home.tutorials.windowsEventLogs.longDescription": "Utilisez Winlogbeat pour collecter des logs depuis le log des événements Windows. [En savoir plus]({learnMoreLink}).", + "home.tutorials.windowsEventLogs.nameTitle": "Log des événements Windows", + "home.tutorials.windowsEventLogs.shortDescription": "Récupérez des logs depuis le log des événements Windows.", + "home.tutorials.windowsMetrics.artifacts.application.label": "Discover", + "home.tutorials.windowsMetrics.longDescription": "Le module Metricbeat \"windows\" récupère des indicateurs internes depuis Windows. [En savoir plus]({learnMoreLink}).", + "home.tutorials.windowsMetrics.nameTitle": "Indicateurs Windows", + "home.tutorials.windowsMetrics.shortDescription": "Récupérez des indicateurs internes depuis Windows.", + "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.nameTitle": "Logs Zeek", + "home.tutorials.zeekLogs.shortDescription": "Collectez les logs de monitoring de la sécurité réseau Zeek.", + "home.tutorials.zookeeperMetrics.artifacts.application.label": "Discover", + "home.tutorials.zookeeperMetrics.longDescription": "Le module Metricbeat \"{moduleName}\" récupère des indicateurs internes depuis un serveur Zookeeper. [En savoir plus]({learnMoreLink}).", + "home.tutorials.zookeeperMetrics.nameTitle": "Indicateurs Zookeeper", + "home.tutorials.zookeeperMetrics.shortDescription": "Récupérez des indicateurs internes depuis un serveur Zookeeper.", + "home.tutorials.zscalerLogs.artifacts.dashboards.linkLabel": "Application Security", + "home.tutorials.zscalerLogs.longDescription": "Ce module permet de recevoir des logs Zscaler NSS par le biais de Syslog ou d'un fichier. [En savoir plus]({learnMoreLink}).", + "home.tutorials.zscalerLogs.nameTitle": "Logs Zscaler", + "home.tutorials.zscalerLogs.shortDescription": "Ce module permet de recevoir des logs Zscaler NSS par le biais de Syslog ou d'un fichier.", + "home.welcomeTitle": "Bienvenue dans Elastic", + "indexPatternEditor.aliasLabel": "Alias", + "indexPatternEditor.createIndex.noMatch": "Le nom doit correspondre à au moins un flux de données, index ou alias d'index.", + "indexPatternEditor.createIndexPattern.emptyState.checkDataButton": "Rechercher de nouvelles données", + "indexPatternEditor.createIndexPattern.emptyState.haveData": "Vous pensez avoir déjà des données ?", + "indexPatternEditor.createIndexPattern.emptyState.integrationCardDescription": "Ajoutez des données depuis une variété de sources.", + "indexPatternEditor.createIndexPattern.emptyState.integrationCardTitle": "Ajouter une intégration", + "indexPatternEditor.createIndexPattern.emptyState.learnMore": "Envie d'en savoir plus ?", + "indexPatternEditor.createIndexPattern.emptyState.noDataTitle": "Vous êtes prêt à essayer Kibana ? Tout d'abord, vous avez besoin de données.", + "indexPatternEditor.createIndexPattern.emptyState.readDocs": "Lire la documentation", + "indexPatternEditor.createIndexPattern.emptyState.sampleDataCardDescription": "Chargez un ensemble de données et un tableau de bord Kibana.", + "indexPatternEditor.createIndexPattern.emptyState.sampleDataCardTitle": "Ajouter un exemple de données", + "indexPatternEditor.createIndexPattern.emptyState.uploadCardDescription": "Importez un fichier CSV, NDJSON ou log.", + "indexPatternEditor.createIndexPattern.emptyState.uploadCardTitle": "Charger un fichier", + "indexPatternEditor.createIndexPattern.stepTime.noTimeFieldOptionLabel": "--- Je ne souhaite pas utiliser le filtre temporel ---", + "indexPatternEditor.dataStreamLabel": "Flux de données", + "indexPatternEditor.editor.emptyPrompt.flyoutCloseButtonLabel": "Fermer", + "indexPatternEditor.editor.flyoutCloseButtonLabel": "Fermer", + "indexPatternEditor.editor.flyoutSaveButtonLabel": "Créer un modèle d'indexation", + "indexPatternEditor.editor.form.advancedSettings.hideButtonLabel": "Masquer les paramètres avancés", + "indexPatternEditor.editor.form.advancedSettings.showButtonLabel": "Afficher les paramètres avancés", + "indexPatternEditor.editor.form.allowHiddenLabel": "Autoriser les index masqués et système", + "indexPatternEditor.editor.form.customIdHelp": "Kibana fournit un identifiant unique pour chaque modèle d'indexation, ou vous pouvez en créer un vous-même.", + "indexPatternEditor.editor.form.customIdLabel": "ID de modèle d'indexation personnalisé", + "indexPatternEditor.editor.form.noTimeFieldsLabel": "Aucun flux de données, index ni alias d'index correspondant ne dispose d'un champ d'horodatage.", + "indexPatternEditor.editor.form.runtimeType.placeholderLabel": "Sélectionner un champ d'horodatage", + "indexPatternEditor.editor.form.timeFieldHelp": "Sélectionnez le champ d'horodatage à utiliser avec le filtre temporel global.", + "indexPatternEditor.editor.form.timeFieldLabel": "Champ d'horodatage", + "indexPatternEditor.editor.form.timestampFieldHelp": "Sélectionnez le champ d'horodatage à utiliser avec le filtre temporel global.", + "indexPatternEditor.editor.form.timestampSelectAriaLabel": "Champ d'horodatage", + "indexPatternEditor.editor.form.titleLabel": "Nom", + "indexPatternEditor.editor.form.TypeLabel": "Type de modèle d'indexation", + "indexPatternEditor.editor.form.typeSelectAriaLabel": "Champ Type", + "indexPatternEditor.emptyIndexPatternPrompt.documentation": "Lire la documentation", + "indexPatternEditor.emptyIndexPatternPrompt.learnMore": "Envie d'en savoir plus ?", + "indexPatternEditor.emptyIndexPatternPrompt.youHaveData": "Vous avez des données dans Elasticsearch.", + "indexPatternEditor.form.allowHiddenAriaLabel": "Autoriser les index masqués et système", + "indexPatternEditor.form.customIndexPatternIdLabel": "ID de modèle d'indexation personnalisé", + "indexPatternEditor.form.titleAriaLabel": "Champ de titre", + "indexPatternEditor.frozenLabel": "Gelé", + "indexPatternEditor.indexLabel": "Index", + "indexPatternEditor.loadingHeader": "Recherche d'index correspondants…", + "indexPatternEditor.pagingLabel": "Lignes par page : {perPage}", + "indexPatternEditor.requireTimestampOption.ValidationErrorMessage": "Sélectionnez un champ d'horodatage.", + "indexPatternEditor.rollup.uncaughtError": "Erreur de modèle d'indexation de cumul : {error}", + "indexPatternEditor.rollupIndexPattern.warning.title": "Fonctionnalité bêta", + "indexPatternEditor.rollupLabel": "Cumul", + "indexPatternEditor.saved": "\"{indexPatternTitle}\" 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.", + "indexPatternEditor.status.noSystemIndicesWithPromptLabel": "Aucun flux de données, index ni alias d'index ne correspond à votre modèle d'indexation.", + "indexPatternEditor.status.notMatchLabel.allIndicesLabel": "{indicesLength, plural, one {# source} other {# sources} }", + "indexPatternEditor.status.notMatchLabel.notMatchDetail": "Le modèle d'indexation spécifié ne correspond à aucun flux de données, index ni alias d'index. Vous pouvez faire correspondre {strongIndices}.", + "indexPatternEditor.status.notMatchLabel.notMatchNoIndicesDetail": "Le modèle d'indexation spécifié ne correspond à aucun flux de données, index ni alias d'index.", + "indexPatternEditor.status.partialMatchLabel.partialMatchDetail": "Votre modèle d'indexation ne correspond à aucun flux de données, index ni alias d'index, mais {strongIndices} {matchedIndicesLength, plural, one {est semblable} other {sont semblables} }.", + "indexPatternEditor.status.partialMatchLabel.strongIndicesLabel": "{matchedIndicesLength, plural, one {source} other {# sources} }", + "indexPatternEditor.status.successLabel.successDetail": "Votre modèle d'indexation correspond à {sourceCount} {sourceCount, plural, one {source} other {sources} }.", + "indexPatternEditor.title": "Créer un modèle d'indexation", + "indexPatternEditor.typeSelect.betaLabel": "Bêta", + "indexPatternEditor.typeSelect.rollup": "Cumul", + "indexPatternEditor.typeSelect.rollupDescription": "Effectuer des agrégations limitées à partir de données résumées", + "indexPatternEditor.typeSelect.rollupTitle": "Modèle d'indexation de cumul", + "indexPatternEditor.typeSelect.standard": "Standard", + "indexPatternEditor.typeSelect.standardDescription": "Effectuer des agrégations complètes à partir de n'importe quelles données", + "indexPatternEditor.typeSelect.standardTitle": "Modèle d'indexation standard", + "indexPatternEditor.validations.titleHelpText": "Utilisez un astérisque (*) pour faire correspondre plusieurs caractères. Les espaces et les caractères , /, ?, \", <, >, | ne sont pas autorisés.", + "indexPatternEditor.validations.titleIsRequiredErrorMessage": "Nom obligatoire.", + "indexPatternFieldEditor.cancelField.confirmationModal.cancelButtonLabel": "Annuler", + "indexPatternFieldEditor.cancelField.confirmationModal.description": "Les modifications apportées à votre champ seront ignorées. Voulez-vous vraiment continuer ?", + "indexPatternFieldEditor.cancelField.confirmationModal.title": "Ignorer les modifications", + "indexPatternFieldEditor.color.actions": "Actions", + "indexPatternFieldEditor.color.addColorButton": "Ajouter une couleur", + "indexPatternFieldEditor.color.backgroundLabel": "Couleur d'arrière-plan", + "indexPatternFieldEditor.color.deleteAria": "Supprimer", + "indexPatternFieldEditor.color.deleteTitle": "Supprimer le format de couleur", + "indexPatternFieldEditor.color.exampleLabel": "Exemple", + "indexPatternFieldEditor.color.patternLabel": "Modèle (expression régulière)", + "indexPatternFieldEditor.color.rangeLabel": "Plage (min:max)", + "indexPatternFieldEditor.color.textColorLabel": "Couleur du texte", + "indexPatternFieldEditor.createField.flyoutAriaLabel": "Créer un champ", + "indexPatternFieldEditor.date.documentationLabel": "Documentation", + "indexPatternFieldEditor.date.momentLabel": "Modèle de format Moment.js (par défaut : {defaultPattern})", + "indexPatternFieldEditor.defaultErrorMessage": "Une erreur s'est produite lors de l'utilisation de cette configuration de format : {message}.", + "indexPatternFieldEditor.defaultFormatDropDown": "- Par défaut -", + "indexPatternFieldEditor.defaultFormatHeader": "Format (par défaut : {defaultFormat})", + "indexPatternFieldEditor.deleteField.savedHeader": "\"{fieldName}\" enregistré", + "indexPatternFieldEditor.deleteRuntimeField.confirmationModal.cancelButtonLabel": "Annuler", + "indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeButtonLabel": "Supprimer le champ", + "indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeMultipleButtonLabel": "Supprimer les champs", + "indexPatternFieldEditor.deleteRuntimeField.confirmationModal.saveButtonLabel": "Enregistrer les modifications", + "indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteMultipleTitle": "Supprimer {count} champs", + "indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteSingleTitle": "Supprimer le champ \"{name}\"", + "indexPatternFieldEditor.deleteRuntimeField.confirmModal.multipleDeletionDescription": "Vous êtes sur le point de supprimer les champs d'exécution suivants :", + "indexPatternFieldEditor.deleteRuntimeField.confirmModal.typeConfirm": "Saisissez REMOVE pour confirmer.", + "indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningChangingFields": "Modifier le nom ou le type peut affecter les recherches et les visualisations utilisant ce champ.", + "indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningRemovingFields": "Supprimer un champ peut affecter les recherches et les visualisations utilisant ce champ.", + "indexPatternFieldEditor.duration.decimalPlacesLabel": "Décimales", + "indexPatternFieldEditor.duration.includeSpace": "Inclure un espace entre le suffixe et la valeur", + "indexPatternFieldEditor.duration.inputFormatLabel": "Format d'entrée", + "indexPatternFieldEditor.duration.outputFormatLabel": "Format de sortie", + "indexPatternFieldEditor.duration.showSuffixLabel": "Afficher le suffixe", + "indexPatternFieldEditor.duration.showSuffixLabel.short": "Utiliser un suffixe court", + "indexPatternFieldEditor.durationErrorMessage": "Le nombre de décimales doit être compris entre 0 et 20.", + "indexPatternFieldEditor.editField.flyoutAriaLabel": "Modifier le champ {fieldName}", + "indexPatternFieldEditor.editor.flyoutCancelButtonLabel": "Annuler", + "indexPatternFieldEditor.editor.flyoutDefaultTitle": "Créer un champ", + "indexPatternFieldEditor.editor.flyoutEditFieldSubtitle": "Modèle d'indexation : {patternName}", + "indexPatternFieldEditor.editor.flyoutEditFieldTitle": "Modifier le champ \"{fieldName}\"", + "indexPatternFieldEditor.editor.flyoutSaveButtonLabel": "Enregistrer", + "indexPatternFieldEditor.editor.form.advancedSettings.hideButtonLabel": "Masquer les paramètres avancés", + "indexPatternFieldEditor.editor.form.advancedSettings.showButtonLabel": "Afficher les paramètres avancés", + "indexPatternFieldEditor.editor.form.changeWarning": "Modifier le nom ou le type peut affecter les recherches et les visualisations utilisant ce champ.", + "indexPatternFieldEditor.editor.form.customLabelDescription": "Créez une étiquette à afficher à la place du nom du champ dans Discover, Maps et Visualize. Utile pour raccourcir un nom de champ long. Les requêtes et les filtres utilisent le nom de champ d'origine.", + "indexPatternFieldEditor.editor.form.customLabelLabel": "Étiquette personnalisée", + "indexPatternFieldEditor.editor.form.customLabelTitle": "Définir une étiquette personnalisée", + "indexPatternFieldEditor.editor.form.defineFieldLabel": "Définir un script", + "indexPatternFieldEditor.editor.form.fieldShadowingCalloutDescription": "Ce champ partage le nom d'un champ mappé. Les valeurs de ce champ seront renvoyées dans les résultats de recherche.", + "indexPatternFieldEditor.editor.form.fieldShadowingCalloutTitle": "Masquage de champ", + "indexPatternFieldEditor.editor.form.formatDescription": "Définissez votre format de prédilection pour l'affichage de la valeur. Changer le format peut avoir un impact sur la valeur et empêcher la mise en surbrillance dans Discover.", + "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.popularityLabel": "Popularité", + "indexPatternFieldEditor.editor.form.popularityTitle": "Définir la popularité", + "indexPatternFieldEditor.editor.form.runtimeType.placeholderLabel": "Sélectionner un type", + "indexPatternFieldEditor.editor.form.runtimeTypeLabel": "Type", + "indexPatternFieldEditor.editor.form.script.learnMoreLinkText": "En savoir plus sur la syntaxe de script.", + "indexPatternFieldEditor.editor.form.scriptEditor.compileErrorMessage": "Erreur lors de la compilation du script Painless", + "indexPatternFieldEditor.editor.form.scriptEditorAriaLabel": "Éditeur de script", + "indexPatternFieldEditor.editor.form.source.scriptFieldHelpText": "Les champs d'exécution sans script récupèrent les valeurs de {source}. Si un champ n'existe pas dans _source, la recherche ne renvoie pas de valeur. {learnMoreLink}", + "indexPatternFieldEditor.editor.form.typeSelectAriaLabel": "Sélection du type", + "indexPatternFieldEditor.editor.form.validations.customLabelIsRequiredErrorMessage": "Spécifiez une étiquette pour le champ.", + "indexPatternFieldEditor.editor.form.validations.nameIsRequiredErrorMessage": "Nom obligatoire.", + "indexPatternFieldEditor.editor.form.validations.popularityGreaterThan0ErrorMessage": "La popularité doit être définie sur 0 ou plus.", + "indexPatternFieldEditor.editor.form.validations.popularityIsRequiredErrorMessage": "Spécifiez la popularité du champ.", + "indexPatternFieldEditor.editor.form.validations.scriptIsRequiredErrorMessage": "Un script est obligatoire pour définir la valeur du champ.", + "indexPatternFieldEditor.editor.form.valueDescription": "Définissez une valeur pour le champ au lieu de la récupérer à partir du champ portant le même nom dans {source}.", + "indexPatternFieldEditor.editor.form.valueTitle": "Définir la valeur", + "indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage": "Un champ portant ce nom existe déjà.", + "indexPatternFieldEditor.fieldPreview.documentIdField.label": "ID du document", + "indexPatternFieldEditor.fieldPreview.documentIdField.loadDocumentsFromCluster": "Charger des documents depuis le cluster", + "indexPatternFieldEditor.fieldPreview.documentNav.nextArialabel": "Document suivant", + "indexPatternFieldEditor.fieldPreview.documentNav.previousArialabel": "Document précédent", + "indexPatternFieldEditor.fieldPreview.emptyPromptDescription": "Saisissez le nom d'un champ existant ou définissez un script pour afficher un aperçu de la sortie calculée.", + "indexPatternFieldEditor.fieldPreview.emptyPromptTitle": "Aperçu", + "indexPatternFieldEditor.fieldPreview.error.documentNotFoundDescription": "ID du document introuvable", + "indexPatternFieldEditor.fieldPreview.errorCallout.title": "Erreur d'aperçu", + "indexPatternFieldEditor.fieldPreview.errorTitle": "Échec du chargement de l'aperçu du champ", + "indexPatternFieldEditor.fieldPreview.filterFieldsPlaceholder": "Champs de filtre", + "indexPatternFieldEditor.fieldPreview.pinFieldButtonLabel": "Épingler le champ", + "indexPatternFieldEditor.fieldPreview.searchResult.emptyPrompt.clearSearchButtonLabel": "Effacer la recherche", + "indexPatternFieldEditor.fieldPreview.searchResult.emptyPromptTitle": "Aucun champ correspondant dans ce modèle d'indexation", + "indexPatternFieldEditor.fieldPreview.showLessFieldsButtonLabel": "Afficher moins", + "indexPatternFieldEditor.fieldPreview.showMoreFieldsButtonLabel": "Afficher plus", + "indexPatternFieldEditor.fieldPreview.subTitle": "Depuis : {from}", + "indexPatternFieldEditor.fieldPreview.subTitle.customData": "Données personnalisées", + "indexPatternFieldEditor.fieldPreview.title": "Aperçu", + "indexPatternFieldEditor.fieldPreview.updatingPreviewLabel": "Mise à jour en cours...", + "indexPatternFieldEditor.fieldPreview.viewImageButtonLabel": "Afficher l'image", + "indexPatternFieldEditor.formatHeader": "Format", + "indexPatternFieldEditor.histogram.histogramAsNumberLabel": "Format de nombre agrégé", + "indexPatternFieldEditor.histogram.numeralLabel": "Modèle de format numérique (facultatif)", + "indexPatternFieldEditor.histogram.subFormat.bytes": "Octets", + "indexPatternFieldEditor.histogram.subFormat.number": "Nombre", + "indexPatternFieldEditor.histogram.subFormat.percent": "Pourcentage", + "indexPatternFieldEditor.noSuchFieldName": "Champ \"{fieldName}\" introuvable dans le modèle d'indexation", + "indexPatternFieldEditor.number.documentationLabel": "Documentation", + "indexPatternFieldEditor.number.numeralLabel": "Modèle de format Numeral.js (par défaut : {defaultPattern})", + "indexPatternFieldEditor.samples.inputHeader": "Entrée", + "indexPatternFieldEditor.samples.outputHeader": "Sortie", + "indexPatternFieldEditor.samplesHeader": "Exemples", + "indexPatternFieldEditor.save.deleteErrorTitle": "Impossible d'enregistrer la suppression du champ", + "indexPatternFieldEditor.save.errorTitle": "Impossible d'enregistrer la modification du champ", + "indexPatternFieldEditor.saveRuntimeField.confirmationModal.cancelButtonLabel": "Annuler", + "indexPatternFieldEditor.saveRuntimeField.confirmModal.title": "Enregistrer les modifications apportées à \"{name}\"", + "indexPatternFieldEditor.saveRuntimeField.confirmModal.typeConfirm": "Saisissez CHANGE pour continuer.", + "indexPatternFieldEditor.staticLookup.actions": "actions", + "indexPatternFieldEditor.staticLookup.addEntryButton": "Ajouter une entrée", + "indexPatternFieldEditor.staticLookup.deleteAria": "Supprimer", + "indexPatternFieldEditor.staticLookup.deleteTitle": "Supprimer l’entrée", + "indexPatternFieldEditor.staticLookup.keyLabel": "Clé", + "indexPatternFieldEditor.staticLookup.leaveBlankPlaceholder": "Laisser vide pour conserver la valeur telle quelle", + "indexPatternFieldEditor.staticLookup.unknownKeyLabel": "Valeur pour clé inconnue", + "indexPatternFieldEditor.staticLookup.valueLabel": "Valeur", + "indexPatternFieldEditor.string.transformLabel": "Transformer", + "indexPatternFieldEditor.truncate.lengthLabel": "Longueur du champ", + "indexPatternFieldEditor.url.heightLabel": "Hauteur", + "indexPatternFieldEditor.url.labelTemplateHelpText": "Aide sur le modèle d'étiquette", + "indexPatternFieldEditor.url.labelTemplateLabel": "Modèle d'étiquette", + "indexPatternFieldEditor.url.offLabel": "Off", + "indexPatternFieldEditor.url.onLabel": "On", + "indexPatternFieldEditor.url.openTabLabel": "Ouvrir dans un nouvel onglet", + "indexPatternFieldEditor.url.template.helpLinkText": "Aide sur le modèle d'URL", + "indexPatternFieldEditor.url.typeLabel": "Type", + "indexPatternFieldEditor.url.urlTemplateLabel": "Modèle d'URL", + "indexPatternFieldEditor.url.widthLabel": "Largeur", + "indexPatternManagement.actions.cancelButton": "Annuler", + "indexPatternManagement.actions.createButton": "Créer un champ", + "indexPatternManagement.actions.deleteButton": "Supprimer", + "indexPatternManagement.actions.saveButton": "Enregistrer le champ", + "indexPatternManagement.createHeader": "Créer un champ scripté", + "indexPatternManagement.customLabel": "Étiquette personnalisée", + "indexPatternManagement.defaultFormatDropDown": "- Par défaut -", + "indexPatternManagement.defaultFormatHeader": "Format (par défaut : {defaultFormat})", + "indexPatternManagement.deleteField.cancelButton": "Annuler", + "indexPatternManagement.deleteField.deleteButton": "Supprimer", + "indexPatternManagement.deleteField.deletedHeader": "\"’{fieldName}\" supprimé", + "indexPatternManagement.deleteField.savedHeader": "\"{fieldName}\" enregistré", + "indexPatternManagement.deleteFieldHeader": "Supprimer le champ \"{fieldName}\"", + "indexPatternManagement.deleteFieldLabel": "Il est impossible de récupérer un champ supprimé.{separator}Voulez-vous vraiment continuer ?", + "indexPatternManagement.disabledCallOutHeader": "Scripts désactivés", + "indexPatternManagement.disabledCallOutLabel": "Tous les scripts en ligne ont été désactivés dans Elasticsearch. Vous devez activer les scripts en ligne pour au moins un langage afin d'utiliser des champs scriptés dans Kibana.", + "indexPatternManagement.editHeader": "Modifier {fieldName}", + "indexPatternManagement.editIndexPattern.deleteButton": "Supprimer", + "indexPatternManagement.editIndexPattern.deprecation": "Les champs scriptés sont déclassés. Utilisez {runtimeDocs} à la place.", + "indexPatternManagement.editIndexPattern.fields.addFieldButtonLabel": "Ajouter un champ", + "indexPatternManagement.editIndexPattern.fields.filterAria": "Filtrer les types de champ", + "indexPatternManagement.editIndexPattern.fields.filterPlaceholder": "Rechercher", + "indexPatternManagement.editIndexPattern.fields.searchAria": "Rechercher des champs", + "indexPatternManagement.editIndexPattern.fields.table.additionalInfoAriaLabel": "Informations supplémentaires sur le champ", + "indexPatternManagement.editIndexPattern.fields.table.aggregatableDescription": "Ces champs peuvent être utilisés dans des agrégations de visualisations.", + "indexPatternManagement.editIndexPattern.fields.table.aggregatableLabel": "Regroupable", + "indexPatternManagement.editIndexPattern.fields.table.customLabelTooltip": "Une étiquette personnalisée pour le champ.", + "indexPatternManagement.editIndexPattern.fields.table.deleteDescription": "Supprimer", + "indexPatternManagement.editIndexPattern.fields.table.deleteLabel": "Supprimer", + "indexPatternManagement.editIndexPattern.fields.table.editDescription": "Modifier", + "indexPatternManagement.editIndexPattern.fields.table.editLabel": "Modifier", + "indexPatternManagement.editIndexPattern.fields.table.excludedDescription": "Champs exclus de _source lors de la récupération", + "indexPatternManagement.editIndexPattern.fields.table.excludedLabel": "Exclu", + "indexPatternManagement.editIndexPattern.fields.table.formatHeader": "Format", + "indexPatternManagement.editIndexPattern.fields.table.isAggregatableAria": "Est regroupable", + "indexPatternManagement.editIndexPattern.fields.table.isExcludedAria": "Est exclu", + "indexPatternManagement.editIndexPattern.fields.table.isSearchableAria": "Est interrogeable", + "indexPatternManagement.editIndexPattern.fields.table.nameHeader": "Nom", + "indexPatternManagement.editIndexPattern.fields.table.primaryTimeAriaLabel": "Champ temporel principal", + "indexPatternManagement.editIndexPattern.fields.table.primaryTimeTooltip": "Ce champ représente l'heure à laquelle les événements se sont produits.", + "indexPatternManagement.editIndexPattern.fields.table.runtimeIconTipTitle": "Champ d'exécution", + "indexPatternManagement.editIndexPattern.fields.table.searchableDescription": "Ces champs peuvent être utilisés dans la barre de filtre.", + "indexPatternManagement.editIndexPattern.fields.table.searchableHeader": "Interrogeable", + "indexPatternManagement.editIndexPattern.fields.table.typeHeader": "Type", + "indexPatternManagement.editIndexPattern.list.DateHistogramDelaySummary": "retard : {delay},", + "indexPatternManagement.editIndexPattern.list.dateHistogramSummary": "{aggName} (intervalle : {interval}, {delay} {time_zone})", + "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "Par défaut", + "indexPatternManagement.editIndexPattern.list.histogramSummary": "{aggName} (intervalle : {interval})", + "indexPatternManagement.editIndexPattern.list.rollupIndexPatternListName": "Cumul", + "indexPatternManagement.editIndexPattern.mappingConflictHeader": "Conflit de mapping", + "indexPatternManagement.editIndexPattern.mappingConflictLabel": "{conflictFieldsLength, plural, one {Un champ est défini} other {# champs sont définis}} avec plusieurs types (chaîne, entier, etc.) dans les différents index qui correspondent à ce modèle. Vous pourrez peut-être utiliser ce ou ces champs en conflit dans certaines parties de Kibana, mais ils ne seront pas disponibles pour les fonctions qui nécessitent que Kibana connaisse leur type. Pour corriger ce problème, vous devrez réindexer vos données.", + "indexPatternManagement.editIndexPattern.scripted.addFieldButton": "Ajouter un champ scripté", + "indexPatternManagement.editIndexPattern.scripted.deleteField.cancelButton": "Annuler", + "indexPatternManagement.editIndexPattern.scripted.deleteField.deleteButton": "Supprimer", + "indexPatternManagement.editIndexPattern.scripted.deleteFieldLabel": "Supprimer le champ scripté \"{fieldName}\" ?", + "indexPatternManagement.editIndexPattern.scripted.deprecationLangHeader": "Langages déclassés en cours d'utilisation", + "indexPatternManagement.editIndexPattern.scripted.deprecationLangLabel.deprecationLangDetail": "Les langages déclassés suivants sont en cours d'utilisation : {deprecatedLangsInUse}. La prise en charge de ces langages sera supprimée dans la prochaine version majeure de Kibana et d'Elasticsearch. Convertissez vos champs scriptés en {link} pour éviter tout problème.", + "indexPatternManagement.editIndexPattern.scripted.deprecationLangLabel.painlessDescription": "Painless", + "indexPatternManagement.editIndexPattern.scripted.newFieldPlaceholder": "Nouveau champ scripté", + "indexPatternManagement.editIndexPattern.scripted.table.deleteDescription": "Supprimer ce champ", + "indexPatternManagement.editIndexPattern.scripted.table.deleteHeader": "Supprimer", + "indexPatternManagement.editIndexPattern.scripted.table.editDescription": "Modifier ce champ", + "indexPatternManagement.editIndexPattern.scripted.table.editHeader": "Modifier", + "indexPatternManagement.editIndexPattern.scripted.table.formatDescription": "Format utilisé pour le champ", + "indexPatternManagement.editIndexPattern.scripted.table.formatHeader": "Format", + "indexPatternManagement.editIndexPattern.scripted.table.langDescription": "Langage utilisé pour le champ", + "indexPatternManagement.editIndexPattern.scripted.table.langHeader": "Lang", + "indexPatternManagement.editIndexPattern.scripted.table.nameDescription": "Nom du champ", + "indexPatternManagement.editIndexPattern.scripted.table.nameHeader": "Nom", + "indexPatternManagement.editIndexPattern.scripted.table.scriptDescription": "Script pour le champ", + "indexPatternManagement.editIndexPattern.scripted.table.scriptHeader": "Script", + "indexPatternManagement.editIndexPattern.scriptedLabel": "Les champs scriptés peuvent être utilisés dans des visualisations et affichés dans des documents. Ils ne peuvent cependant pas faire l'objet d'une recherche.", + "indexPatternManagement.editIndexPattern.source.addButtonLabel": "Ajouter", + "indexPatternManagement.editIndexPattern.source.deleteFilter.cancelButtonLabel": "Annuler", + "indexPatternManagement.editIndexPattern.source.deleteFilter.deleteButtonLabel": "Supprimer", + "indexPatternManagement.editIndexPattern.source.deleteSourceFilterLabel": "Supprimer le filtre de champ \"{value}\" ?", + "indexPatternManagement.editIndexPattern.source.noteLabel": "Notez que les champs multiples apparaîtront incorrectement comme des correspondances dans le tableau ci-dessous. Ces filtres ne s'appliquent qu'aux champs dans le document source d'origine. Par conséquent, les champs multiples ne sont pas réellement filtrés.", + "indexPatternManagement.editIndexPattern.source.table.cancelAria": "Annuler", + "indexPatternManagement.editIndexPattern.source.table.deleteAria": "Supprimer", + "indexPatternManagement.editIndexPattern.source.table.editAria": "Modifier", + "indexPatternManagement.editIndexPattern.source.table.filterDescription": "Nom du filtre", + "indexPatternManagement.editIndexPattern.source.table.filterHeader": "Filtre", + "indexPatternManagement.editIndexPattern.source.table.matchesDescription": "Langage utilisé pour le champ", + "indexPatternManagement.editIndexPattern.source.table.matchesHeader": "Correspondances", + "indexPatternManagement.editIndexPattern.source.table.notMatchedLabel": "Le filtre source ne correspond à aucun champ connu.", + "indexPatternManagement.editIndexPattern.source.table.saveAria": "Enregistrer", + "indexPatternManagement.editIndexPattern.sourceLabel": "Les filtres de champ peuvent être utilisés pour exclure un ou plusieurs champs lors de la récupération d'un document. Cela se produit lors de l'affichage d'un document dans l'application Discover ou avec un tableau affichant les résultats d'une recherche enregistrée dans l'application Dashboard. Si vous avez des documents avec des champs de grande taille ou peu importants, il pourrait être utile de filtrer ces champs à ce niveau plus bas.", + "indexPatternManagement.editIndexPattern.sourcePlaceholder": "filtre de champ, accepte les caractères génériques (par ex. \"utilisateur*\" pour filtrer les champs commençant par \"utilisateur\")", + "indexPatternManagement.editIndexPattern.tabs.fieldsHeader": "Champs", + "indexPatternManagement.editIndexPattern.tabs.scriptedHeader": "Champs scriptés", + "indexPatternManagement.editIndexPattern.tabs.sourceHeader": "Filtres de champ", + "indexPatternManagement.editIndexPattern.timeFilterHeader": "Champ temporel : \"{timeFieldName}\"", + "indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "mappings de champ", + "indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "Affichez et modifiez les champs dans {indexPatternTitle}. Les attributs de champ tels que le type et le niveau de recherche sont basés sur {mappingAPILink} dans Elasticsearch.", + "indexPatternManagement.fieldTypeConflict": "Conflit de type de champ", + "indexPatternManagement.formatHeader": "Format", + "indexPatternManagement.formatLabel": "La mise en forme vous permet de contrôler la façon dont des valeurs spécifiques sont affichées. Cela peut également entraîner une modification complète des valeurs et empêcher la mise en surbrillance dans Discover de fonctionner.", + "indexPatternManagement.header.runtimeLink": "champs d'exécution", + "indexPatternManagement.indexNameLabel": "Nom des index", + "indexPatternManagement.indexPatterns.badge.readOnly.text": "Lecture seule", + "indexPatternManagement.indexPatterns.createFieldBreadcrumb": "Créer un champ", + "indexPatternManagement.labelHelpText": "Définissez une étiquette personnalisée à utiliser lorsque ce champ est affiché dans Discover, Maps et Visualize. Actuellement, les requêtes et les filtres ne prennent pas en charge les étiquettes personnalisées et utilisent le nom d'origine des champs.", + "indexPatternManagement.languageLabel": "Langage", + "indexPatternManagement.mappingConflictLabel.mappingConflictDetail": "{mappingConflict} Vous avez déjà un champ nommé {fieldName}. Si vous donnez le même nom à votre champ scripté, vous ne pourrez pas interroger les deux champs en même temps.", + "indexPatternManagement.mappingConflictLabel.mappingConflictLabel": "Conflit de mapping :", + "indexPatternManagement.multiTypeLabelDesc": "Le type de ce champ varie selon les index. Il n'est pas disponible pour de nombreuses fonctions d'analyse. Les index par type sont les suivants :", + "indexPatternManagement.nameErrorMessage": "Nom obligatoire", + "indexPatternManagement.nameLabel": "Nom", + "indexPatternManagement.namePlaceholder": "Nouveau champ scripté", + "indexPatternManagement.popularityLabel": "Popularité", + "indexPatternManagement.script.accessWithLabel": "Accédez aux champs avec {code}.", + "indexPatternManagement.script.getHelpLabel": "Obtenez de l'aide pour la syntaxe et prévisualisez les résultats de votre script.", + "indexPatternManagement.scriptedFieldsDeprecatedBody": "Pour profiter de plus de flexibilité et de la prise en charge des scripts Painless, utilisez {runtimeDocs}.", + "indexPatternManagement.scriptedFieldsDeprecatedTitle": "Les champs scriptés sont déclassés.", + "indexPatternManagement.scriptingLanguages.errorFetchingToastDescription": "Erreur lors de l'obtention des langages de script disponibles à partir d'Elasticsearch", + "indexPatternManagement.scriptInvalidErrorMessage": "Script non valide. Voir l'aperçu du script pour plus de détails.", + "indexPatternManagement.scriptLabel": "Script", + "indexPatternManagement.scriptRequiredErrorMessage": "Script obligatoire", + "indexPatternManagement.syntax.default.formatLabel": "doc['some_field'].value", + "indexPatternManagement.syntax.defaultLabel.defaultDetail": "Par défaut, les champs scriptés Kibana emploient {painless}, un langage de script simple et sécurisé spécialement conçu pour Elasticsearch. Pour accéder aux valeurs du document, utilisez le format suivant :", + "indexPatternManagement.syntax.defaultLabel.painlessLink": "Painless", + "indexPatternManagement.syntax.kibanaLabel": "Kibana impose actuellement une limitation spéciale sur les scripts Painless. Ils ne peuvent pas contenir de fonctions nommées.", + "indexPatternManagement.syntax.lucene.commonLabel.commonDetail": "Vous venez d'une ancienne version de Kibana ? Les expressions {lucene} que vous connaissez et adorez sont toujours disponibles. Les expressions Lucene ressemblent beaucoup à du JavaScript, mais elles se limitent aux opérations arithmétiques de base, aux opérations au niveau du bit et aux opérations de comparaison.", + "indexPatternManagement.syntax.lucene.commonLabel.luceneLink": "Expressions Lucene", + "indexPatternManagement.syntax.lucene.limits.fieldsLabel": "Les champs stockés ne sont pas disponibles.", + "indexPatternManagement.syntax.lucene.limits.sparseLabel": "Si un champ est clairsemé (seuls certains documents contiennent une valeur), les documents où ce champ est vide auront une valeur de 0.", + "indexPatternManagement.syntax.lucene.limits.typesLabel": "Seuls les champs numériques, booléens, de date et de point géographique sont accessibles.", + "indexPatternManagement.syntax.lucene.limitsLabel": "L'utilisation d’expressions Lucene implique quelques limitations :", + "indexPatternManagement.syntax.lucene.operations.arithmeticLabel": "Opérateurs arithmétiques : {operators}", + "indexPatternManagement.syntax.lucene.operations.bitwiseLabel": "Opérateurs au niveau du bit : {operators}", + "indexPatternManagement.syntax.lucene.operations.booleanLabel": "Opérateurs booléens (y compris l'opérateur ternaire) : {operators}", + "indexPatternManagement.syntax.lucene.operations.comparisonLabel": "Opérateurs de comparaison : {operators}", + "indexPatternManagement.syntax.lucene.operations.distanceLabel": "Fonctions de distance : {operators}", + "indexPatternManagement.syntax.lucene.operations.mathLabel": "Fonctions mathématiques communes : {operators}", + "indexPatternManagement.syntax.lucene.operations.miscellaneousLabel": "Fonctions diverses : {operators}", + "indexPatternManagement.syntax.lucene.operations.trigLabel": "Fonctions de bibliothèque trigonométrique : {operators}", + "indexPatternManagement.syntax.lucene.operationsLabel": "Voici toutes les opérations disponibles pour les expressions Lucene :", + "indexPatternManagement.syntax.painlessLabel.javaAPIsLink": "API Java natives", + "indexPatternManagement.syntax.painlessLabel.painlessDetail": "Painless est un langage puissant, mais facile à utiliser. Il donne accès à de nombreuses {javaAPIs}. Lisez-en plus sur sa {syntax} et découvrez tout ce que vous devez savoir en un rien de temps !", + "indexPatternManagement.syntax.painlessLabel.syntaxLink": "syntaxe", + "indexPatternManagement.syntaxHeader": "Syntaxe", + "indexPatternManagement.testScript.errorMessage": "Votre script présente une erreur.", + "indexPatternManagement.testScript.fieldsLabel": "Champs supplémentaires", + "indexPatternManagement.testScript.fieldsPlaceholder": "Sélectionner…", + "indexPatternManagement.testScript.instructions": "Exécutez votre script pour prévisualiser les 10 premiers résultats. Vous pouvez également sélectionner des champs supplémentaires à inclure dans les résultats pour obtenir plus de contexte ou ajouter une requête pour filtrer des documents spécifiques.", + "indexPatternManagement.testScript.resultsLabel": "10 premiers résultats", + "indexPatternManagement.testScript.resultsTitle": "Prévisualiser les résultats", + "indexPatternManagement.testScript.submitButtonLabel": "Exécuter le script", + "indexPatternManagement.typeLabel": "Type", + "indexPatternManagement.warningCallOutLabel.callOutDetail": "Familiarisez-vous avec les {scripFields} et les {scriptsInAggregation} avant d'utiliser cette fonctionnalité. Les champs scriptés peuvent être utilisés pour afficher et agréger les valeurs calculées. Dès lors, ils peuvent être très lents et, s'ils ne sont pas faits correctement, ils peuvent rendre Kibana inutilisable.", + "indexPatternManagement.warningCallOutLabel.runtimeLink": "champs d'exécution", + "indexPatternManagement.warningCallOutLabel.scripFieldsLink": "champs scriptés", + "indexPatternManagement.warningCallOutLabel.scriptsInAggregationLink": "scripts en agrégations", + "indexPatternManagement.warningHeader": "Avertissement de déclassement :", + "indexPatternManagement.warningLabel.painlessLinkLabel": "Painless", + "indexPatternManagement.warningLabel.warningDetail": "{language} est déclassé et ne sera plus pris en charge dans la prochaine version majeure de Kibana et d'Elasticsearch. Nous recommandons d'utiliser {painlessLink} pour les nouveaux champs scriptés.", + "inputControl.control.noIndexPatternTooltip": "Impossible de localiser l'ID du modèle d'indexation : {indexPatternId}.", + "inputControl.control.notInitializedTooltip": "Le contrôle n'a pas été initialisé.", + "inputControl.control.noValuesDisableTooltip": "Le filtrage se produit sur le champ \"{fieldName}\", qui n'existe dans aucun document du modèle d'indexation \"{indexPatternName}\". Sélectionnez un champ différent ou des documents d'index qui contiennent des valeurs pour ce champ.", + "inputControl.editor.controlEditor.controlLabel": "Contrôler l'étiquette", + "inputControl.editor.controlEditor.moveControlDownAriaLabel": "Abaisser le contrôle", + "inputControl.editor.controlEditor.moveControlUpAriaLabel": "Remonter le contrôle", + "inputControl.editor.controlEditor.removeControlAriaLabel": "Retirer le contrôle", + "inputControl.editor.controlsTab.addButtonLabel": "Ajouter", + "inputControl.editor.controlsTab.select.addControlAriaLabel": "Ajouter un contrôle", + "inputControl.editor.controlsTab.select.controlTypeAriaLabel": "Choisir le type de contrôle", + "inputControl.editor.controlsTab.select.listDropDownOptionLabel": "Liste des options", + "inputControl.editor.controlsTab.select.rangeDropDownOptionLabel": "Curseur de plage", + "inputControl.editor.fieldSelect.fieldLabel": "Champ", + "inputControl.editor.fieldSelect.selectFieldPlaceholder": "Sélectionner un champ…", + "inputControl.editor.indexPatternSelect.patternLabel": "Modèle d'indexation", + "inputControl.editor.indexPatternSelect.patternPlaceholder": "Sélectionner un modèle d'indexation…", + "inputControl.editor.listControl.dynamicOptions.stringFieldDescription": "Uniquement disponible pour les champs de type chaîne", + "inputControl.editor.listControl.dynamicOptions.updateDescription": "Mettre à jour les options en réponse aux informations fournies par l'utilisateur", + "inputControl.editor.listControl.dynamicOptionsLabel": "Options dynamiques", + "inputControl.editor.listControl.multiselectDescription": "Permettre une sélection multiple", + "inputControl.editor.listControl.multiselectLabel": "Sélection multiple", + "inputControl.editor.listControl.parentDescription": "Les options sont basées sur la valeur du contrôle parent. Désactivé si le parent n'est pas défini.", + "inputControl.editor.listControl.parentLabel": "Contrôle parent", + "inputControl.editor.listControl.sizeDescription": "Nombre d'options", + "inputControl.editor.listControl.sizeLabel": "Taille", + "inputControl.editor.optionsTab.pinFiltersLabel": "Épingler les filtres pour toutes les applications", + "inputControl.editor.optionsTab.updateFilterLabel": "Mettre à jour les filtres Kibana à chaque modification", + "inputControl.editor.optionsTab.useTimeFilterLabel": "Utiliser le filtre temporel", + "inputControl.editor.rangeControl.decimalPlacesLabel": "Décimales", + "inputControl.editor.rangeControl.stepSizeLabel": "Taille de l'étape", + "inputControl.function.help": "Visualisation du contrôle d'entrée", + "inputControl.listControl.disableTooltip": "Désactivé jusqu'à ce que \"{label}\" soit défini.", + "inputControl.listControl.unableToFetchTooltip": "Impossible de récupérer les termes. Erreur : {errorMessage}.", + "inputControl.rangeControl.unableToFetchTooltip": "Impossible de récupérer les valeurs min. et max. de la plage. Erreur : {errorMessage}.", + "inputControl.register.controlsDescription": "Ajoutez des menus déroulants et des curseurs de plage à votre tableau de bord.", + "inputControl.register.controlsTitle": "Contrôles", + "inputControl.register.tabs.controlsTitle": "Contrôles", + "inputControl.register.tabs.optionsTitle": "Options", + "inputControl.vis.inputControlVis.applyChangesButtonLabel": "Appliquer les modifications", + "inputControl.vis.inputControlVis.cancelChangesButtonLabel": "Annuler les modifications", + "inputControl.vis.inputControlVis.clearFormButtonLabel": "Effacer le formulaire", + "inputControl.vis.listControl.partialResultsWarningMessage": "La liste des termes peut être incomplète, car la requête prend trop de temps. Ajustez les paramètres de saisie semi-automatique dans le fichier kibana.yml pour obtenir des résultats complets.", + "inputControl.vis.listControl.selectPlaceholder": "Sélectionner…", + "inputControl.vis.listControl.selectTextPlaceholder": "Sélectionner…", + "inspector.closeButton": "Fermer l'inspecteur", + "inspector.reqTimestampDescription": "Heure de début de la requête", + "inspector.reqTimestampKey": "Horodatage de la requête", + "inspector.requests.copyToClipboardLabel": "Copier dans le presse-papiers", + "inspector.requests.descriptionRowIconAriaLabel": "Description", + "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", + "inspector.requests.requestFailedTooltipTitle": "Échec de la requête", + "inspector.requests.requestInProgressAriaLabel": "Requête en cours", + "inspector.requests.requestsDescriptionTooltip": "Voir les requêtes qui ont collecté les données", + "inspector.requests.requestsTitle": "Requêtes", + "inspector.requests.requestSucceededTooltipTitle": "Requête réussie", + "inspector.requests.requestTabLabel": "Requête", + "inspector.requests.requestTimeLabel": "{requestTime}ms", + "inspector.requests.requestTooltipDescription": "Durée totale qu'a nécessité la requête.", + "inspector.requests.requestWasMadeDescription": "{requestsCount, plural, one {# requête a été effectuée} other {# requêtes ont été effectuées} }{failedRequests}", + "inspector.requests.requestWasMadeDescription.requestHadFailureText": ", {failedCount} a/ont échoué.", + "inspector.requests.responseTabLabel": "Réponse", + "inspector.requests.searchSessionId": "ID de la session de recherche : {searchSessionId}", + "inspector.requests.statisticsTabLabel": "Statistiques", + "inspector.title": "Inspecteur", + "inspector.view": "Vue : {viewName}", + "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.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", + "kibana-react.dualRangeControl.minInputAriaLabel": "Minimum de la plage", + "kibana-react.dualRangeControl.mustSetBothErrorMessage": "Les valeurs inférieure et supérieure doivent être définies.", + "kibana-react.dualRangeControl.outsideOfRangeErrorMessage": "Les valeurs doivent être comprises entre {min} et {max}, inclus.", + "kibana-react.dualRangeControl.upperValidErrorMessage": "La valeur supérieure doit être supérieure ou égale à la valeur inférieure.", + "kibana-react.exitFullScreenButton.exitFullScreenModeButtonAriaLabel": "Quitter le mode Plein écran", + "kibana-react.exitFullScreenButton.exitFullScreenModeButtonText": "Quitter le plein écran", + "kibana-react.exitFullScreenButton.fullScreenModeDescription": "En mode Plein écran, appuyez sur Échap pour quitter.", + "kibana-react.kbnOverviewPageHeader.devToolsButtonLabel": "Outils de développement", + "kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel": "Gérer", + "kibana-react.kibanaCodeEditor.ariaLabel": "Éditeur de code", + "kibana-react.kibanaCodeEditor.enterKeyLabel": "Entrée", + "kibana-react.kibanaCodeEditor.escapeKeyLabel": "Échap", + "kibana-react.kibanaCodeEditor.startEditing": "Appuyez sur {key} pour modifier.", + "kibana-react.kibanaCodeEditor.startEditingReadOnly": "Appuyez sur {key} pour interagir avec le code.", + "kibana-react.kibanaCodeEditor.stopEditing": "Appuyez sur {key} pour arrêter la modification.", + "kibana-react.kibanaCodeEditor.stopEditingReadOnly": "Appuyez sur {key} pour arrêter l'interaction.", + "kibana-react.mountPointPortal.errorMessage": "Erreur lors du rendu du contenu du portail.", + "kibana-react.noDataPage.cantDecide": "Vous ne savez pas quoi utiliser ? {link}", + "kibana-react.noDataPage.cantDecide.link": "Consultez la documentation pour en savoir plus.", + "kibana-react.noDataPage.elasticAgentCard.description": "Utilisez Elastic Agent pour collecter de manière simple et unifiée les données de vos machines.", + "kibana-react.noDataPage.elasticAgentCard.title": "Ajouter Elastic Agent", + "kibana-react.noDataPage.intro": "Ajoutez vos données pour commencer, ou {link} sur {solution}.", + "kibana-react.noDataPage.intro.link": "en savoir plus", + "kibana-react.noDataPage.noDataPage.recommended": "Recommandé", + "kibana-react.noDataPage.welcomeTitle": "Bienvenue dans Elastic {solution}.", + "kibana-react.pageFooter.changeDefaultRouteSuccessToast": "Page de destination mise à jour", + "kibana-react.pageFooter.changeHomeRouteLink": "Afficher une page différente à la connexion", + "kibana-react.pageFooter.makeDefaultRouteLink": "Choisir comme page de destination", + "kibana-react.solutionNav.collapsibleLabel": "Réduire la navigation latérale", + "kibana-react.solutionNav.mobileTitleText": "Menu {solutionName}", + "kibana-react.solutionNav.openLabel": "Ouvrir la navigation latérale", + "kibana-react.tableListView.listing.createNewItemButtonLabel": "Créer {entityName}", + "kibana-react.tableListView.listing.deleteButtonMessage": "Supprimer {itemCount} {entityName}", + "kibana-react.tableListView.listing.deleteConfirmModalDescription": "Vous ne pourrez pas récupérer les {entityNamePlural} supprimés.", + "kibana-react.tableListView.listing.deleteSelectedConfirmModal.title": "Supprimer {itemCount} {entityName} ?", + "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.cancelButtonLabel": "Annuler", + "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.confirmButtonLabel": "Supprimer", + "kibana-react.tableListView.listing.deleteSelectedItemsConfirmModal.confirmButtonLabelDeleting": "Suppression", + "kibana-react.tableListView.listing.fetchErrorDescription": "Le listing {entityName} n'a pas pu être récupéré : {message}.", + "kibana-react.tableListView.listing.fetchErrorTitle": "Échec de la récupération du listing", + "kibana-react.tableListView.listing.listingLimitExceeded.advancedSettingsLinkText": "Paramètres avancés", + "kibana-react.tableListView.listing.listingLimitExceededDescription": "Vous avez {totalItems} {entityNamePlural}, mais votre paramètre {listingLimitText} empêche le tableau ci-dessous d'en afficher plus de {listingLimitValue}. Vous pouvez modifier ce paramètre sous {advancedSettingsLink}.", + "kibana-react.tableListView.listing.listingLimitExceededTitle": "Limite de listing dépassée", + "kibana-react.tableListView.listing.table.actionTitle": "Actions", + "kibana-react.tableListView.listing.table.editActionDescription": "Modifier", + "kibana-react.tableListView.listing.table.editActionName": "Modifier", + "kibana-react.tableListView.listing.unableToDeleteDangerMessage": "Impossible de supprimer le/les {entityName}(s)", + "kibanaOverview.addData.sampleDataButtonLabel": "Essayer l’exemple de données", + "kibanaOverview.addData.sectionTitle": "Ingérer des données", + "kibanaOverview.apps.title": "Explorer les applications", + "kibanaOverview.breadcrumbs.title": "Analytique", + "kibanaOverview.header.title": "Analytique", + "kibanaOverview.kibana.solution.description": "Explorez, visualisez et analysez vos données à l'aide d'une puissante suite d'outils et d'applications analytiques.", + "kibanaOverview.kibana.solution.title": "Analytique", + "kibanaOverview.manageData.sectionTitle": "Gérer vos données", + "kibanaOverview.more.title": "Toujours plus avec Elastic", + "kibanaOverview.news.title": "Nouveautés", + "kibanaOverview.noDataConfig.solutionName": "Analytique", + "lists.exceptions.doesNotExistOperatorLabel": "n'existe pas", + "lists.exceptions.existsOperatorLabel": "existe", + "lists.exceptions.isInListOperatorLabel": "est dans la liste", + "lists.exceptions.isNotInListOperatorLabel": "n'est pas dans la liste", + "lists.exceptions.isNotOneOfOperatorLabel": "n'est pas l'une des options suivantes", + "lists.exceptions.isNotOperatorLabel": "n'est pas", + "lists.exceptions.isOneOfOperatorLabel": "est l'une des options suivantes", + "lists.exceptions.isOperatorLabel": "est", + "management.breadcrumb": "Gestion de la Suite", + "management.landing.header": "Bienvenue dans Gestion de la Suite {version}", + "management.landing.subhead": "Gérez vos index, modèles d'indexation, objets enregistrés, paramètres Kibana et plus encore.", + "management.landing.text": "Vous trouverez une liste complète des applications dans le menu de gauche.", + "management.nav.label": "Gestion", + "management.sections.dataTip": "Gérez les données et les sauvegardes de vos clusters.", + "management.sections.dataTitle": "Données", + "management.sections.ingestTip": "Gérez la manière dont les données sont transformées et chargées dans le cluster.", + "management.sections.ingestTitle": "Ingestion", + "management.sections.insightsAndAlertingTip": "Gérez le mode de détection des changements dans vos données.", + "management.sections.insightsAndAlertingTitle": "Alertes et informations exploitables", + "management.sections.kibanaTip": "Personnalisez Kibana et gérez les objets enregistrés.", + "management.sections.kibanaTitle": "Kibana", + "management.sections.section.tip": "Contrôlez l'accès aux fonctionnalités et aux données.", + "management.sections.section.title": "Sécurité", + "management.sections.stackTip": "Gérez votre licence et mettez la Suite à niveau.", + "management.sections.stackTitle": "Suite", + "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.painlessLanguage.autocomplete.docKeywordDescription": "Accéder à une valeur de champ dans un script au moyen de la syntaxe doc['field_name']", + "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", + "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", + "newsfeed.flyoutList.versionTextLabel": "{version}", + "newsfeed.flyoutList.whatsNewTitle": "Nouveautés Elastic", + "newsfeed.headerButton.readAriaLabel": "Menu du fil d'actualités – Tous les éléments lus", + "newsfeed.headerButton.unreadAriaLabel": "Menu du fil d'actualités – Éléments non lus disponibles", + "newsfeed.loadingPrompt.gettingNewsText": "Obtention des dernières actualités…", + "presentationUtil.dashboardPicker.searchDashboardPlaceholder": "Recherche dans les tableaux de bord…", + "presentationUtil.labs.components.browserSwitchHelp": "Active l'atelier pour ce navigateur et persiste après sa fermeture.", + "presentationUtil.labs.components.browserSwitchName": "Navigateur", + "presentationUtil.labs.components.calloutHelp": "Actualiser pour appliquer les modifications", + "presentationUtil.labs.components.closeButtonLabel": "Fermer", + "presentationUtil.labs.components.descriptionMessage": "Essayez nos fonctionnalités expérimentales ou en cours.", + "presentationUtil.labs.components.disabledStatusMessage": "Par défaut : {status}", + "presentationUtil.labs.components.enabledStatusMessage": "Par défaut : {status}", + "presentationUtil.labs.components.kibanaSwitchHelp": "Active cet atelier pour tous les utilisateurs Kibana.", + "presentationUtil.labs.components.kibanaSwitchName": "Kibana", + "presentationUtil.labs.components.labFlagsLabel": "Indicateurs d'atelier", + "presentationUtil.labs.components.noProjectsinSolutionMessage": "Aucun atelier actuellement dans {solutionName}.", + "presentationUtil.labs.components.noProjectsMessage": "Aucun atelier actuellement disponible.", + "presentationUtil.labs.components.overrideFlagsLabel": "Remplacements", + "presentationUtil.labs.components.overridenIconTipLabel": "Valeur par défaut remplacée", + "presentationUtil.labs.components.resetToDefaultLabel": "Réinitialiser aux valeurs par défaut", + "presentationUtil.labs.components.sessionSwitchHelp": "Active l’atelier pour cette session de navigateur afin de le réinitialiser lors de sa fermeture.", + "presentationUtil.labs.components.sessionSwitchName": "Session", + "presentationUtil.labs.components.titleLabel": "Ateliers", + "presentationUtil.labs.enableDeferBelowFoldProjectDescription": "Les panneaux sous \"le pli\", la zone masquée en-dessous de la fenêtre accessible en faisant défiler, ne se chargeront pas immédiatement, mais seulement lorsqu'ils entreront dans la fenêtre d'affichage.", + "presentationUtil.labs.enableDeferBelowFoldProjectName": "Différer le chargement des panneaux sous \"le pli\"", + "presentationUtil.saveModalDashboard.addToDashboardLabel": "Ajouter au tableau de bord", + "presentationUtil.saveModalDashboard.dashboardInfoTooltip": "Les éléments ajoutés à la bibliothèque Visualize sont disponibles pour tous les tableaux de bord. Les modifications apportées à un élément de bibliothèque sont répercutées partout où il est utilisé.", + "presentationUtil.saveModalDashboard.existingDashboardOptionLabel": "Existant", + "presentationUtil.saveModalDashboard.libraryOptionLabel": "Ajouter à la bibliothèque", + "presentationUtil.saveModalDashboard.newDashboardOptionLabel": "Nouveau", + "presentationUtil.saveModalDashboard.noDashboardOptionLabel": "Aucun", + "presentationUtil.saveModalDashboard.saveAndGoToDashboardLabel": "Enregistrer et accéder au tableau de bord", + "presentationUtil.saveModalDashboard.saveLabel": "Enregistrer", + "presentationUtil.saveModalDashboard.saveToLibraryLabel": "Enregistrer et ajouter à la bibliothèque", + "presentationUtil.solutionToolbar.editorMenuButtonLabel": "Tous les éditeurs", + "presentationUtil.solutionToolbar.libraryButtonLabel": "Ajouter depuis la bibliothèque", + "presentationUtil.solutionToolbar.quickButton.ariaButtonLabel": "Créer {createType}", + "presentationUtil.solutionToolbar.quickButton.legendLabel": "Création rapide", + "savedObjects.advancedSettings.listingLimitText": "Nombre d'objets à récupérer pour les pages de listing", + "savedObjects.advancedSettings.listingLimitTitle": "Limite de listing d’objets", + "savedObjects.advancedSettings.perPageText": "Nombre d'objets à afficher par page dans la boîte de dialogue de chargement", + "savedObjects.advancedSettings.perPageTitle": "Objets par page", + "savedObjects.confirmModal.cancelButtonLabel": "Annuler", + "savedObjects.confirmModal.overwriteButtonLabel": "Écraser", + "savedObjects.confirmModal.overwriteConfirmationMessage": "Êtes-vous sûr de vouloir écraser {title} ?", + "savedObjects.confirmModal.overwriteTitle": "Écraser {name} ?", + "savedObjects.confirmModal.saveDuplicateButtonLabel": "Enregistrer {name}", + "savedObjects.confirmModal.saveDuplicateConfirmationMessage": "Il y a déjà une occurrence de {name} avec le titre \"{title}\". Voulez-vous tout de même enregistrer ?", + "savedObjects.finder.filterButtonLabel": "Types", + "savedObjects.finder.searchPlaceholder": "Rechercher…", + "savedObjects.finder.sortAsc": "Croissant", + "savedObjects.finder.sortAuto": "Meilleure correspondance", + "savedObjects.finder.sortButtonLabel": "Trier", + "savedObjects.finder.sortDesc": "Décroissant", + "savedObjects.overwriteRejectedDescription": "La confirmation d'écrasement a été rejetée.", + "savedObjects.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.", + "savedObjects.saveModal.cancelButtonLabel": "Annuler", + "savedObjects.saveModal.descriptionLabel": "Description", + "savedObjects.saveModal.duplicateTitleDescription": "L'enregistrement de \"{title}\" crée un doublon de titre.", + "savedObjects.saveModal.duplicateTitleLabel": "Ce {objectType} existe déjà.", + "savedObjects.saveModal.saveAsNewLabel": "Enregistrer en tant que nouveau {objectType}", + "savedObjects.saveModal.saveButtonLabel": "Enregistrer", + "savedObjects.saveModal.saveTitle": "Enregistrer {objectType}", + "savedObjects.saveModal.titleLabel": "Titre", + "savedObjects.saveModalOrigin.addToOriginLabel": "Ajouter", + "savedObjects.saveModalOrigin.originAfterSavingSwitchLabel": "{originVerb} à {origin} après l'enregistrement", + "savedObjects.saveModalOrigin.returnToOriginLabel": "Renvoyer", + "savedObjects.saveModalOrigin.saveAndReturnLabel": "Enregistrer et renvoyer", + "savedObjectsManagement.breadcrumb.index": "Objets enregistrés", + "savedObjectsManagement.deleteConfirm.modalDeleteButtonLabel": "Supprimer", + "savedObjectsManagement.deleteConfirm.modalDescription": "Cette action supprime définitivement l'objet de Kibana.", + "savedObjectsManagement.deleteConfirm.modalTitle": "Supprimer \"{title}\" ?", + "savedObjectsManagement.deleteSavedObjectsConfirmModalDescription": "Cette action supprimera les objets enregistrés suivants :", + "savedObjectsManagement.importSummary.createdCountHeader": "{createdCount} nouveau(x)", + "savedObjectsManagement.importSummary.createdOutcomeLabel": "Créé", + "savedObjectsManagement.importSummary.errorCountHeader": "{errorCount} erreur(s)", + "savedObjectsManagement.importSummary.errorOutcomeLabel": "{errorMessage}", + "savedObjectsManagement.importSummary.headerLabel": "{importCount, plural, one {1 objet importé} other {# objets importés}}", + "savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount} écrasé(s)", + "savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "Écrasé", + "savedObjectsManagement.importSummary.warnings.defaultButtonLabel": "Go", + "savedObjectsManagement.managementSectionLabel": "Objets enregistrés", + "savedObjectsManagement.objects.savedObjectsDescription": "Importez, exportez et gérez vos recherches enregistrées, vos visualisations et vos tableaux de bord.", + "savedObjectsManagement.objects.savedObjectsTitle": "Objets enregistrés", + "savedObjectsManagement.objectsTable.deleteConfirmModal.cannotDeleteCallout.title": "Certains objets ne peuvent pas être supprimés.", + "savedObjectsManagement.objectsTable.deleteConfirmModal.sharedObjectsCallout.content": "Les objets partagés sont supprimés de tous les espaces dans lesquels ils se trouvent.", + "savedObjectsManagement.objectsTable.deleteConfirmModal.sharedObjectsCallout.title": "{sharedObjectsCount, plural, one {# objet enregistré est partagé} other {# de vos objets enregistrés sont partagés}}.", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "Annuler", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "Supprimer {objectsCount, plural, one {# objet} other {# objets}}", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName": "ID", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName": "Titre", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName": "Type", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModalTitle": "Supprimer les objets enregistrés", + "savedObjectsManagement.objectsTable.export.successNotification": "Votre fichier est en cours de téléchargement en arrière-plan.", + "savedObjectsManagement.objectsTable.export.successWithExcludedObjectsNotification": "Votre fichier est en cours de téléchargement en arrière-plan. Certains objets ont été exclus de l'export. Vous trouverez la liste des objets exclus à la dernière ligne du fichier exporté.", + "savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification": "Votre fichier est en cours de téléchargement en arrière-plan. Certains objets associés sont introuvables. Vous trouverez la liste des objets manquants à la dernière ligne du fichier exporté.", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "Annuler", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "Exporter tout", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "Options", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel": "Inclure les objets associés", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription": "Sélectionner les types d'objet à exporter", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModalTitle": "Exporter {filteredItemCount, plural, one {# objet} other {# objets}}", + "savedObjectsManagement.objectsTable.flyout.errorCalloutTitle": "Désolé, une erreur est survenue.", + "savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel": "Annuler", + "savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel": "Importer", + "savedObjectsManagement.objectsTable.flyout.importFileErrorMessage": "Impossible de traiter le fichier en raison d'une erreur : \"{error}\".", + "savedObjectsManagement.objectsTable.flyout.importPromptText": "Importer", + "savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle": "Importer les objets enregistrés", + "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "Confirmer toutes les modifications", + "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmButtonLabel": "Terminé", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText": "créer un nouveau modèle d'indexation", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription": "Les objets enregistrés suivants utilisent des modèles d'indexation qui n'existent pas. Veuillez sélectionner les modèles d'indexation que vous souhaitez réassocier aux objets. Vous pouvez {indexPatternLink} si nécessaire.", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle": "Conflits de modèle d'indexation", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription": "Nombre d'objets concernés", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName": "Décompte", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription": "ID du modèle d'indexation", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdName": "ID", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnNewIndexPatternName": "Nouveau modèle d'indexation", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "Exemple d'objets concernés", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "Exemple d'objets concernés", + "savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel": "Sélectionner un fichier à importer", + "savedObjectsManagement.objectsTable.header.exportButtonLabel": "Exporter {filteredCount, plural, one{# objet} other {# objets}}", + "savedObjectsManagement.objectsTable.header.importButtonLabel": "Importer", + "savedObjectsManagement.objectsTable.header.refreshButtonLabel": "Actualiser", + "savedObjectsManagement.objectsTable.header.savedObjectsTitle": "Objets enregistrés", + "savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription": "Gérez et partagez vos objets enregistrés. Pour modifier les données sous-jacentes d'un objet, accédez à l’application associée.", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.disabledText": "Vérifiez si les objets ont déjà été copiés ou importés.", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.disabledTitle": "Rechercher les objets existants", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledText": "Utilisez cette option pour créer une ou plusieurs copies de l'objet.", + "savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledTitle": "Créer de nouveaux objets avec des ID aléatoires", + "savedObjectsManagement.objectsTable.importModeControl.importOptionsTitle": "Options d'importation", + "savedObjectsManagement.objectsTable.importModeControl.overwrite.disabledLabel": "Demander une action en cas de conflit", + "savedObjectsManagement.objectsTable.importModeControl.overwrite.enabledLabel": "Écraser automatiquement les conflits", + "savedObjectsManagement.objectsTable.importSummary.unsupportedTypeError": "Type d'objet non pris en charge", + "savedObjectsManagement.objectsTable.overwriteModal.body.ambiguousConflict": "\"{title}\" est en conflit avec plusieurs objets existants. En écraser un ?", + "savedObjectsManagement.objectsTable.overwriteModal.body.conflict": "\"{title}\" est en conflit avec un objet existant. L'écraser ?", + "savedObjectsManagement.objectsTable.overwriteModal.cancelButtonText": "Ignorer", + "savedObjectsManagement.objectsTable.overwriteModal.overwriteButtonText": "Écraser", + "savedObjectsManagement.objectsTable.overwriteModal.selectControlLabel": "ID d'objet", + "savedObjectsManagement.objectsTable.overwriteModal.title": "Écraser {type} ?", + "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionDescription": "Inspecter cet objet enregistré", + "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionName": "Inspecter", + "savedObjectsManagement.objectsTable.relationships.columnActionsName": "Actions", + "savedObjectsManagement.objectsTable.relationships.columnErrorDescription": "Erreur rencontrée avec la relation", + "savedObjectsManagement.objectsTable.relationships.columnErrorName": "Erreur", + "savedObjectsManagement.objectsTable.relationships.columnIdDescription": "ID de l'objet enregistré", + "savedObjectsManagement.objectsTable.relationships.columnIdName": "ID", + "savedObjectsManagement.objectsTable.relationships.columnRelationship.childAsValue": "Enfant", + "savedObjectsManagement.objectsTable.relationships.columnRelationship.parentAsValue": "Parent", + "savedObjectsManagement.objectsTable.relationships.columnRelationshipName": "Relation directe", + "savedObjectsManagement.objectsTable.relationships.columnTitleDescription": "Titre de l'objet enregistré", + "savedObjectsManagement.objectsTable.relationships.columnTitleName": "Titre", + "savedObjectsManagement.objectsTable.relationships.columnTypeDescription": "Type de l'objet enregistré", + "savedObjectsManagement.objectsTable.relationships.columnTypeName": "Type", + "savedObjectsManagement.objectsTable.relationships.invalidRelationShip": "Cet objet enregistré présente des relations non valides.", + "savedObjectsManagement.objectsTable.relationships.relationshipsTitle": "Voici les objets enregistrés associés à {title}. La suppression de ce {type} a un impact sur ses objets parents, mais pas sur ses enfants.", + "savedObjectsManagement.objectsTable.relationships.renderErrorMessage": "Erreur", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.childAsValue.view": "Enfant", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.name": "Relation directe", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.parentAsValue.view": "Parent", + "savedObjectsManagement.objectsTable.relationships.search.filters.type.name": "Type", + "savedObjectsManagement.objectsTable.searchBar.unableToParseQueryErrorMessage": "Impossible d'analyser la requête", + "savedObjectsManagement.objectsTable.table.columnActions.inspectActionDescription": "Inspecter cet objet enregistré", + "savedObjectsManagement.objectsTable.table.columnActions.inspectActionName": "Inspecter", + "savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionDescription": "Afficher les relations entre cet objet enregistré et d'autres objets enregistrés", + "savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionName": "Relations", + "savedObjectsManagement.objectsTable.table.columnActionsName": "Actions", + "savedObjectsManagement.objectsTable.table.columnTitleDescription": "Titre de l'objet enregistré", + "savedObjectsManagement.objectsTable.table.columnTitleName": "Titre", + "savedObjectsManagement.objectsTable.table.columnTypeDescription": "Type de l'objet enregistré", + "savedObjectsManagement.objectsTable.table.columnTypeName": "Type", + "savedObjectsManagement.objectsTable.table.deleteButtonLabel": "Supprimer", + "savedObjectsManagement.objectsTable.table.deleteButtonTitle": "Impossible de supprimer les objets enregistrés", + "savedObjectsManagement.objectsTable.table.exportButtonLabel": "Exporter", + "savedObjectsManagement.objectsTable.table.exportPopoverButtonLabel": "Exporter", + "savedObjectsManagement.objectsTable.table.typeFilterName": "Type", + "savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage": "Objet enregistré introuvable", + "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "Objets enregistrés introuvables", + "savedObjectsManagement.objectView.unableFindSavedObjectNotificationMessage": "Objet enregistré introuvable", + "savedObjectsManagement.view.fieldDoesNotExistErrorMessage": "Un champ associé à cet objet n'existe plus dans le modèle d'indexation.", + "savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage": "Le modèle d'indexation associé à cet objet n'existe plus.", + "savedObjectsManagement.view.savedObjectProblemErrorMessage": "Un problème est survenu avec cet objet enregistré.", + "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "La recherche enregistrée associée à cet objet n'existe plus.", + "savedObjectsManagement.view.viewItemButtonLabel": "Afficher {title}", + "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", + "share.advancedSettings.csv.separatorTitle": "Séparateur CSV", + "share.contextMenu.embedCodeLabel": "Incorporer le code", + "share.contextMenu.embedCodePanelTitle": "Incorporer le code", + "share.contextMenu.permalinkPanelTitle": "Permalien", + "share.contextMenu.permalinksLabel": "Permaliens", + "share.contextMenuTitle": "Partager ce {objectType}", + "share.urlPanel.canNotShareAsSavedObjectHelpText": "Impossible de partager comme objet enregistré tant que {objectType} n'a pas été enregistré.", + "share.urlPanel.copyIframeCodeButtonLabel": "Copier le code iFrame", + "share.urlPanel.copyLinkButtonLabel": "Copier le lien", + "share.urlPanel.generateLinkAsLabel": "Générer le lien en tant que", + "share.urlPanel.publicUrlHelpText": "Utilisez l'URL publique pour partager avec tout le monde. Elle permet un accès anonyme en une étape, en supprimant l'invite de connexion.", + "share.urlPanel.publicUrlLabel": "URL publique", + "share.urlPanel.savedObjectDescription": "Vous pouvez partager cette URL avec des personnes pour leur permettre de charger la version enregistrée la plus récente de ce {objectType}.", + "share.urlPanel.savedObjectLabel": "Objet enregistré", + "share.urlPanel.shortUrlHelpText": "Nous vous recommandons de partager des URL de snapshot raccourcies pour une compatibilité maximale. Internet Explorer présente des restrictions de longueur d'URL et certains analyseurs de wiki et de balisage ne fonctionnent pas bien avec les URL de snapshot longues, mais les URL courtes devraient bien fonctionner.", + "share.urlPanel.shortUrlLabel": "URL courte", + "share.urlPanel.snapshotDescription": "Les URL de snapshot encodent l'état actuel de {objectType} dans l'URL elle-même. Les modifications apportées au {objectType} enregistré ne seront pas visibles via cette URL.", + "share.urlPanel.snapshotLabel": "Snapshot", + "share.urlPanel.unableCreateShortUrlErrorMessage": "Impossible de créer une URL courte. Erreur : {errorMessage}.", + "share.urlPanel.urlGroupTitle": "URL", + "share.urlService.redirect.components.Error.title": "Erreur de redirection", + "share.urlService.redirect.components.Spinner.label": "Redirection…", + "share.urlService.redirect.RedirectManager.invalidParamParams": "Impossible d'analyser les paramètres du localisateur. Les paramètres du localisateur doivent être sérialisés en tant que JSON et définis au paramètre de recherche d'URL \"p\".", + "share.urlService.redirect.RedirectManager.locatorNotFound": "Le localisateur [ID = {id}] n'existe pas.", + "share.urlService.redirect.RedirectManager.missingParamLocator": "ID du localisateur non spécifié. Spécifiez le paramètre de recherche \"l\" dans l'URL ; ce devrait être un ID de localisateur existant.", + "share.urlService.redirect.RedirectManager.missingParamParams": "Paramètres du localisateur non spécifiés. Spécifiez le paramètre de recherche \"p\" dans l'URL ; ce devrait être un objet sérialisé JSON des paramètres du localisateur.", + "share.urlService.redirect.RedirectManager.missingParamVersion": "Version des paramètres du localisateur non spécifiée. Spécifiez le paramètre de recherche \"v\" dans l'URL ; ce devrait être la version de Kibana au moment de la génération des paramètres du localisateur.", + "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.", + "telemetry.callout.clusterStatisticsTitle": "Statistiques du cluster", + "telemetry.callout.errorLoadingClusterStatisticsDescription": "Une erreur inattendue s'est produite lors de la récupération des statistiques du cluster. Cela peut être dû à un échec d'Elasticsearch ou de Kibana, ou d'une erreur réseau. Vérifiez Kibana, puis rechargez la page et réessayez.", + "telemetry.callout.errorLoadingClusterStatisticsTitle": "Erreur lors du chargement des statistiques du cluster", + "telemetry.callout.errorUnprivilegedUserDescription": "Vous ne disposez pas de l'accès requis pour voir les statistiques non chiffrées du cluster.", + "telemetry.callout.errorUnprivilegedUserTitle": "Erreur lors de l'affichage des statistiques du cluster", + "telemetry.clusterData": "données du cluster", + "telemetry.optInErrorToastText": "Une erreur s'est produite lors de la définition des préférences relatives aux statistiques d'utilisation.", + "telemetry.optInErrorToastTitle": "Erreur", + "telemetry.optInNoticeSeenErrorTitle": "Erreur", + "telemetry.optInNoticeSeenErrorToastText": "Une erreur s'est produite lors du rejet de l'avis.", + "telemetry.optInSuccessOff": "Collecte des données d'utilisation désactivée.", + "telemetry.optInSuccessOn": "Collecte des données d'utilisation activée.", + "telemetry.readOurUsageDataPrivacyStatementLinkText": "Déclaration de confidentialité", + "telemetry.securityData": "données de sécurité des points de terminaison", + "telemetry.telemetryBannerDescription": "Vous souhaitez nous aider à améliorer la Suite Elastic ? La collecte de données d'utilisation est actuellement désactivée. En activant la collecte de données d'utilisation, vous nous aidez à gérer et à améliorer nos produits et nos services. Consultez notre {privacyStatementLink} pour plus d'informations.", + "telemetry.telemetryConfigAndLinkDescription": "En activant la collecte de données d'utilisation, vous nous aidez à gérer et à améliorer nos produits et nos services. Consultez notre {privacyStatementLink} pour plus d'informations.", + "telemetry.telemetryOptedInDisableUsage": "désactivez les données d'utilisation ici", + "telemetry.telemetryOptedInDismissMessage": "Rejeter", + "telemetry.telemetryOptedInNoticeDescription": "Pour en savoir plus sur la manière dont les données d'utilisation nous aident à gérer et à améliorer nos produits et nos services, consultez notre {privacyStatementLink}. Pour mettre fin à la collecte, {disableLink}.", + "telemetry.telemetryOptedInNoticeTitle": "Aidez-nous à améliorer la Suite Elastic.", + "telemetry.telemetryOptedInPrivacyStatement": "Déclaration de confidentialité", + "telemetry.usageDataTitle": "Données d'utilisation", + "telemetry.welcomeBanner.disableButtonLabel": "Désactiver", + "telemetry.welcomeBanner.enableButtonLabel": "Activer", + "telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "Déclaration de confidentialité", + "telemetry.welcomeBanner.title": "Aidez-nous à améliorer la Suite Elastic.", + "timelion.emptyExpressionErrorMessage": "Erreur Timelion : aucune expression fournie", + "timelion.expressionSuggestions.argument.description.acceptsText": "Accepte", + "timelion.expressionSuggestions.func.description.chainableHelpText": "Enchaînable", + "timelion.expressionSuggestions.func.description.dataSourceHelpText": "Source de données", + "timelion.fitFunctions.carry.downSampleErrorMessage": "N'utilisez pas la méthode fit \"carry\" pour sous-échantillonner, utilisez \"scale\" ou \"average\".", + "timelion.function.help": "Visualisation Timelion", + "timelion.help.functions.absHelpText": "Renvoyer la valeur absolue de chaque valeur dans la liste des séries", + "timelion.help.functions.aggregate.args.functionHelpText": "L'une des options suivantes : {functions}.", + "timelion.help.functions.aggregateHelpText": "Crée une ligne statique sur la base du résultat du traitement de tous les points de la série. Fonctions disponibles : {functions}", + "timelion.help.functions.bars.args.stackHelpText": "Vrai par défaut si les barres sont empilées", + "timelion.help.functions.bars.args.widthHelpText": "Largeur des barres en pixels", + "timelion.help.functions.barsHelpText": "Afficher la liste des séries sous la forme de barres", + "timelion.help.functions.color.args.colorHelpText": "Couleur des séries en valeurs hexadécimales, par ex. #c6c6c6 est un très joli gris clair. Si vous spécifiez plusieurs couleurs et que vous avez plusieurs séries, vous obtiendrez un dégradé, par ex. \"#00B1CC:#00FF94:#FF3A39:#CC1A6F\".", + "timelion.help.functions.colorHelpText": "Changer la couleur des séries", + "timelion.help.functions.common.args.fitHelpText": "Algorithme à utiliser pour adapter les séries à l'intervalle et à la période cible. Disponible : {fitFunctions}", + "timelion.help.functions.common.args.offsetHelpText": "Décalez la récupération des séries avec une expression de date, par ex. -1M pour afficher les événements d'il y a un mois comme s'ils se produisaient maintenant. Décalez les séries par rapport à la plage temporelle globale des graphiques en utilisant la valeur \"timerange\", par ex. \"timerange:-2\" pour obtenir un décalage correspondant à deux fois la plage temporelle globale du graphique dans le passé.", + "timelion.help.functions.condition.args.elseHelpText": "La valeur à laquelle le point sera défini si la comparaison est fausse. Si vous spécifiez une liste de séries, la première série sera utilisée.", + "timelion.help.functions.condition.args.ifHelpText": "La valeur à laquelle le point sera comparé. Si vous spécifiez une liste de séries, la première série sera utilisée.", + "timelion.help.functions.condition.args.operator.suggestions.eqHelpText": "égal", + "timelion.help.functions.condition.args.operator.suggestions.gteHelpText": "supérieur ou égal", + "timelion.help.functions.condition.args.operator.suggestions.gtHelpText": "supérieur à", + "timelion.help.functions.condition.args.operator.suggestions.lteHelpText": "inférieur ou égal", + "timelion.help.functions.condition.args.operator.suggestions.ltHelpText": "inférieur à", + "timelion.help.functions.condition.args.operator.suggestions.neHelpText": "différent", + "timelion.help.functions.condition.args.operatorHelpText": "Opérateur de comparaison à utiliser pour la comparaison ; les opérateurs valides sont eq (égal), ne (différent), lt (inférieur à), lte (inférieur ou égal), gt (supérieur à), gte (supérieur ou égal).", + "timelion.help.functions.condition.args.thenHelpText": "La valeur à laquelle le point sera défini si la comparaison est vraie. Si vous spécifiez une liste de séries, la première série sera utilisée.", + "timelion.help.functions.conditionHelpText": "Compare chaque point à un nombre ou au même point dans une autre série à l'aide d'un opérateur, puis définit sa valeur sur le résultat si la condition est vraie, avec un sinon facultatif.", + "timelion.help.functions.cusum.args.baseHelpText": "Numéro auquel commencer. Cela ajoute simplement ce numéro au début de la série", + "timelion.help.functions.cusumHelpText": "Renvoyez la somme cumulée d'une série, à partir d’une base.", + "timelion.help.functions.derivativeHelpText": "Tracez l'évolution des valeurs au fil du temps.", + "timelion.help.functions.divide.args.divisorHelpText": "Nombre de séries par lequel diviser. Une liste de plusieurs séries sera appliquée pour l'étiquette.", + "timelion.help.functions.divideHelpText": "Divise les valeurs d'une ou de plusieurs séries d'une liste de séries à chaque position, dans chaque série, de la liste de séries d'entrée.", + "timelion.help.functions.es.args.indexHelpText": "Index à interroger, caractères génériques acceptés. Fournissez le nom du modèle d'indexation pour les champs scriptés et le type de nom de champ devant les suggestions pour les arguments metrics, split et timefield.", + "timelion.help.functions.es.args.intervalHelpText": "**NE PAS UTILISER**. C'est amusant pour déboguer les fonctions fit, mais vous devriez vraiment utiliser le sélecteur d'intervalle.", + "timelion.help.functions.es.args.kibanaHelpText": "Respectez les filtres des tableaux de bord Kibana. Cela n'a d'effet qu’en cas d'utilisation dans des tableaux de bord Kibana", + "timelion.help.functions.es.args.metricHelpText": "Une agrégation d'indicateurs Elasticsearch Moyenne, Somme, Min, Max, Centiles ou Cardinalité, puis un champ. Par ex. \"sum:bytes\", \"percentiles:bytes:95,99,99.9\" ou simplement \"count\".", + "timelion.help.functions.es.args.qHelpText": "Requête dans la syntaxe de chaîne de requête Lucene", + "timelion.help.functions.es.args.splitHelpText": "Un champ Elasticsearch avec lequel diviser la série et une limite. Par ex. \"{hostnameSplitArg}\" pour obtenir les 10 premiers noms d'hôte.", + "timelion.help.functions.es.args.timefieldHelpText": "Champ de type \"date\" à utiliser pour l'axe X", + "timelion.help.functions.esHelpText": "Extraire des données d'une instance Elasticsearch", + "timelion.help.functions.firstHelpText": "Il s'agit d'une fonction interne qui renvoie simplement la liste de séries d'entrée. Ne l'utilisez pas.", + "timelion.help.functions.fit.args.modeHelpText": "L'algorithme à utiliser pour adapter les séries à la cible. L'une des options suivantes : {fitFunctions}.", + "timelion.help.functions.fitHelpText": "Remplit les valeurs nulles à l'aide d'une fonction fit définie.", + "timelion.help.functions.graphite.args.metricHelpText": "Indicateur Graphite à extraire, par ex. {metricExample}", + "timelion.help.functions.graphiteHelpText": "[expérimental] Extrayez des données de Graphite. Configurez votre serveur Graphite dans les paramètres avancés de Kibana.", + "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.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.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", + "timelion.help.functions.legend.args.columnsHelpText": "Nombre de colonnes à utiliser lors de la division de la légende", + "timelion.help.functions.legend.args.position.suggestions.falseHelpText": "désactiver la légende", + "timelion.help.functions.legend.args.position.suggestions.neHelpText": "placer la légende dans le coin nord-est", + "timelion.help.functions.legend.args.position.suggestions.nwHelpText": "placer la légende dans le coin nord-ouest", + "timelion.help.functions.legend.args.position.suggestions.seHelpText": "placer la légende dans le coin sud-est", + "timelion.help.functions.legend.args.position.suggestions.swHelpText": "placer la légende dans le coin sud-ouest", + "timelion.help.functions.legend.args.positionHelpText": "Coin dans lequel placer la légende : nw, ne, se ou sw. Il est également possible d'indiquer \"false\" pour désactiver la légende.", + "timelion.help.functions.legend.args.showTimeHelpText": "Afficher la valeur temporelle en légende lors du passage du curseur sur le graphique. Par défaut : true.", + "timelion.help.functions.legend.args.timeFormatHelpText": "Modèle de format moment.js. Par défaut : {defaultTimeFormat}", + "timelion.help.functions.legendHelpText": "Définir la position et le style de la légende sur le tracé", + "timelion.help.functions.lines.args.fillHelpText": "Nombre compris entre 0 et 10. À utiliser pour créer des graphiques en aires.", + "timelion.help.functions.lines.args.showHelpText": "Afficher ou masquer les lignes", + "timelion.help.functions.lines.args.stackHelpText": "Empiler les lignes, souvent équivoque. Utilisez au moins des remplissages si vous utilisez cette option.", + "timelion.help.functions.lines.args.stepsHelpText": "Afficher la ligne comme une étape ; autrement dit, ne pas interpoler entre les points", + "timelion.help.functions.lines.args.widthHelpText": "Épaisseur de ligne", + "timelion.help.functions.linesHelpText": "Afficher la liste de séries sous la forme de lignes", + "timelion.help.functions.log.args.baseHelpText": "Définir la base logarithmique ; 10 par défaut", + "timelion.help.functions.logHelpText": "Renvoyer la valeur logarithmique de chaque valeur de la liste des séries (base par défaut : 10)", + "timelion.help.functions.max.args.valueHelpText": "Définit le point sur la valeur existante ou la valeur transmise, selon la plus élevée des deux. Si une liste de séries est transmise, elle doit contenir exactement 1 série.", + "timelion.help.functions.maxHelpText": "Valeurs maximales d'une ou de plusieurs séries d'une liste de séries à chaque position, dans chaque série, de la liste de séries d'entrée.", + "timelion.help.functions.min.args.valueHelpText": "Définit le point sur la valeur existante ou la valeur transmise, selon la plus basse des deux. Si une liste de séries est transmise, elle doit contenir exactement 1 série.", + "timelion.help.functions.minHelpText": "Valeurs minimales d'une ou de plusieurs séries d'une liste de séries à chaque position, dans chaque série, de la liste de séries d'entrée.", + "timelion.help.functions.movingaverage.args.positionHelpText": "Position des points moyens par rapport à l'heure du résultat. L'une des options suivantes : {validPositions}.", + "timelion.help.functions.movingaverage.args.windowHelpText": "Nombre de points ou une expression mathématique de date (par ex. 1d, 1M) à utiliser pour calculer la moyenne. Si une expression mathématique de date est spécifiée, la fonction sera la plus proche possible compte tenu de l'intervalle sélectionné. Si l'expression mathématique de date n'est pas divisible uniformément par l'intervalle, les résultats peuvent sembler être anormaux.", + "timelion.help.functions.movingaverageHelpText": "Calculez la moyenne mobile pour une fenêtre donnée. Idéal pour lisser les séries avec beaucoup de bruit.", + "timelion.help.functions.movingstd.args.positionHelpText": "Position de la section de la fenêtre par rapport à l'heure du résultat. Les options sont {positions}. Par défaut : {defaultPosition}.", + "timelion.help.functions.movingstd.args.windowHelpText": "Nombre de points à utiliser pour calculer l'écart-type.", + "timelion.help.functions.movingstdHelpText": "Calculez l'écart-type mobile pour une fenêtre donnée. Utilise l'algorithme naïf en deux passes. Les erreurs d'arrondi peuvent devenir plus évidentes avec les séries très longues ou celles comportant de très grands nombres.", + "timelion.help.functions.multiply.args.multiplierHelpText": "Nombre de séries par lequel multiplier. Une liste de plusieurs séries sera appliquée pour l'étiquette.", + "timelion.help.functions.multiplyHelpText": "Multiplie les valeurs d'une ou de plusieurs séries d'une liste de séries à chaque position, dans chaque série, de la liste de séries d'entrée.", + "timelion.help.functions.notAllowedGraphiteUrl": "Cette URL Graphite n'est pas configurée dans le fichier kibana.yml.\n Veuillez configurer votre liste de serveurs Graphite dans le fichier kibana.yml, sous \"timelion.graphiteUrls\", puis\n en sélectionner un dans les paramètres avancés de Kibana.", + "timelion.help.functions.points.args.fillColorHelpText": "Couleur à utiliser pour remplir le point", + "timelion.help.functions.points.args.fillHelpText": "Nombre compris entre 0 et 10 représentant l'opacité du remplissage", + "timelion.help.functions.points.args.radiusHelpText": "Taille des points", + "timelion.help.functions.points.args.showHelpText": "Afficher ou non les points", + "timelion.help.functions.points.args.symbolHelpText": "Symbole de point. L'un des options suivantes : {validSymbols}", + "timelion.help.functions.points.args.weightHelpText": "Épaisseur de la ligne autour du point", + "timelion.help.functions.pointsHelpText": "Afficher les séries sous la forme de points", + "timelion.help.functions.precision.args.precisionHelpText": "Le nombre de chiffres à garder lors de la troncature de chaque valeur", + "timelion.help.functions.precisionHelpText": "Le nombre de chiffres à garder lors de la troncature de la partie décimale de la valeur", + "timelion.help.functions.props.args.globalHelpText": "Définir des propositions sur la liste de séries plutôt que sur chaque série", + "timelion.help.functions.propsHelpText": "À utiliser à vos risques et périls ; définit des propriétés arbitraires sur la série. Par exemple : {example}", + "timelion.help.functions.quandl.args.codeHelpText": "Le code Quandl à tracer. Disponible sur quandl.com.", + "timelion.help.functions.quandl.args.positionHelpText": "Certaines sources Quandl renvoient plusieurs séries. Laquelle utiliser ? Index basé sur 1.", + "timelion.help.functions.quandlHelpText": "\n [expérimental]\n Extrayez des données de quandl.com à l'aide du code Quandl. Définissez {quandlKeyField} sur votre clé d'API gratuite dans\n les paramètres avancés de Kibana. La limite de taux de l'API est très basse sans clé.", + "timelion.help.functions.range.args.maxHelpText": "Nouvelle valeur maximale", + "timelion.help.functions.range.args.minHelpText": "Nouvelle valeur minimale", + "timelion.help.functions.rangeHelpText": "Modifie le maximum et le minimum d'une série sans changer la forme.", + "timelion.help.functions.scaleInterval.args.intervalHelpText": "Le nouvel intervalle en notation mathématique de date, par ex. 1s pour 1 seconde. 1m, 5m, 1M, 1w, 1y, etc.", + "timelion.help.functions.scaleIntervalHelpText": "Scale une valeur (généralement une somme ou un décompte) à un nouvel intervalle. Par exemple, un taux par seconde.", + "timelion.help.functions.static.args.labelHelpText": "Une manière rapide de définir l'étiquette pour la série. Vous pouvez également utiliser la fonction .label().", + "timelion.help.functions.static.args.valueHelpText": "La valeur unique à afficher. Vous pouvez également passer plusieurs valeurs, elles seront interpolées uniformément sur la plage temporelle.", + "timelion.help.functions.staticHelpText": "Dessine une valeur unique sur le graphique", + "timelion.help.functions.subtract.args.termHelpText": "Nombre de séries à soustraire de l'entrée. Une liste de plusieurs séries sera appliquée pour l'étiquette.", + "timelion.help.functions.subtractHelpText": "Soustrait les valeurs d'une ou de plusieurs séries d'une liste de séries à chaque position, dans chaque série, de la liste de séries d'entrée.", + "timelion.help.functions.sum.args.termHelpText": "Nombre de séries à ajouter à l'entrée. Une liste de plusieurs séries sera appliquée pour l'étiquette.", + "timelion.help.functions.sumHelpText": "Ajoute les valeurs d'une ou de plusieurs séries d'une liste de séries à chaque position, dans chaque série, de la liste de séries d'entrée.", + "timelion.help.functions.title.args.titleHelpText": "Titre pour le tracé.", + "timelion.help.functions.titleHelpText": "Ajoute un titre en haut du tracé. En cas d’appel sur plusieurs listes de séries, le dernier appel est utilisé.", + "timelion.help.functions.trend.args.endHelpText": "Quand arrêter de calculer par rapport au début ou à la fin. Par exemple, -10 indique qu'il faut arrêter de calculer 10 points avant la fin, et +15 indique que le calcul doit s'arrêter 15 points après le début. Par défaut : 0", + "timelion.help.functions.trend.args.modeHelpText": "L'algorithme à utiliser pour générer la courbe de tendance. L'une des options suivantes : {validRegressions}.", + "timelion.help.functions.trend.args.startHelpText": "Quand commencer à calculer par rapport au début ou à la fin. Par exemple, -10 indique qu'il faut commencer à calculer 10 points avant la fin, et +15 indique que le calcul doit commencer 15 points après le début. Par défaut : 0", + "timelion.help.functions.trendHelpText": "Dessine une courbe de tendance à l'aide d'un algorithme de régression spécifié.", + "timelion.help.functions.trim.args.endHelpText": "Compartiments à retirer de la fin de la série. Par défaut : 1", + "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.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.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.", + "timelion.help.functions.yaxis.args.minHelpText": "Valeur min.", + "timelion.help.functions.yaxis.args.positionHelpText": "gauche ou droite", + "timelion.help.functions.yaxis.args.tickDecimalsHelpText": "Le nombre de décimales pour les étiquettes de graduation de l'axe Y.", + "timelion.help.functions.yaxis.args.unitsHelpText": "La fonction à utiliser pour mettre en forme les étiquettes de l'axe Y. L'une des options suivantes : {formatters}.", + "timelion.help.functions.yaxis.args.yaxisHelpText": "L'axe Y numéroté sur lequel tracer cette série, par exemple .yaxis(2) pour un deuxième axe Y.", + "timelion.help.functions.yaxisHelpText": "Configure une variété d'options pour l'axe Y, la plus importante étant sans doute celle permettant d'ajouter un énième (par ex. deuxième) axe Y.", + "timelion.noFunctionErrorMessage": "Fonction inconnue : {name}", + "timelion.panels.timechart.unknownIntervalErrorMessage": "Intervalle inconnu", + "timelion.requestHandlerErrorTitle": "Erreur de requête Timelion", + "timelion.serverSideErrors.argumentsOverflowErrorMessage": "Trop d'arguments transmis à : {functionName}", + "timelion.serverSideErrors.bucketsOverflowErrorMessage": "Nombre max. de compartiments dépassé : {bucketCount} sur {maxBuckets} autorisés. Sélectionnez un intervalle plus grand ou une période plus courte.", + "timelion.serverSideErrors.colorFunction.colorNotProvidedErrorMessage": "couleur non spécifiée", + "timelion.serverSideErrors.conditionFunction.unknownOperatorErrorMessage": "Opérateur inconnu", + "timelion.serverSideErrors.conditionFunction.wrongArgTypeErrorMessage": "doit être un nombre ou une liste de séries", + "timelion.serverSideErrors.esFunction.indexNotFoundErrorMessage": "Index Elasticsearch introuvable : {index}", + "timelion.serverSideErrors.holtFunction.missingParamsErrorMessage": "Vous devez spécifier une longueur de saison et une taille d'échantillon >= 2.", + "timelion.serverSideErrors.holtFunction.notEnoughPointsErrorMessage": "Au moins 2 points sont nécessaires pour utiliser le lissage exponentiel double.", + "timelion.serverSideErrors.movingaverageFunction.notValidPositionErrorMessage": "Les positions valides sont : {validPositions}.", + "timelion.serverSideErrors.movingstdFunction.notValidPositionErrorMessage": "Les positions valides sont : {validPositions}.", + "timelion.serverSideErrors.pointsFunction.notValidSymbolErrorMessage": "Les symboles valides sont : {validSymbols}.", + "timelion.serverSideErrors.quandlFunction.unsupportedIntervalErrorMessage": "Intervalle non pris en charge par quandl() : {interval}. Les intervalles pris en charge par quandl() sont les suivants : {intervals}.", + "timelion.serverSideErrors.sheetParseErrorMessage": "Attendu : {expectedDescription} au caractère {column}", + "timelion.serverSideErrors.unknownArgumentErrorMessage": "Argument inconnu pour {functionName} : {argumentName}", + "timelion.serverSideErrors.unknownArgumentTypeErrorMessage": "Type d'argument non pris en charge : {argument}", + "timelion.serverSideErrors.worldbankFunction.noDataErrorMessage": "La requête à la Banque mondiale a réussi, mais il n'y a pas de données pour {code}.", + "timelion.serverSideErrors.wrongFunctionArgumentTypeErrorMessage": "{functionName}({argumentName}) doit être l'une des options suivantes : {requiredTypes}. Obtenu : {actualType}", + "timelion.serverSideErrors.yaxisFunction.notSupportedUnitTypeErrorMessage": "{units} n'est pas un type d'unité pris en charge.", + "timelion.serverSideErrors.yaxisFunction.notValidCurrencyFormatErrorMessage": "La devise doit être un code à trois caractères.", + "timelion.timelionDescription": "Affichez des données temporelles sur un graphe.", + "timelion.uiSettings.defaultIndexDescription": "Index Elasticsearch par défaut dans lequel rechercher avec {esParam}", + "timelion.uiSettings.defaultIndexLabel": "Index par défaut", + "timelion.uiSettings.experimentalLabel": "expérimental", + "timelion.uiSettings.graphiteURLDescription": "{experimentalLabel} L'URL de l'hôte Graphite", + "timelion.uiSettings.graphiteURLLabel": "URL Graphite", + "timelion.uiSettings.legacyChartsLibraryDeprication": "Ce paramètre est déclassé et ne sera plus pris en charge à partir de la version 8.0.", + "timelion.uiSettings.legacyChartsLibraryDescription": "Active la bibliothèque de graphiques héritée pour les graphiques Timelion dans Visualize.", + "timelion.uiSettings.legacyChartsLibraryLabel": "Bibliothèque de graphiques Timelion héritée", + "timelion.uiSettings.maximumBucketsDescription": "Le nombre maximal de compartiments qu'une source de données unique peut renvoyer", + "timelion.uiSettings.maximumBucketsLabel": "Nombre maximal de compartiments", + "timelion.uiSettings.minimumIntervalDescription": "Le plus petit intervalle qui sera calculé lors de l'utilisation de l'option \"auto\"", + "timelion.uiSettings.minimumIntervalLabel": "Intervalle minimum", + "timelion.uiSettings.quandlKeyDescription": "{experimentalLabel} Votre clé d'API de www.quandl.com", + "timelion.uiSettings.quandlKeyLabel": "Clé Quandl", + "timelion.uiSettings.targetBucketsDescription": "Le nombre de compartiments visé lors de l'utilisation d'intervalles automatiques", + "timelion.uiSettings.targetBucketsLabel": "Compartiments cibles", + "timelion.uiSettings.timeFieldDescription": "Champ par défaut contenant un horodatage lors de l'utilisation de {esParam}", + "timelion.uiSettings.timeFieldLabel": "Champ temporel", + "timelion.vis.expressionLabel": "Expression Timelion", + "timelion.vis.interval.auto": "Auto", + "timelion.vis.interval.day": "1 jour", + "timelion.vis.interval.hour": "1 heure", + "timelion.vis.interval.minute": "1 minute", + "timelion.vis.interval.month": "1 mois", + "timelion.vis.interval.second": "1 seconde", + "timelion.vis.interval.week": "1 semaine", + "timelion.vis.interval.year": "1 an", + "timelion.vis.intervalLabel": "Intervalle", + "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", + "uiActions.actionPanel.more": "Plus", + "uiActions.actionPanel.title": "Options", + "uiActions.errors.incompatibleAction": "Action non compatible", + "uiActions.triggers.rowClickkDescription": "Un clic sur une ligne de tableau", + "uiActions.triggers.rowClickTitle": "Clic sur ligne de tableau", + "visDefaultEditor.advancedToggle.advancedLinkLabel": "Avancé", + "visDefaultEditor.agg.disableAggButtonTooltip": "Désactiver l'agrégation {aggTitle} de {schemaTitle}", + "visDefaultEditor.agg.enableAggButtonTooltip": "Activer l'agrégation {aggTitle} de {schemaTitle}", + "visDefaultEditor.agg.errorsAriaLabel": "L'agrégation {aggTitle} de {schemaTitle} présente des erreurs.", + "visDefaultEditor.agg.modifyPriorityButtonTooltip": "Modifier la priorité de l'agrégation {aggTitle} de {schemaTitle} en la faisant glisser", + "visDefaultEditor.agg.removeDimensionButtonTooltip": "Supprimer l'agrégation {aggTitle} de {schemaTitle}", + "visDefaultEditor.agg.toggleEditorButtonAriaLabel": "Activer/Désactiver l'éditeur {schema}", + "visDefaultEditor.aggAdd.addButtonLabel": "Ajouter", + "visDefaultEditor.aggAdd.addGroupButtonLabel": "Ajouter {groupNameLabel}", + "visDefaultEditor.aggAdd.addSubGroupButtonLabel": "Ajouter sous-{groupNameLabel}", + "visDefaultEditor.aggAdd.bucketLabel": "compartiment", + "visDefaultEditor.aggAdd.maxBuckets": "Nombre maximal de {groupNameLabel} atteint", + "visDefaultEditor.aggAdd.metricLabel": "indicateur", + "visDefaultEditor.aggParams.errors.aggWrongRunOrderErrorMessage": "Les agrégations \"{schema}\" doivent s'exécuter avant tous les autres compartiments.", + "visDefaultEditor.aggSelect.aggregationLabel": "Agrégation", + "visDefaultEditor.aggSelect.helpLinkLabel": "Aide {aggTitle}", + "visDefaultEditor.aggSelect.noCompatibleAggsDescription": "Le modèle d'indexation {indexPatternTitle} ne possède pas de champs regroupables.", + "visDefaultEditor.aggSelect.selectAggPlaceholder": "Choisir une agrégation", + "visDefaultEditor.aggSelect.subAggregationLabel": "Sous-agrégation", + "visDefaultEditor.buckets.mustHaveBucketErrorMessage": "Ajoutez un compartiment avec une agrégation Histogramme de date ou Histogramme.", + "visDefaultEditor.controls.aggNotValidLabel": "- agrégation non valide -", + "visDefaultEditor.controls.aggregateWith.noAggsErrorTooltip": "Le champ choisi n'a pas d'agrégations compatibles.", + "visDefaultEditor.controls.aggregateWithLabel": "Agréger avec", + "visDefaultEditor.controls.aggregateWithTooltip": "Choisissez une stratégie pour combiner plusieurs occurrences ou un champ à valeurs multiples en un seul indicateur.", + "visDefaultEditor.controls.changePrecisionLabel": "Modifier la précision lors d'un zoom sur la carte", + "visDefaultEditor.controls.columnsLabel": "Colonnes", + "visDefaultEditor.controls.customMetricLabel": "Indicateur personnalisé", + "visDefaultEditor.controls.dateRanges.acceptedDateFormatsLinkText": "Formats de date acceptables", + "visDefaultEditor.controls.dateRanges.addRangeButtonLabel": "Ajouter une plage", + "visDefaultEditor.controls.dateRanges.errorMessage": "Chaque plage doit avoir au moins une date valide.", + "visDefaultEditor.controls.dateRanges.fromColumnLabel": "De", + "visDefaultEditor.controls.dateRanges.removeRangeButtonAriaLabel": "Supprimer la plage allant de {from} à {to}", + "visDefaultEditor.controls.dateRanges.toColumnLabel": "Au", + "visDefaultEditor.controls.definiteMetricLabel": "Indicateur : {metric}", + "visDefaultEditor.controls.dotSizeRatioHelpText": "Remplacez le rapport du rayon du plus petit point par le plus grand point.", + "visDefaultEditor.controls.dotSizeRatioLabel": "Rapport de taille de point", + "visDefaultEditor.controls.dropPartialBucketsLabel": "Abandonner les compartiments partiels", + "visDefaultEditor.controls.dropPartialBucketsTooltip": "Retirez les compartiments qui s'étendent au-delà de la plage temporelle afin que l'histogramme ne commence pas et ne se termine pas avec des compartiments incomplets.", + "visDefaultEditor.controls.extendedBounds.errorMessage": "Le minimum doit être inférieur ou égal au maximum.", + "visDefaultEditor.controls.extendedBounds.maxLabel": "Max.", + "visDefaultEditor.controls.extendedBounds.minLabel": "Min.", + "visDefaultEditor.controls.extendedBoundsLabel": "Étendre les limites", + "visDefaultEditor.controls.extendedBoundsTooltip": "Le minimum et le maximum ne filtrent pas de résultats, mais étendent plutôt les limites de l'ensemble de résultats.", + "visDefaultEditor.controls.field.fieldIsNotExists": "Le champ \"{fieldParameter}\" associé à cet objet n'existe plus dans le modèle d'indexation. Veuillez utiliser un autre champ.", + "visDefaultEditor.controls.field.fieldLabel": "Champ", + "visDefaultEditor.controls.field.invalidFieldForAggregation": "Le champ enregistré \"{fieldParameter}\" du modèle d'indexation \"{indexPatternTitle}\" n'est pas valide pour une utilisation avec cette agrégation. Veuillez sélectionner un nouveau champ.", + "visDefaultEditor.controls.field.noCompatibleFieldsDescription": "Le modèle d'indexation {indexPatternTitle} ne contient aucun des types de champs compatibles suivants : {fieldTypes}.", + "visDefaultEditor.controls.field.selectFieldPlaceholder": "Sélectionner un champ", + "visDefaultEditor.controls.filters.addFilterButtonLabel": "Ajouter un filtre", + "visDefaultEditor.controls.filters.definiteFilterLabel": "Étiquette du filtre {index}", + "visDefaultEditor.controls.filters.filterLabel": "Filtre {index}", + "visDefaultEditor.controls.filters.labelPlaceholder": "Étiquette", + "visDefaultEditor.controls.filters.removeFilterButtonAriaLabel": "Supprimer ce filtre", + "visDefaultEditor.controls.filters.toggleFilterButtonAriaLabel": "Activer/Désactiver l'étiquette du filtre", + "visDefaultEditor.controls.includeExclude.addUnitButtonLabel": "Ajouter une valeur", + "visDefaultEditor.controls.ipRanges.addRangeButtonLabel": "Ajouter une plage", + "visDefaultEditor.controls.ipRanges.cidrMaskAriaLabel": "Masque CIDR : {mask}", + "visDefaultEditor.controls.ipRanges.cidrMasksButtonLabel": "Masques CIDR", + "visDefaultEditor.controls.ipRanges.fromToButtonLabel": "De/à", + "visDefaultEditor.controls.ipRanges.ipRangeFromAriaLabel": "Début de la plage d’IP : {value}", + "visDefaultEditor.controls.ipRanges.ipRangeToAriaLabel": "Fin de la plage d’IP : {value}", + "visDefaultEditor.controls.ipRanges.removeCidrMaskButtonAriaLabel": "Supprimer la valeur du masque CIDR de {mask}", + "visDefaultEditor.controls.ipRanges.removeEmptyCidrMaskButtonAriaLabel": "Supprimer la valeur par défaut du masque CIDR", + "visDefaultEditor.controls.ipRanges.removeRangeAriaLabel": "Supprimer la plage allant de {from} à {to}", + "visDefaultEditor.controls.ipRangesAriaLabel": "Plages d’IP", + "visDefaultEditor.controls.jsonInputLabel": "Entrée JSON", + "visDefaultEditor.controls.jsonInputTooltip": "Toutes les propriétés au format JSON ajoutées ici seront fusionnées avec la définition d'agrégation Elasticsearch pour cette section. Par exemple, \"shard_size\" pour une agrégation de termes.", + "visDefaultEditor.controls.maxBars.autoPlaceholder": "Auto", + "visDefaultEditor.controls.maxBars.maxBarsHelpText": "Les intervalles seront sélectionnés automatiquement en fonction des données disponibles. Le nombre maximal de barres ne peut jamais être supérieur à la valeur {histogramMaxBars} des paramètres avancés.", + "visDefaultEditor.controls.maxBars.maxBarsLabel": "Barres max.", + "visDefaultEditor.controls.metricLabel": "Indicateur", + "visDefaultEditor.controls.metrics.bucketTitle": "Compartiment", + "visDefaultEditor.controls.metrics.metricTitle": "Indicateur", + "visDefaultEditor.controls.numberInterval.autoInteralIsUsed": "L'intervalle automatique est utilisé.", + "visDefaultEditor.controls.numberInterval.minimumIntervalLabel": "Intervalle minimum", + "visDefaultEditor.controls.numberInterval.minimumIntervalTooltip": "L'intervalle sera scalé automatiquement si la valeur fournie crée plus de compartiments que ce qui est spécifié par la valeur {histogramMaxBars} dans les paramètres avancés.", + "visDefaultEditor.controls.numberInterval.selectIntervalPlaceholder": "Saisir un intervalle", + "visDefaultEditor.controls.numberList.addUnitButtonLabel": "Ajouter {unitName}", + "visDefaultEditor.controls.numberList.duplicateValueErrorMessage": "Dupliquez la valeur.", + "visDefaultEditor.controls.numberList.enterValuePlaceholder": "Saisir une valeur", + "visDefaultEditor.controls.numberList.invalidAscOrderErrorMessage": "La valeur n'est pas dans l'ordre croissant.", + "visDefaultEditor.controls.numberList.invalidRangeErrorMessage": "La valeur doit être comprise dans la plage allant de {min} à {max}.", + "visDefaultEditor.controls.numberList.removeUnitButtonAriaLabel": "Supprimer la valeur de rang de {value}", + "visDefaultEditor.controls.onlyRequestDataAroundMapExtentLabel": "Demander uniquement des données sur l'étendue de la carte", + "visDefaultEditor.controls.onlyRequestDataAroundMapExtentTooltip": "Appliquer l'agrégation de filtres geo_bounding_box pour réduire la zone d’intérêt à la zone d'affichage de la carte avec collier", + "visDefaultEditor.controls.orderAgg.alphabeticalLabel": "Alphabétique", + "visDefaultEditor.controls.orderAgg.orderByLabel": "Classer par", + "visDefaultEditor.controls.orderLabel": "Ordre", + "visDefaultEditor.controls.otherBucket.groupValuesLabel": "Regrouper les autres valeurs dans un compartiment séparé", + "visDefaultEditor.controls.otherBucket.groupValuesTooltip": "Les valeurs qui ne sont pas dans le top N sont regroupées dans ce compartiment. Pour inclure les documents avec des valeurs manquantes, activez l'option \"Afficher les valeurs manquantes\".", + "visDefaultEditor.controls.otherBucket.showMissingValuesLabel": "Afficher les valeurs manquantes", + "visDefaultEditor.controls.otherBucket.showMissingValuesTooltip": "Ne fonctionne que pour les champs de type \"chaîne\". Lorsque cette option est activée, les documents avec des valeurs manquantes sont inclus dans la recherche. Si ce compartiment est dans le top N, il apparaît dans le graphique. S'il n'est pas dans le top N et que l’option \"Regrouper les autres valeurs dans un compartiment séparé\" est activée, Elasticsearch ajoute les valeurs manquantes à \"l'autre\" compartiment.", + "visDefaultEditor.controls.percentileRanks.percentUnitNameText": "pour cent", + "visDefaultEditor.controls.percentileRanks.valuesLabel": "Valeurs", + "visDefaultEditor.controls.percentileRanks.valueUnitNameText": "valeur", + "visDefaultEditor.controls.percentiles.percentsLabel": "Pour cent", + "visDefaultEditor.controls.placeMarkersOffGridLabel": "Placer les marqueurs hors grille (utiliser un centroïde géométrique)", + "visDefaultEditor.controls.precisionLabel": "Précision", + "visDefaultEditor.controls.ranges.addRangeButtonLabel": "Ajouter une plage", + "visDefaultEditor.controls.ranges.fromLabel": "De", + "visDefaultEditor.controls.ranges.greaterThanOrEqualPrepend": "≥", + "visDefaultEditor.controls.ranges.greaterThanOrEqualTooltip": "Supérieur ou égal à", + "visDefaultEditor.controls.ranges.lessThanPrepend": "<", + "visDefaultEditor.controls.ranges.lessThanTooltip": "Inférieur à", + "visDefaultEditor.controls.ranges.removeRangeButtonAriaLabel": "Supprimer la plage allant de {from} à {to}", + "visDefaultEditor.controls.ranges.toLabel": "Au", + "visDefaultEditor.controls.rowsLabel": "Lignes", + "visDefaultEditor.controls.scaleMetricsLabel": "Scaler les valeurs des indicateurs (déclassé)", + "visDefaultEditor.controls.scaleMetricsTooltip": "Si vous sélectionnez un intervalle minimal manuel et qu'un intervalle plus grand est utilisé, l'activation de cette option entraînera le scaling des indicateurs de décompte et de somme à l'intervalle manuel sélectionné.", + "visDefaultEditor.controls.showEmptyBucketsLabel": "Afficher les compartiments vides", + "visDefaultEditor.controls.showEmptyBucketsTooltip": "Afficher tous les compartiments, pas seulement ceux avec des résultats", + "visDefaultEditor.controls.sizeLabel": "Taille", + "visDefaultEditor.controls.sizeTooltip": "Demander les K premiers résultats. Plusieurs résultats seront combinés par le biais de \"agréger avec\".", + "visDefaultEditor.controls.sortOnLabel": "Trier en fonction de", + "visDefaultEditor.controls.splitByLegend": "Diviser le graphique par lignes ou colonnes.", + "visDefaultEditor.controls.timeInterval.createsTooLargeBucketsTooltip": "Cet intervalle crée des compartiments trop grands pour permettre l’affichage dans la plage temporelle sélectionnée, il a donc été réduit.", + "visDefaultEditor.controls.timeInterval.createsTooManyBucketsTooltip": "Cet intervalle crée trop de compartiments pour permettre l’affichage dans la plage temporelle sélectionnée, il a donc été augmenté.", + "visDefaultEditor.controls.timeInterval.invalidFormatErrorMessage": "Format d'intervalle non valide.", + "visDefaultEditor.controls.timeInterval.minimumIntervalLabel": "Intervalle minimum", + "visDefaultEditor.controls.timeInterval.scaledHelpText": "Actuellement scalé à {bucketDescription}", + "visDefaultEditor.controls.timeInterval.selectIntervalPlaceholder": "Choisir un intervalle", + "visDefaultEditor.controls.timeInterval.selectOptionHelpText": "Choisissez une option ou créez une valeur personnalisée. Exemples : 30s, 20m, 24h, 2d, 1w, 1M", + "visDefaultEditor.controls.useAutoInterval": "Utiliser l'intervalle automatique", + "visDefaultEditor.editorConfig.dateHistogram.customInterval.helpText": "Doit être un multiple de l'intervalle de configuration : {interval}.", + "visDefaultEditor.editorConfig.histogram.interval.helpText": "Doit être un multiple de l'intervalle de configuration : {interval}.", + "visDefaultEditor.metrics.wrongLastBucketTypeErrorMessage": "La dernière agrégation de compartiments doit être \"Histogramme de date\" ou \"Histogramme\" lorsque vous utilisez l'agrégation d'indicateurs \"{type}\".", + "visDefaultEditor.options.colorRanges.errorText": "Chaque plage doit être supérieure à la précédente.", + "visDefaultEditor.options.colorSchema.colorSchemaLabel": "Schéma de couleurs", + "visDefaultEditor.options.colorSchema.howToChangeColorsDescription": "Les couleurs individuelles peuvent être modifiées dans la légende.", + "visDefaultEditor.options.colorSchema.resetColorsButtonLabel": "Réinitialiser les couleurs", + "visDefaultEditor.options.colorSchema.reverseColorSchemaLabel": "Inverser le schéma", + "visDefaultEditor.options.percentageMode.documentationLabel": "Documentation Numeral.js", + "visDefaultEditor.options.percentageMode.numeralLabel": "Modèle de format", + "visDefaultEditor.options.percentageMode.percentageModeLabel": "Mode de pourcentage", + "visDefaultEditor.options.rangeErrorMessage": "Les valeurs doivent être comprises entre {min} et {max}, inclus.", + "visDefaultEditor.options.vislibBasicOptions.legendPositionLabel": "Position de la légende", + "visDefaultEditor.options.vislibBasicOptions.showTooltipLabel": "Afficher l'infobulle", + "visDefaultEditor.palettePicker.label": "Palette de couleurs", + "visDefaultEditor.sidebar.autoApplyChangesLabelOff": "Application automatique désactivée", + "visDefaultEditor.sidebar.autoApplyChangesLabelOn": "Application automatique activée", + "visDefaultEditor.sidebar.autoApplyChangesOff": "Off", + "visDefaultEditor.sidebar.autoApplyChangesOffLabel": "Application automatique désactivée", + "visDefaultEditor.sidebar.autoApplyChangesOn": "On", + "visDefaultEditor.sidebar.autoApplyChangesOnLabel": "Application automatique activée", + "visDefaultEditor.sidebar.autoApplyChangesTooltip": "Met automatiquement à jour la visualisation à chaque modification.", + "visDefaultEditor.sidebar.collapseButtonAriaLabel": "Activer/Désactiver la barre latérale", + "visDefaultEditor.sidebar.discardChangesButtonLabel": "Abandonner", + "visDefaultEditor.sidebar.errorButtonTooltip": "Les erreurs dans les champs mis en évidence doivent être corrigées.", + "visDefaultEditor.sidebar.indexPatternAriaLabel": "Modèle d'indexation : {title}", + "visDefaultEditor.sidebar.savedSearch.goToDiscoverButtonText": "Afficher cette recherche dans Discover", + "visDefaultEditor.sidebar.savedSearch.linkButtonAriaLabel": "Lier à la recherche enregistrée. Cliquez pour en savoir plus ou rompre le lien.", + "visDefaultEditor.sidebar.savedSearch.popoverHelpText": "Les modifications apportées ultérieurement à cette recherche enregistrée sont reflétées dans la visualisation. Pour désactiver les mises à jour automatiques, supprimez le lien.", + "visDefaultEditor.sidebar.savedSearch.popoverTitle": "Lié à la recherche enregistrée", + "visDefaultEditor.sidebar.savedSearch.titleAriaLabel": "Recherche enregistrée : {title}", + "visDefaultEditor.sidebar.savedSearch.unlinkSavedSearchButtonText": "Supprimer le lien avec la recherche enregistrée", + "visDefaultEditor.sidebar.tabs.dataLabel": "Données", + "visDefaultEditor.sidebar.tabs.optionsLabel": "Options", + "visDefaultEditor.sidebar.updateChartButtonLabel": "Mettre à jour", + "visDefaultEditor.sidebar.updateInfoTooltip": "CTRL + Entrée est le raccourci clavier pour Mettre à jour.", + "visTypeMarkdown.function.font.help": "Paramètres de police.", + "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", + "visTypeMarkdown.tabs.dataText": "Données", + "visTypeMarkdown.tabs.optionsText": "Options", + "visTypeMetric.colorModes.backgroundOptionLabel": "Arrière-plan", + "visTypeMetric.colorModes.labelsOptionLabel": "Étiquettes", + "visTypeMetric.colorModes.noneOptionLabel": "Aucun", + "visTypeMetric.metricDescription": "Affiche un calcul sous la forme d'un nombre unique.", + "visTypeMetric.metricTitle": "Indicateur", + "visTypeMetric.params.color.useForLabel": "Utiliser la couleur pour", + "visTypeMetric.params.rangesTitle": "Plages", + "visTypeMetric.params.settingsTitle": "Paramètres", + "visTypeMetric.params.showTitleLabel": "Afficher le titre", + "visTypeMetric.params.style.fontSizeLabel": "Taille de police de l'indicateur en points", + "visTypeMetric.params.style.styleTitle": "Style", + "visTypeMetric.schemas.metricTitle": "Indicateur", + "visTypeMetric.schemas.splitGroupTitle": "Diviser le groupe", + "visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.deprecation": "La bibliothèque de graphiques héritée pour les camemberts dans Visualize est déclassée et ne sera plus prise en charge à partir de la version 8.0.", + "visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.description": "Active la bibliothèque de graphiques héritée pour les camemberts dans Visualize.", + "visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.name": "Bibliothèque de graphiques héritée pour les camemberts", + "visTypePie.controls.truncateLabel": "Tronquer", + "visTypePie.editors.pie.decimalSliderLabel": "Nombre maximal de décimales pour les pourcentages", + "visTypePie.editors.pie.distinctColorsLabel": "Utiliser des couleurs distinctes pour chaque section", + "visTypePie.editors.pie.donutLabel": "Graphique en anneau", + "visTypePie.editors.pie.labelPositionLabel": "Position de l'étiquette", + "visTypePie.editors.pie.labelsSettingsTitle": "Paramètres des étiquettes", + "visTypePie.editors.pie.nestedLegendLabel": "Imbriquer la légende", + "visTypePie.editors.pie.pieSettingsTitle": "Paramètres du camembert", + "visTypePie.editors.pie.showLabelsLabel": "Afficher les étiquettes", + "visTypePie.editors.pie.showTopLevelOnlyLabel": "Afficher uniquement le niveau supérieur", + "visTypePie.editors.pie.showValuesLabel": "Afficher les valeurs", + "visTypePie.editors.pie.valueFormatsLabel": "Valeurs", + "visTypePie.labelPositions.insideOrOutsideText": "Intérieur ou extérieur", + "visTypePie.labelPositions.insideText": "Intérieur", + "visTypePie.legendPositions.bottomText": "Bas", + "visTypePie.legendPositions.leftText": "Gauche", + "visTypePie.legendPositions.rightText": "Droite", + "visTypePie.legendPositions.topText": "Haut", + "visTypePie.pie.metricTitle": "Taille de section", + "visTypePie.pie.pieDescription": "Comparez des données proportionnellement à un ensemble.", + "visTypePie.pie.pieTitle": "Camembert", + "visTypePie.pie.segmentTitle": "Diviser les sections", + "visTypePie.pie.splitTitle": "Diviser le graphique", + "visTypePie.valuesFormats.percent": "Afficher le pourcentage", + "visTypePie.valuesFormats.value": "Afficher la valeur", + "visTypeTable.defaultAriaLabel": "Visualisation du tableau de données", + "visTypeTable.function.adimension.buckets": "Compartiments", + "visTypeTable.function.args.bucketsHelpText": "Configuration des dimensions de compartiment", + "visTypeTable.function.args.metricsHelpText": "Configuration des dimensions d’indicateur", + "visTypeTable.function.args.percentageColHelpText": "Nom de la colonne pour laquelle afficher le pourcentage", + "visTypeTable.function.args.perPageHelpText": "Le nombre de lignes dans une page de tableau est utilisé pour la pagination.", + "visTypeTable.function.args.rowHelpText": "La valeur de ligne est utilisée pour le mode de division de tableau. Définir sur \"vrai\" pour diviser par ligne", + "visTypeTable.function.args.showToolbarHelpText": "Définir sur \"vrai\" pour afficher la barre d'outils de la grille avec le bouton \"Exporter\"", + "visTypeTable.function.args.showTotalHelpText": "Définir sur \"vrai\" pour afficher le nombre total de lignes", + "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.dimension.metrics": "Indicateurs", + "visTypeTable.function.dimension.splitColumn": "Diviser par colonne", + "visTypeTable.function.dimension.splitRow": "Diviser par ligne", + "visTypeTable.function.help": "Visualisation du tableau", + "visTypeTable.params.defaultPercentageCol": "Ne pas afficher", + "visTypeTable.params.PercentageColLabel": "Colonne de pourcentage", + "visTypeTable.params.percentageTableColumnName": "Pourcentages de {title}", + "visTypeTable.params.perPageLabel": "Nombre max. de lignes par page", + "visTypeTable.params.showMetricsLabel": "Afficher les indicateurs pour chaque compartiment/niveau", + "visTypeTable.params.showPartialRowsLabel": "Afficher les lignes partielles", + "visTypeTable.params.showPartialRowsTip": "Affichez les lignes contenant des données partielles. Les indicateurs de chaque compartiment/niveau seront toujours calculés, même s'ils ne sont pas affichés.", + "visTypeTable.params.showToolbarLabel": "Afficher la barre d'outils", + "visTypeTable.params.showTotalLabel": "Afficher le total", + "visTypeTable.params.totalFunctionLabel": "Fonction de total", + "visTypeTable.sort.ascLabel": "Tri croissant", + "visTypeTable.sort.descLabel": "Tri décroissant", + "visTypeTable.tableCellFilter.filterForValueAriaLabel": "Filtrer sur la valeur : {cellContent}", + "visTypeTable.tableCellFilter.filterForValueText": "Filtrer sur la valeur", + "visTypeTable.tableCellFilter.filterOutValueAriaLabel": "Exclure la valeur : {cellContent}", + "visTypeTable.tableCellFilter.filterOutValueText": "Exclure la valeur", + "visTypeTable.tableVisDescription": "Affichez des données en lignes et en colonnes.", + "visTypeTable.tableVisEditorConfig.schemas.bucketTitle": "Diviser les lignes", + "visTypeTable.tableVisEditorConfig.schemas.metricTitle": "Indicateur", + "visTypeTable.tableVisEditorConfig.schemas.splitTitle": "Diviser le tableau", + "visTypeTable.tableVisTitle": "Tableau de données", + "visTypeTable.totalAggregations.averageText": "Moyenne", + "visTypeTable.totalAggregations.countText": "Décompte", + "visTypeTable.totalAggregations.maxText": "Max.", + "visTypeTable.totalAggregations.minText": "Min.", + "visTypeTable.totalAggregations.sumText": "Somme", + "visTypeTable.vis.controls.exportButtonAriaLabel": "Exporter {dataGridAriaLabel} au format CSV", + "visTypeTable.vis.controls.exportButtonFormulasWarning": "Votre fichier CSV contient des caractères que les applications de feuilles de calcul pourraient considérer comme des formules.", + "visTypeTable.vis.controls.exportButtonLabel": "Exporter", + "visTypeTable.vis.controls.formattedCSVButtonLabel": "Formaté", + "visTypeTable.vis.controls.rawCSVButtonLabel": "Brut", + "visTypeTagCloud.orientations.multipleText": "Multiple", + "visTypeTagCloud.orientations.rightAngledText": "Angle droit", + "visTypeTagCloud.orientations.singleText": "Unique", + "visTypeTagCloud.scales.linearText": "Linéaire", + "visTypeTagCloud.scales.logText": "Log", + "visTypeTagCloud.scales.squareRootText": "Racine carrée", + "visTypeTagCloud.vis.schemas.metricTitle": "Taille de balise", + "visTypeTagCloud.vis.schemas.segmentTitle": "Balises", + "visTypeTagCloud.vis.tagCloudDescription": "Affichez la fréquence des mots avec la taille de police.", + "visTypeTagCloud.vis.tagCloudTitle": "Nuage de balises", + "visTypeTagCloud.visParams.fontSizeLabel": "Plage de taille de police en pixels", + "visTypeTagCloud.visParams.orientationsLabel": "Orientations", + "visTypeTagCloud.visParams.showLabelToggleLabel": "Afficher l'étiquette", + "visTypeTagCloud.visParams.textScaleLabel": "Échelle de texte", + "visTypeTimeseries.addDeleteButtons.addButtonDefaultTooltip": "Ajouter", + "visTypeTimeseries.addDeleteButtons.cloneButtonDefaultTooltip": "Cloner", + "visTypeTimeseries.addDeleteButtons.deleteButtonDefaultTooltip": "Supprimer", + "visTypeTimeseries.addDeleteButtons.reEnableTooltip": "Réactiver", + "visTypeTimeseries.addDeleteButtons.temporarilyDisableTooltip": "Désactiver temporairement", + "visTypeTimeseries.advancedSettings.maxBucketsText": "A un impact sur la densité de l'histogramme TSVB. Doit être défini sur une valeur supérieure à \"histogram:maxBars\".", + "visTypeTimeseries.advancedSettings.maxBucketsTitle": "Limite de compartiments TSVB", + "visTypeTimeseries.aggRow.addMetricButtonTooltip": "Ajouter un indicateur", + "visTypeTimeseries.aggRow.deleteMetricButtonTooltip": "Supprimer un indicateur", + "visTypeTimeseries.aggSelect.aggGroups.metricAggLabel": "Agrégations d'indicateurs", + "visTypeTimeseries.aggSelect.aggGroups.parentPipelineAggLabel": "Agrégations de pipelines parents", + "visTypeTimeseries.aggSelect.aggGroups.siblingPipelineAggLabel": "Agrégations de pipelines enfants", + "visTypeTimeseries.aggSelect.aggGroups.specialAggLabel": "Agrégations spéciales", + "visTypeTimeseries.aggSelect.selectAggPlaceholder": "Sélectionner une agrégation", + "visTypeTimeseries.annotationsEditor.addDataSourceButtonLabel": "Ajouter une source de données", + "visTypeTimeseries.annotationsEditor.dataSourcesLabel": "Sources de données", + "visTypeTimeseries.annotationsEditor.fieldsLabel": "Champs (requis – chemins séparés par des virgules)", + "visTypeTimeseries.annotationsEditor.howToCreateAnnotationDataSourceDescription": "Cliquez sur le bouton ci-dessous pour créer une source de données d'annotation.", + "visTypeTimeseries.annotationsEditor.iconLabel": "Icône (requis)", + "visTypeTimeseries.annotationsEditor.ignoreGlobalFiltersLabel": "Ignorer les filtres globaux ?", + "visTypeTimeseries.annotationsEditor.ignorePanelFiltersLabel": "Ignorer les filtres de panneau ?", + "visTypeTimeseries.annotationsEditor.queryStringLabel": "Chaîne de requête", + "visTypeTimeseries.annotationsEditor.rowTemplateHelpText": "eg.{rowTemplateExample}", + "visTypeTimeseries.annotationsEditor.rowTemplateLabel": "Modèle de ligne (requis)", + "visTypeTimeseries.annotationsEditor.timeFieldLabel": "Champ temporel (requis)", + "visTypeTimeseries.axisLabelOptions.axisLabel": "par {unitValue} {unitString}", + "visTypeTimeseries.calculateLabel.bucketScriptsLabel": "Script de compartiment", + "visTypeTimeseries.calculateLabel.countLabel": "Décompte", + "visTypeTimeseries.calculateLabel.filterRatioLabel": "Rapport de filtre", + "visTypeTimeseries.calculateLabel.mathLabel": "Mathématique", + "visTypeTimeseries.calculateLabel.positiveRateLabel": "Taux de compteur de {field}", + "visTypeTimeseries.calculateLabel.seriesAggLabel": "Agrégation de séries ({metricFunction})", + "visTypeTimeseries.calculateLabel.staticValueLabel": "Valeur statique de {metricValue}", + "visTypeTimeseries.calculateLabel.unknownLabel": "Inconnu", + "visTypeTimeseries.calculation.aggregationLabel": "Agrégation", + "visTypeTimeseries.calculation.painlessScriptDescription": "Les variables sont des clés sur l'objet {params}, c.-à-d. {paramsName}. Pour accéder à l'intervalle de compartiment (en millisecondes), utilisez {paramsInterval}.", + "visTypeTimeseries.calculation.painlessScriptLabel": "Script Painless", + "visTypeTimeseries.calculation.variablesLabel": "Variables", + "visTypeTimeseries.colorPicker.clearIconLabel": "Effacer", + "visTypeTimeseries.colorPicker.notAccessibleAriaLabel": "Sélecteur de couleur, non accessible", + "visTypeTimeseries.colorPicker.notAccessibleWithValueAriaLabel": "Sélecteur de couleur ({value}), non accessible", + "visTypeTimeseries.colorRules.adjustChartSizeAriaLabel": "Utilisez les flèches haut/bas pour ajuster la taille du graphique.", + "visTypeTimeseries.colorRules.defaultPrimaryNameLabel": "arrière-plan", + "visTypeTimeseries.colorRules.defaultSecondaryNameLabel": "texte", + "visTypeTimeseries.colorRules.emptyLabel": "vide", + "visTypeTimeseries.colorRules.greaterThanLabel": "> supérieur à", + "visTypeTimeseries.colorRules.greaterThanOrEqualLabel": ">= supérieur ou égal à", + "visTypeTimeseries.colorRules.ifMetricIsLabel": "si l'indicateur est", + "visTypeTimeseries.colorRules.lessThanLabel": "< inférieur à", + "visTypeTimeseries.colorRules.lessThanOrEqualLabel": "<= inférieur ou égal à", + "visTypeTimeseries.colorRules.setPrimaryColorLabel": "Définissez {primaryName} sur", + "visTypeTimeseries.colorRules.setSecondaryColorLabel": "et {secondaryName} sur", + "visTypeTimeseries.colorRules.valueAriaLabel": "Valeur", + "visTypeTimeseries.cumulativeSum.aggregationLabel": "Agrégation", + "visTypeTimeseries.cumulativeSum.metricLabel": "Indicateur", + "visTypeTimeseries.dataFormatPicker.bytesLabel": "Octets", + "visTypeTimeseries.dataFormatPicker.customLabel": "Personnalisé", + "visTypeTimeseries.dataFormatPicker.decimalPlacesLabel": "Décimales", + "visTypeTimeseries.dataFormatPicker.durationLabel": "Durée", + "visTypeTimeseries.dataFormatPicker.fromLabel": "De", + "visTypeTimeseries.dataFormatPicker.numberLabel": "Nombre", + "visTypeTimeseries.dataFormatPicker.percentLabel": "Pour cent", + "visTypeTimeseries.dataFormatPicker.toLabel": "À", + "visTypeTimeseries.defaultDataFormatterLabel": "Formateur de données", + "visTypeTimeseries.derivative.aggregationLabel": "Agrégation", + "visTypeTimeseries.derivative.metricLabel": "Indicateur", + "visTypeTimeseries.derivative.unitsLabel": "Unités (1s, 1m, etc.)", + "visTypeTimeseries.durationOptions.daysLabel": "Jours", + "visTypeTimeseries.durationOptions.hoursLabel": "Heures", + "visTypeTimeseries.durationOptions.humanize": "Lisible par l'utilisateur", + "visTypeTimeseries.durationOptions.microsecondsLabel": "Microsecondes", + "visTypeTimeseries.durationOptions.millisecondsLabel": "Millisecondes", + "visTypeTimeseries.durationOptions.minutesLabel": "Minutes", + "visTypeTimeseries.durationOptions.monthsLabel": "Mois", + "visTypeTimeseries.durationOptions.nanosecondsLabel": "Nanosecondes", + "visTypeTimeseries.durationOptions.picosecondsLabel": "Picosecondes", + "visTypeTimeseries.durationOptions.secondsLabel": "Secondes", + "visTypeTimeseries.durationOptions.weeksLabel": "Semaines", + "visTypeTimeseries.durationOptions.yearsLabel": "Années", + "visTypeTimeseries.emptyTextValue": "(vide)", + "visTypeTimeseries.error.requestForPanelFailedErrorMessage": "La requête pour ce panneau a échoué.", + "visTypeTimeseries.fetchFields.loadIndexPatternFieldsErrorMessage": "Impossible de charger les champs index_pattern", + "visTypeTimeseries.fieldSelect.fieldIsNotValid": "Le champ \"{fieldParameter}\" n'est pas valide pour une utilisation avec l'index actuel. Veuillez sélectionner un nouveau champ.", + "visTypeTimeseries.fieldSelect.selectFieldPlaceholder": "Sélectionner un champ…", + "visTypeTimeseries.filterRatio.aggregationLabel": "Agrégation", + "visTypeTimeseries.filterRatio.denominatorLabel": "Dénominateur", + "visTypeTimeseries.filterRatio.fieldLabel": "Champ", + "visTypeTimeseries.filterRatio.metricAggregationLabel": "Agrégation d'indicateurs", + "visTypeTimeseries.filterRatio.numeratorLabel": "Numérateur", + "visTypeTimeseries.function.help": "Visualisation TSVB", + "visTypeTimeseries.gauge.dataTab.dataButtonLabel": "Données", + "visTypeTimeseries.gauge.dataTab.metricsButtonLabel": "Indicateurs", + "visTypeTimeseries.gauge.editor.addSeriesTooltip": "Ajouter une série", + "visTypeTimeseries.gauge.editor.cloneSeriesTooltip": "Cloner la série", + "visTypeTimeseries.gauge.editor.deleteSeriesTooltip": "Supprimer la série", + "visTypeTimeseries.gauge.editor.labelPlaceholder": "Étiquette", + "visTypeTimeseries.gauge.editor.toggleEditorAriaLabel": "Activer/Désactiver l'éditeur de séries", + "visTypeTimeseries.gauge.optionsTab.backgroundColorLabel": "Couleur d'arrière-plan :", + "visTypeTimeseries.gauge.optionsTab.colorRulesLabel": "Règles de couleur", + "visTypeTimeseries.gauge.optionsTab.dataLabel": "Données", + "visTypeTimeseries.gauge.optionsTab.gaugeLineWidthLabel": "Largeur de la ligne de jauge", + "visTypeTimeseries.gauge.optionsTab.gaugeMaxLabel": "Jauge max. (vide pour auto)", + "visTypeTimeseries.gauge.optionsTab.gaugeStyleLabel": "Style de jauge", + "visTypeTimeseries.gauge.optionsTab.ignoreGlobalFilterLabel": "Ignorer le filtre global ?", + "visTypeTimeseries.gauge.optionsTab.innerColorLabel": "Couleur intérieure :", + "visTypeTimeseries.gauge.optionsTab.innerLineWidthLabel": "Largeur de la ligne intérieure", + "visTypeTimeseries.gauge.optionsTab.optionsButtonLabel": "Options", + "visTypeTimeseries.gauge.optionsTab.panelFilterLabel": "Filtre de panneau", + "visTypeTimeseries.gauge.optionsTab.panelOptionsButtonLabel": "Options du panneau", + "visTypeTimeseries.gauge.optionsTab.styleLabel": "Style", + "visTypeTimeseries.gauge.styleOptions.circleLabel": "Cercle", + "visTypeTimeseries.gauge.styleOptions.halfCircleLabel": "Demi-cercle", + "visTypeTimeseries.getInterval.daysLabel": "jours", + "visTypeTimeseries.getInterval.hoursLabel": "heures", + "visTypeTimeseries.getInterval.minutesLabel": "minutes", + "visTypeTimeseries.getInterval.monthsLabel": "mois", + "visTypeTimeseries.getInterval.secondsLabel": "secondes", + "visTypeTimeseries.getInterval.weeksLabel": "semaines", + "visTypeTimeseries.getInterval.yearsLabel": "années", + "visTypeTimeseries.handleErrorResponse.unexpectedError": "Erreur inattendue", + "visTypeTimeseries.iconSelect.asteriskLabel": "Astérisque", + "visTypeTimeseries.iconSelect.bellLabel": "Cloche", + "visTypeTimeseries.iconSelect.boltLabel": "Éclair", + "visTypeTimeseries.iconSelect.bombLabel": "Bombe", + "visTypeTimeseries.iconSelect.bugLabel": "Bug", + "visTypeTimeseries.iconSelect.commentLabel": "Commentaire", + "visTypeTimeseries.iconSelect.exclamationCircleLabel": "Cercle exclamation", + "visTypeTimeseries.iconSelect.exclamationTriangleLabel": "Triangle exclamation", + "visTypeTimeseries.iconSelect.fireLabel": "Feu", + "visTypeTimeseries.iconSelect.flagLabel": "Drapeau", + "visTypeTimeseries.iconSelect.heartLabel": "Cœur", + "visTypeTimeseries.iconSelect.mapMarkerLabel": "Repère", + "visTypeTimeseries.iconSelect.mapPinLabel": "Punaise", + "visTypeTimeseries.iconSelect.starLabel": "Étoile", + "visTypeTimeseries.iconSelect.tagLabel": "Balise", + "visTypeTimeseries.indexPattern.detailLevel": "Niveau de détail", + "visTypeTimeseries.indexPattern.detailLevelAriaLabel": "Niveau de détail", + "visTypeTimeseries.indexPattern.detailLevelHelpText": "Contrôle les intervalles auto et gte en fonction de la plage temporelle. Les paramètres avancés {histogramTargetBars} et {histogramMaxBars} ont un impact sur l'intervalle par défaut.", + "visTypeTimeseries.indexPattern.dropLastBucketLabel": "Abandonner le dernier compartiment ?", + "visTypeTimeseries.indexPattern.finest": "Plus fin", + "visTypeTimeseries.indexPattern.intervalHelpText": "Exemples : auto, 1m, 1d, 7d, 1y, >=1m", + "visTypeTimeseries.indexPattern.intervalLabel": "Intervalle", + "visTypeTimeseries.indexPattern.timeFieldLabel": "Champ temporel", + "visTypeTimeseries.indexPattern.timeRange.entireTimeRange": "Toute la plage temporelle", + "visTypeTimeseries.indexPattern.timeRange.error": "Vous ne pouvez pas utiliser \"{mode}\" avec le type d'index actuel.", + "visTypeTimeseries.indexPattern.timeRange.hint": "Ce paramètre contrôle la période utilisée pour la mise en correspondance des documents. L'option \"Toute la plage temporelle\" mettra en correspondance tous les documents sélectionnés dans le sélecteur d'heure. L'option \"Dernière valeur\" ne mettra en correspondance que les documents pour l'intervalle spécifié à partir de la fin de la plage temporelle.", + "visTypeTimeseries.indexPattern.timeRange.label": "Mode de plage temporelle des données", + "visTypeTimeseries.indexPattern.timeRange.lastValue": "Dernière valeur", + "visTypeTimeseries.indexPattern.timeRange.selectTimeRange": "Sélectionner", + "visTypeTimeseries.indexPattern.сoarse": "Grossier", + "visTypeTimeseries.indexPatternSelect.label": "Modèle d'indexation", + "visTypeTimeseries.indexPatternSelect.switchModePopover.areaLabel": "Configurer le mode de sélection du modèle d'indexation", + "visTypeTimeseries.indexPatternSelect.switchModePopover.title": "Mode de sélection du modèle d'indexation", + "visTypeTimeseries.indexPatternSelect.switchModePopover.useKibanaIndices": "Utiliser uniquement les modèles d'indexation Kibana", + "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", + "visTypeTimeseries.lastValueModeIndicator.lastValueModeBadgeAriaLabel": "Afficher les détails de la dernière valeur", + "visTypeTimeseries.lastValueModeIndicator.panelInterval": "Intervalle : {formattedPanelInterval}", + "visTypeTimeseries.lastValueModePopover.gearButton": "Modifier l'option d'affichage de l'indicateur Dernière valeur", + "visTypeTimeseries.lastValueModePopover.switch": "Afficher l'étiquette lors de l'utilisation du mode Dernière valeur", + "visTypeTimeseries.lastValueModePopover.title": "Options de Dernière valeur", + "visTypeTimeseries.markdown.alignOptions.bottomLabel": "Bas", + "visTypeTimeseries.markdown.alignOptions.middleLabel": "Milieu", + "visTypeTimeseries.markdown.alignOptions.topLabel": "Haut", + "visTypeTimeseries.markdown.dataTab.dataButtonLabel": "Données", + "visTypeTimeseries.markdown.dataTab.metricsButtonLabel": "Indicateurs", + "visTypeTimeseries.markdown.editor.addSeriesTooltip": "Ajouter une série", + "visTypeTimeseries.markdown.editor.cloneSeriesTooltip": "Cloner la série", + "visTypeTimeseries.markdown.editor.deleteSeriesTooltip": "Supprimer la série", + "visTypeTimeseries.markdown.editor.labelPlaceholder": "Étiquette", + "visTypeTimeseries.markdown.editor.toggleEditorAriaLabel": "Activer/Désactiver l'éditeur de séries", + "visTypeTimeseries.markdown.editor.variableNamePlaceholder": "Nom de la variable", + "visTypeTimeseries.markdown.optionsTab.backgroundColorLabel": "Couleur d'arrière-plan :", + "visTypeTimeseries.markdown.optionsTab.customCSSLabel": "CSS personnalisé (prend en charge Less)", + "visTypeTimeseries.markdown.optionsTab.dataLabel": "Données", + "visTypeTimeseries.markdown.optionsTab.ignoreGlobalFilterLabel": "Ignorer le filtre global ?", + "visTypeTimeseries.markdown.optionsTab.openLinksInNewTab": "Ouvrir les liens dans un nouvel onglet ?", + "visTypeTimeseries.markdown.optionsTab.optionsButtonLabel": "Options", + "visTypeTimeseries.markdown.optionsTab.panelFilterLabel": "Filtre de panneau", + "visTypeTimeseries.markdown.optionsTab.panelOptionsButtonLabel": "Options du panneau", + "visTypeTimeseries.markdown.optionsTab.showScrollbarsLabel": "Afficher les barres de défilement ?", + "visTypeTimeseries.markdown.optionsTab.styleLabel": "Style", + "visTypeTimeseries.markdown.optionsTab.verticalAlignmentLabel": "Alignement vertical :", + "visTypeTimeseries.markdownEditor.howToAccessEntireTreeDescription": "Il existe également une variable spéciale nommée {all} que vous pouvez utiliser pour accéder à l'ensemble de l'arborescence. C'est utile pour créer des listes avec des données à l'aide d'une action Regrouper par :", + "visTypeTimeseries.markdownEditor.howToUseVariablesInMarkdownDescription": "Les variables suivantes peuvent être utilisées dans Markdown à l'aide de la syntaxe Handlebar (moustache). {handlebarLink} sur les expressions disponibles.", + "visTypeTimeseries.markdownEditor.howUseVariablesInMarkdownDescription.documentationLinkText": "Cliquer ici pour la documentation", + "visTypeTimeseries.markdownEditor.nameLabel": "Nom", + "visTypeTimeseries.markdownEditor.noVariablesAvailableDescription": "Aucune variable disponible pour les indicateurs de données sélectionnés.", + "visTypeTimeseries.markdownEditor.valueLabel": "Valeur", + "visTypeTimeseries.math.aggregationLabel": "Agrégation", + "visTypeTimeseries.math.expressionDescription": "Ce champ utilise des expressions mathématiques de base (voir {link}). Les variables sont des clés sur l'objet {params}, c.-à-d. {paramsName}. Pour accéder à toutes les données, utilisez {paramsValues} pour un tableau de valeurs et {paramsTimestamps} pour un tableau d’horodatages. {paramsTimestamp} est disponible pour l'horodatage du compartiment actuel, {paramsIndex} est disponible pour l'index du compartiment actuel et {paramsInterval} est disponible pour l'intervalle en millisecondes.", + "visTypeTimeseries.math.expressionDescription.tinyMathLinkText": "TinyMath", + "visTypeTimeseries.math.expressionLabel": "Expression", + "visTypeTimeseries.math.variablesLabel": "Variables", + "visTypeTimeseries.metric.dataTab.dataButtonLabel": "Données", + "visTypeTimeseries.metric.dataTab.metricsButtonLabel": "Indicateurs", + "visTypeTimeseries.metric.editor.addSeriesTooltip": "Ajouter une série", + "visTypeTimeseries.metric.editor.cloneSeriesTooltip": "Cloner la série", + "visTypeTimeseries.metric.editor.deleteSeriesTooltip": "Supprimer la série", + "visTypeTimeseries.metric.editor.labelPlaceholder": "Étiquette", + "visTypeTimeseries.metric.editor.toggleEditorAriaLabel": "Activer/Désactiver l'éditeur de séries", + "visTypeTimeseries.metric.optionsTab.colorRulesLabel": "Règles de couleur", + "visTypeTimeseries.metric.optionsTab.dataLabel": "Données", + "visTypeTimeseries.metric.optionsTab.ignoreGlobalFilterLabel": "Ignorer le filtre global ?", + "visTypeTimeseries.metric.optionsTab.optionsButtonLabel": "Options", + "visTypeTimeseries.metric.optionsTab.panelFilterLabel": "Filtre de panneau", + "visTypeTimeseries.metric.optionsTab.panelOptionsButtonLabel": "Options du panneau", + "visTypeTimeseries.metricMissingErrorMessage": "Indicateur manquant {field}", + "visTypeTimeseries.metricSelect.selectMetricPlaceholder": "Sélectionner l'indicateur…", + "visTypeTimeseries.missingPanelConfigDescription": "Configuration de panneau manquante pour \"{modelType}\"", + "visTypeTimeseries.movingAverage.aggregationLabel": "Agrégation", + "visTypeTimeseries.movingAverage.alpha": "Alpha", + "visTypeTimeseries.movingAverage.beta": "Bêta", + "visTypeTimeseries.movingAverage.gamma": "Gamma", + "visTypeTimeseries.movingAverage.metricLabel": "Indicateur", + "visTypeTimeseries.movingAverage.model.selectPlaceholder": "Sélectionner", + "visTypeTimeseries.movingAverage.modelLabel": "Modèle", + "visTypeTimeseries.movingAverage.modelOptions.exponentiallyWeightedLabel": "Pondéré exponentiellement", + "visTypeTimeseries.movingAverage.modelOptions.holtLinearLabel": "Holt–Linéaire", + "visTypeTimeseries.movingAverage.modelOptions.holtWintersLabel": "Holt-Winters", + "visTypeTimeseries.movingAverage.modelOptions.linearLabel": "Linéaire", + "visTypeTimeseries.movingAverage.modelOptions.simpleLabel": "Simple", + "visTypeTimeseries.movingAverage.multiplicative": "Multiplicative", + "visTypeTimeseries.movingAverage.multiplicative.selectPlaceholder": "Sélectionner", + "visTypeTimeseries.movingAverage.multiplicativeOptions.false": "Faux", + "visTypeTimeseries.movingAverage.multiplicativeOptions.true": "Vrai", + "visTypeTimeseries.movingAverage.period": "Période", + "visTypeTimeseries.movingAverage.windowSizeHint": "La fenêtre doit toujours être au moins deux fois plus grande que la période.", + "visTypeTimeseries.movingAverage.windowSizeLabel": "Taille de la fenêtre", + "visTypeTimeseries.noButtonLabel": "Non", + "visTypeTimeseries.percentile.aggregationLabel": "Agrégation", + "visTypeTimeseries.percentile.fieldLabel": "Champ", + "visTypeTimeseries.percentile.fillToLabel": "Remplir à :", + "visTypeTimeseries.percentile.modeLabel": "Mode :", + "visTypeTimeseries.percentile.modeOptions.bandLabel": "Bande", + "visTypeTimeseries.percentile.modeOptions.lineLabel": "Ligne", + "visTypeTimeseries.percentile.percentile": "Centile", + "visTypeTimeseries.percentile.percentileAriaLabel": "Centile", + "visTypeTimeseries.percentile.percents": "Pour cent", + "visTypeTimeseries.percentile.shadeLabel": "Ombre (0 à 1) :", + "visTypeTimeseries.percentileHdr.numberOfSignificantValueDigits": "Nombre de chiffres à valeur significative (histogramme HDR)", + "visTypeTimeseries.percentileHdr.numberOfSignificantValueDigits.hint": "L'histogramme HDR (High Dynamic Range, grande plage dynamique) est une autre implémentation qui peut être utile lors du calcul des rangs centiles pour les mesures de la latence, car elle peut être plus rapide que l'implémentation t-digest, bien qu'elle présente une empreinte mémoire plus élevée. Le paramètre \"Nombre de chiffres à valeur significative\" spécifie le nombre de chiffres significatifs pour la résolution des valeurs de l'histogramme.", + "visTypeTimeseries.percentileRank.aggregationLabel": "Agrégation", + "visTypeTimeseries.percentileRank.fieldLabel": "Champ", + "visTypeTimeseries.percentileRank.values": "Valeurs", + "visTypeTimeseries.positiveOnly.aggregationLabel": "Agrégation", + "visTypeTimeseries.positiveOnly.metricLabel": "Indicateur", + "visTypeTimeseries.positiveRate.aggregationLabel": "Agrégation", + "visTypeTimeseries.positiveRate.helpText": "Cette agrégation ne doit être appliquée qu'à {link} ; il s'agit d'un raccourci pour appliquer Max., Dérivée et Positif uniquement à un champ.", + "visTypeTimeseries.positiveRate.helpTextLink": "nombres augmentant de manière monolithique", + "visTypeTimeseries.positiveRate.unitSelectPlaceholder": "Sélectionner le scaling…", + "visTypeTimeseries.positiveRate.unitsLabel": "Scaling", + "visTypeTimeseries.postiveRate.fieldLabel": "Champ", + "visTypeTimeseries.replaceVars.errors.markdownErrorDescription": "Veuillez vérifier que vous utilisez uniquement Markdown, des variables connues et des expressions Handlebar intégrées.", + "visTypeTimeseries.replaceVars.errors.markdownErrorTitle": "Erreur lors du traitement de votre Markdown", + "visTypeTimeseries.replaceVars.errors.unknownVarDescription": "{badVar} est une variable inconnue.", + "visTypeTimeseries.replaceVars.errors.unknownVarTitle": "Erreur lors du traitement de votre Markdown", + "visTypeTimeseries.searchStrategyUndefinedErrorMessage": "La stratégie de recherche n'était pas définie.", + "visTypeTimeseries.serialDiff.aggregationLabel": "Agrégation", + "visTypeTimeseries.serialDiff.lagLabel": "Décalage", + "visTypeTimeseries.serialDiff.metricLabel": "Indicateur", + "visTypeTimeseries.series.missingAggregationKeyErrorMessage": "La clé des agrégations est manquante dans la réponse. Vérifiez vos autorisations pour cette requête.", + "visTypeTimeseries.series.shouldOneSeriesPerRequestErrorMessage": "Il ne devrait y avoir qu'une seule série par requête.", + "visTypeTimeseries.seriesAgg.aggregationLabel": "Agrégation", + "visTypeTimeseries.seriesAgg.functionLabel": "Fonction", + "visTypeTimeseries.seriesAgg.functionOptions.avgLabel": "Moy.", + "visTypeTimeseries.seriesAgg.functionOptions.countLabel": "Nombre de séries", + "visTypeTimeseries.seriesAgg.functionOptions.cumulativeSumLabel": "Somme cumulée", + "visTypeTimeseries.seriesAgg.functionOptions.maxLabel": "Max.", + "visTypeTimeseries.seriesAgg.functionOptions.minLabel": "Min.", + "visTypeTimeseries.seriesAgg.functionOptions.overallAvgLabel": "Moy. générale", + "visTypeTimeseries.seriesAgg.functionOptions.overallMaxLabel": "Max. général", + "visTypeTimeseries.seriesAgg.functionOptions.overallMinLabel": "Min. général", + "visTypeTimeseries.seriesAgg.functionOptions.overallSumLabel": "Somme générale", + "visTypeTimeseries.seriesAgg.functionOptions.sumLabel": "Somme", + "visTypeTimeseries.seriesAgg.seriesAggIsNotCompatibleLabel": "L'agrégation de séries n'est pas compatible avec la visualisation de tableau.", + "visTypeTimeseries.seriesConfig.filterLabel": "Filtre", + "visTypeTimeseries.seriesConfig.ignoreGlobalFilterDisabledTooltip": "Cette option est désactivée, car les filtres globaux sont ignorés dans les options du panneau.", + "visTypeTimeseries.seriesConfig.ignoreGlobalFilterLabel": "Ignorer le filtre global ?", + "visTypeTimeseries.seriesConfig.missingSeriesComponentDescription": "Composant de série manquant pour le type de panneau : {panelType}", + "visTypeTimeseries.seriesConfig.offsetSeriesTimeLabel": "Décaler l'heure de la série de (1m, 1h, 1w, 1d)", + "visTypeTimeseries.seriesConfig.templateHelpText": "par ex. {templateExample}", + "visTypeTimeseries.seriesConfig.templateLabel": "Modèle", + "visTypeTimeseries.sort.dragToSortAriaLabel": "Faire glisser pour trier", + "visTypeTimeseries.sort.dragToSortTooltip": "Faire glisser pour trier", + "visTypeTimeseries.splits.everything.groupByLabel": "Regrouper par", + "visTypeTimeseries.splits.filter.groupByLabel": "Regrouper par", + "visTypeTimeseries.splits.filter.queryStringLabel": "Chaîne de requête", + "visTypeTimeseries.splits.filterItems.labelAriaLabel": "Étiquette", + "visTypeTimeseries.splits.filterItems.labelPlaceholder": "Étiquette", + "visTypeTimeseries.splits.filters.groupByLabel": "Regrouper par", + "visTypeTimeseries.splits.groupBySelect.modeOptions.everythingLabel": "Tout", + "visTypeTimeseries.splits.groupBySelect.modeOptions.filterLabel": "Filtre", + "visTypeTimeseries.splits.groupBySelect.modeOptions.filtersLabel": "Filtres", + "visTypeTimeseries.splits.groupBySelect.modeOptions.termsLabel": "Termes", + "visTypeTimeseries.splits.terms.byLabel": "Par", + "visTypeTimeseries.splits.terms.defaultCountLabel": "Nombre de docs (par défaut)", + "visTypeTimeseries.splits.terms.directionLabel": "Sens", + "visTypeTimeseries.splits.terms.dirOptions.ascendingLabel": "Croissant", + "visTypeTimeseries.splits.terms.dirOptions.descendingLabel": "Décroissant", + "visTypeTimeseries.splits.terms.excludeLabel": "Exclure", + "visTypeTimeseries.splits.terms.groupByLabel": "Regrouper par", + "visTypeTimeseries.splits.terms.includeLabel": "Inclure", + "visTypeTimeseries.splits.terms.orderByLabel": "Classer par", + "visTypeTimeseries.splits.terms.sizePlaceholder": "Taille", + "visTypeTimeseries.splits.terms.termsLabel": "Termes", + "visTypeTimeseries.splits.terms.topLabel": "Haut", + "visTypeTimeseries.static.aggregationLabel": "Agrégation", + "visTypeTimeseries.static.staticValuesLabel": "Valeur statique", + "visTypeTimeseries.stdAgg.aggregationLabel": "Agrégation", + "visTypeTimeseries.stdAgg.fieldLabel": "Champ", + "visTypeTimeseries.stdDeviation.aggregationLabel": "Agrégation", + "visTypeTimeseries.stdDeviation.fieldLabel": "Champ", + "visTypeTimeseries.stdDeviation.modeLabel": "Mode", + "visTypeTimeseries.stdDeviation.modeOptions.boundsBandLabel": "Bande de limites", + "visTypeTimeseries.stdDeviation.modeOptions.lowerBoundLabel": "Limite inférieure", + "visTypeTimeseries.stdDeviation.modeOptions.rawLabel": "Brut", + "visTypeTimeseries.stdDeviation.modeOptions.upperBoundLabel": "Limite supérieure", + "visTypeTimeseries.stdDeviation.sigmaLabel": "Sigma", + "visTypeTimeseries.stdSibling.aggregationLabel": "Agrégation", + "visTypeTimeseries.stdSibling.metricLabel": "Indicateur", + "visTypeTimeseries.stdSibling.modeLabel": "Mode", + "visTypeTimeseries.stdSibling.modeOptions.boundsBandLabel": "Bande de limites", + "visTypeTimeseries.stdSibling.modeOptions.lowerBoundLabel": "Limite inférieure", + "visTypeTimeseries.stdSibling.modeOptions.rawLabel": "Brut", + "visTypeTimeseries.stdSibling.modeOptions.upperBoundLabel": "Limite supérieure", + "visTypeTimeseries.stdSibling.sigmaLabel": "Sigma", + "visTypeTimeseries.table.addSeriesTooltip": "Ajouter une série", + "visTypeTimeseries.table.aggregateFunctionLabel": "Fonction agrégée", + "visTypeTimeseries.table.avgLabel": "Moy.", + "visTypeTimeseries.table.cloneSeriesTooltip": "Cloner la série", + "visTypeTimeseries.table.colorRulesLabel": "Règles de couleurs", + "visTypeTimeseries.table.columnNotSortableTooltip": "Cette colonne ne peut pas être triée", + "visTypeTimeseries.table.cumulativeSumLabel": "Somme cumulée", + "visTypeTimeseries.table.dataTab.columnLabel": "Étiquette de colonne", + "visTypeTimeseries.table.dataTab.columnsButtonLabel": "Colonnes", + "visTypeTimeseries.table.dataTab.defineFieldDescription": "Pour la visualisation du tableau, vous devez définir un champ sur lequel effectuer le regroupement, en utilisant une agrégation de termes.", + "visTypeTimeseries.table.dataTab.groupByFieldLabel": "Champ Regrouper par", + "visTypeTimeseries.table.dataTab.rowsLabel": "Lignes", + "visTypeTimeseries.table.deleteSeriesTooltip": "Supprimer la série", + "visTypeTimeseries.table.fieldLabel": "Champ", + "visTypeTimeseries.table.filterLabel": "Filtre", + "visTypeTimeseries.table.labelAriaLabel": "Étiquette", + "visTypeTimeseries.table.labelPlaceholder": "Étiquette", + "visTypeTimeseries.table.maxLabel": "Max", + "visTypeTimeseries.table.minLabel": "Min", + "visTypeTimeseries.table.noResultsAvailableWithDescriptionMessage": "Aucun résultat disponible. Vous devez choisir un champ Regrouper par pour cette visualisation.", + "visTypeTimeseries.table.optionsTab.dataLabel": "Données", + "visTypeTimeseries.table.optionsTab.ignoreGlobalFilterLabel": "Ignorer le filtre global ?", + "visTypeTimeseries.table.optionsTab.itemUrlHelpText": "Prend en charge les modèles de moustaches. {key} est défini sur le terme.", + "visTypeTimeseries.table.optionsTab.itemUrlLabel": "URL de l'élément", + "visTypeTimeseries.table.optionsTab.panelFilterLabel": "Filtre de panneau", + "visTypeTimeseries.table.optionsTab.panelOptionsButtonLabel": "Options du panneau", + "visTypeTimeseries.table.overallAvgLabel": "Moy. générale", + "visTypeTimeseries.table.overallMaxLabel": "Max général", + "visTypeTimeseries.table.overallMinLabel": "Min général", + "visTypeTimeseries.table.overallSumLabel": "Somme générale", + "visTypeTimeseries.table.showTrendArrowsLabel": "Afficher les flèches de tendance ?", + "visTypeTimeseries.table.sumLabel": "Somme", + "visTypeTimeseries.table.tab.metricsLabel": "Indicateurs", + "visTypeTimeseries.table.tab.optionsLabel": "Options", + "visTypeTimeseries.table.templateHelpText": "par ex. {templateExample}", + "visTypeTimeseries.table.templateLabel": "Modèle", + "visTypeTimeseries.table.toggleSeriesEditorAriaLabel": "Basculer l'éditeur de séries", + "visTypeTimeseries.timeSeries.addSeriesTooltip": "Ajouter une série", + "visTypeTimeseries.timeseries.annotationsTab.annotationsButtonLabel": "Annotations", + "visTypeTimeseries.timeSeries.axisMaxLabel": "Max. de l'axe", + "visTypeTimeseries.timeSeries.axisMinLabel": "Min. de l'axe", + "visTypeTimeseries.timeSeries.axisPositionLabel": "Position de l'axe", + "visTypeTimeseries.timeSeries.barLabel": "Barre", + "visTypeTimeseries.timeSeries.chartBar.chartTypeLabel": "Type de graphique", + "visTypeTimeseries.timeSeries.chartBar.fillLabel": "Remplissage (0 à 1)", + "visTypeTimeseries.timeSeries.chartBar.lineWidthLabel": "Largeur de la ligne", + "visTypeTimeseries.timeSeries.chartBar.stackedLabel": "Empilé", + "visTypeTimeseries.timeSeries.chartLine.chartTypeLabel": "Type de graphique", + "visTypeTimeseries.timeSeries.chartLine.fillLabel": "Remplissage (0 à 1)", + "visTypeTimeseries.timeSeries.chartLine.lineWidthLabel": "Largeur de la ligne", + "visTypeTimeseries.timeSeries.chartLine.pointSizeLabel": "Taille du point", + "visTypeTimeseries.timeSeries.chartLine.stackedLabel": "Empilé", + "visTypeTimeseries.timeSeries.chartLine.stepsLabel": "Étapes", + "visTypeTimeseries.timeSeries.cloneSeriesTooltip": "Cloner la série", + "visTypeTimeseries.timeseries.dataTab.dataButtonLabel": "Données", + "visTypeTimeseries.timeSeries.deleteSeriesTooltip": "Supprimer la série", + "visTypeTimeseries.timeSeries.gradientLabel": "Gradient", + "visTypeTimeseries.timeSeries.hideInLegendLabel": "Masquer dans la légende", + "visTypeTimeseries.timeSeries.labelPlaceholder": "Étiquette", + "visTypeTimeseries.timeSeries.leftLabel": "Gauche", + "visTypeTimeseries.timeseries.legendPositionOptions.bottomLabel": "Bas", + "visTypeTimeseries.timeseries.legendPositionOptions.leftLabel": "Gauche", + "visTypeTimeseries.timeseries.legendPositionOptions.rightLabel": "Droite", + "visTypeTimeseries.timeSeries.lineLabel": "Ligne", + "visTypeTimeseries.timeSeries.noneLabel": "Aucun", + "visTypeTimeseries.timeSeries.offsetSeriesTimeLabel": "Décaler l'heure de la série de (1m, 1h, 1w, 1d)", + "visTypeTimeseries.timeseries.optionsTab.axisMaxLabel": "Max. de l'axe", + "visTypeTimeseries.timeseries.optionsTab.axisMinLabel": "Min. de l'axe", + "visTypeTimeseries.timeseries.optionsTab.axisPositionLabel": "Position de l'axe", + "visTypeTimeseries.timeseries.optionsTab.axisScaleLabel": "Échelle de l'axe", + "visTypeTimeseries.timeseries.optionsTab.backgroundColorLabel": "Couleur de l'arrière-plan :", + "visTypeTimeseries.timeseries.optionsTab.dataLabel": "Données", + "visTypeTimeseries.timeseries.optionsTab.displayGridLabel": "Afficher la grille", + "visTypeTimeseries.timeseries.optionsTab.ignoreDaylightTimeLabel": "Ignorer l'heure d'été ?", + "visTypeTimeseries.timeseries.optionsTab.ignoreGlobalFilterLabel": "Ignorer le filtre global ?", + "visTypeTimeseries.timeseries.optionsTab.legendPositionLabel": "Position de la légende", + "visTypeTimeseries.timeseries.optionsTab.maxLinesLabel": "Nombre maxi de lignes de légende", + "visTypeTimeseries.timeseries.optionsTab.panelFilterLabel": "Filtre de panneau", + "visTypeTimeseries.timeseries.optionsTab.panelOptionsButtonLabel": "Options du panneau", + "visTypeTimeseries.timeseries.optionsTab.showLegendLabel": "Afficher la légende ?", + "visTypeTimeseries.timeseries.optionsTab.styleLabel": "Style", + "visTypeTimeseries.timeseries.optionsTab.tooltipMode": "Infobulle", + "visTypeTimeseries.timeseries.optionsTab.truncateLegendLabel": "Tronquer la légende ?", + "visTypeTimeseries.timeSeries.percentLabel": "Pour cent", + "visTypeTimeseries.timeseries.positionOptions.leftLabel": "Gauche", + "visTypeTimeseries.timeseries.positionOptions.rightLabel": "Droite", + "visTypeTimeseries.timeSeries.rainbowLabel": "Arc-en-ciel", + "visTypeTimeseries.timeSeries.rightLabel": "Droite", + "visTypeTimeseries.timeseries.scaleOptions.logLabel": "Logarithmique", + "visTypeTimeseries.timeseries.scaleOptions.normalLabel": "Normal", + "visTypeTimeseries.timeSeries.separateAxisLabel": "Axe séparé ?", + "visTypeTimeseries.timeSeries.splitColorThemeLabel": "Thème de couleurs de division", + "visTypeTimeseries.timeSeries.stackedLabel": "Empilé", + "visTypeTimeseries.timeSeries.stackedWithinSeriesLabel": "Empilé dans la série", + "visTypeTimeseries.timeSeries.tab.metricsLabel": "Indicateurs", + "visTypeTimeseries.timeSeries.tab.optionsLabel": "Options", + "visTypeTimeseries.timeSeries.templateHelpText": "par ex. {templateExample}", + "visTypeTimeseries.timeSeries.templateLabel": "Modèle", + "visTypeTimeseries.timeSeries.toggleSeriesEditorAriaLabel": "Basculer l'éditeur de séries", + "visTypeTimeseries.timeseries.tooltipOptions.showAll": "Afficher toutes les valeurs", + "visTypeTimeseries.timeseries.tooltipOptions.showFocused": "Afficher les valeurs ciblées", + "visTypeTimeseries.topHit.aggregateWith.selectPlaceholder": "Sélectionner…", + "visTypeTimeseries.topHit.aggregateWithLabel": "Agréger avec", + "visTypeTimeseries.topHit.aggregationLabel": "Agrégation", + "visTypeTimeseries.topHit.aggWithOptions.averageLabel": "Moy.", + "visTypeTimeseries.topHit.aggWithOptions.concatenate": "Concaténer", + "visTypeTimeseries.topHit.aggWithOptions.maxLabel": "Max", + "visTypeTimeseries.topHit.aggWithOptions.minLabel": "Min", + "visTypeTimeseries.topHit.aggWithOptions.sumLabel": "Somme", + "visTypeTimeseries.topHit.fieldLabel": "Champ", + "visTypeTimeseries.topHit.order.selectPlaceholder": "Sélectionner…", + "visTypeTimeseries.topHit.orderByLabel": "Classer par", + "visTypeTimeseries.topHit.orderLabel": "Ordre", + "visTypeTimeseries.topHit.orderOptions.ascLabel": "Croiss.", + "visTypeTimeseries.topHit.orderOptions.descLabel": "Décroiss.", + "visTypeTimeseries.topHit.sizeLabel": "Taille", + "visTypeTimeseries.topN.addSeriesTooltip": "Ajouter une série", + "visTypeTimeseries.topN.cloneSeriesTooltip": "Cloner la série", + "visTypeTimeseries.topN.dataTab.dataButtonLabel": "Données", + "visTypeTimeseries.topN.deleteSeriesTooltip": "Supprimer la série", + "visTypeTimeseries.topN.labelPlaceholder": "Étiquette", + "visTypeTimeseries.topN.optionsTab.backgroundColorLabel": "Couleur de l'arrière-plan :", + "visTypeTimeseries.topN.optionsTab.colorRulesLabel": "Règles de couleurs", + "visTypeTimeseries.topN.optionsTab.dataLabel": "Données", + "visTypeTimeseries.topN.optionsTab.ignoreGlobalFilterLabel": "Ignorer le filtre global ?", + "visTypeTimeseries.topN.optionsTab.itemUrlDescription": "Prend en charge les modèles de moustaches. {key} est défini sur le terme.", + "visTypeTimeseries.topN.optionsTab.itemUrlLabel": "URL de l'élément", + "visTypeTimeseries.topN.optionsTab.panelFilterLabel": "Filtre de panneau", + "visTypeTimeseries.topN.optionsTab.panelOptionsButtonLabel": "Options du panneau", + "visTypeTimeseries.topN.optionsTab.styleLabel": "Style", + "visTypeTimeseries.topN.tab.metricsLabel": "Indicateurs", + "visTypeTimeseries.topN.tab.optionsLabel": "Options", + "visTypeTimeseries.topN.toggleSeriesEditorAriaLabel": "Basculer l'éditeur de séries", + "visTypeTimeseries.units.auto": "auto", + "visTypeTimeseries.units.perDay": "par jour", + "visTypeTimeseries.units.perHour": "par heure", + "visTypeTimeseries.units.perMillisecond": "par milliseconde", + "visTypeTimeseries.units.perMinute": "par minute", + "visTypeTimeseries.units.perSecond": "par seconde", + "visTypeTimeseries.unsupportedSplit.splitIsUnsupportedDescription": "Diviser par {modelType} n'est pas pris en charge.", + "visTypeTimeseries.vars.variableNameAriaLabel": "Nom de la variable", + "visTypeTimeseries.vars.variableNamePlaceholder": "Nom de la variable", + "visTypeTimeseries.visEditorVisualization.applyChangesLabel": "Appliquer les modifications", + "visTypeTimeseries.visEditorVisualization.autoApplyLabel": "Appliquer automatiquement", + "visTypeTimeseries.visEditorVisualization.changesHaveNotBeenAppliedMessage": "Les modifications apportées à cette visualisation n'ont pas été appliquées.", + "visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage": "Les dernières modifications ont été appliquées.", + "visTypeTimeseries.visEditorVisualization.changesWillBeAutomaticallyAppliedMessage": "Les modifications seront appliquées automatiquement.", + "visTypeTimeseries.visPicker.gaugeLabel": "Jauge", + "visTypeTimeseries.visPicker.metricLabel": "Indicateur", + "visTypeTimeseries.visPicker.tableLabel": "Tableau", + "visTypeTimeseries.visPicker.timeSeriesLabel": "Séries temporelles", + "visTypeTimeseries.visPicker.topNLabel": "N premiers", + "visTypeTimeseries.yesButtonLabel": "Oui", + "visTypeVega.editor.formatError": "Erreur lors du formatage des spécifications", + "visTypeVega.editor.reformatAsHJSONButtonLabel": "Reformater en HJSON", + "visTypeVega.editor.reformatAsJSONButtonLabel": "Reformater en JSON, supprimer les commentaires", + "visTypeVega.editor.vegaDocumentationLinkText": "Documentation Vega", + "visTypeVega.editor.vegaEditorOptionsButtonAriaLabel": "Options de l'éditeur Vega", + "visTypeVega.editor.vegaHelpButtonAriaLabel": "Aide Vega", + "visTypeVega.editor.vegaHelpLinkText": "Aide Kibana Vega", + "visTypeVega.editor.vegaLiteDocumentationLinkText": "Documentation Vega-Lite", + "visTypeVega.emsFileParser.emsFileNameDoesNotExistErrorMessage": "{emsfile} {emsfileName} n'existe pas", + "visTypeVega.emsFileParser.missingNameOfFileErrorMessage": "{dataUrlParam} avec {dataUrlParamValue} requiert le paramètre {nameParam} (nom du fichier)", + "visTypeVega.esQueryParser.autointervalValueTypeErrorMessage": "{autointerval} doit être {trueValue} ou un nombre", + "visTypeVega.esQueryParser.dataUrlMustNotHaveLegacyAndBodyQueryValuesAtTheSameTimeErrorMessage": "{dataUrlParam} ne doit pas avoir de {legacyContext} existant et de valeurs {bodyQueryConfigName} en même temps", + "visTypeVega.esQueryParser.dataUrlMustNotHaveLegacyContextTogetherWithContextOrTimefieldErrorMessage": "{dataUrlParam} ne doit pas avoir de {legacyContext} avec {context} ou {timefield}", + "visTypeVega.esQueryParser.legacyContextCanBeTrueErrorMessage": "{legacyContext} existant peut être {trueValue} (ignore le sélecteur de plage temporelle), ou il peut s'agir du nom du champ temporel, par ex. {timestampParam}", + "visTypeVega.esQueryParser.legacyUrlShouldChangeToWarningMessage": "{urlParam} existant : {legacyUrl} doit être modifié en {result}", + "visTypeVega.esQueryParser.shiftMustValueTypeErrorMessage": "{shiftParam} doit être une valeur numérique", + "visTypeVega.esQueryParser.timefilterValueErrorMessage": "La propriété {timefilter} doit être définie sur {trueValue}, {minValue} ou {maxValue}", + "visTypeVega.esQueryParser.unknownUnitValueErrorMessage": "Valeur {unitParamName} inconnue. Doit être l'une des valeurs suivantes : [{unitParamValues}]", + "visTypeVega.esQueryParser.unnamedRequest": "Requête sans nom #{index}", + "visTypeVega.esQueryParser.urlBodyValueTypeErrorMessage": "{configName} doit être un objet", + "visTypeVega.esQueryParser.urlContextAndUrlTimefieldMustNotBeUsedErrorMessage": "{urlContext} et {timefield} ne doivent pas être utilisés lorsque {queryParam} est défini", + "visTypeVega.function.help": "Visualisation Vega", + "visTypeVega.inspector.dataSetsLabel": "Ensembles de données", + "visTypeVega.inspector.dataViewer.dataSetAriaLabel": "Ensemble de données", + "visTypeVega.inspector.dataViewer.gridAriaLabel": "Grille de données {name}", + "visTypeVega.inspector.signalValuesLabel": "Valeurs de signal", + "visTypeVega.inspector.signalViewer.gridAriaLabel": "Grille de données des valeurs de signal", + "visTypeVega.inspector.specLabel": "Spéc.", + "visTypeVega.inspector.specViewer.copyToClipboardLabel": "Copier dans le presse-papiers", + "visTypeVega.inspector.vegaAdapter.signal": "Signal", + "visTypeVega.inspector.vegaAdapter.value": "Valeur", + "visTypeVega.inspector.vegaDebugLabel": "Débogage Vega", + "visTypeVega.mapView.experimentalMapLayerInfo": "Les calques de cartes sont expérimentaux et ne sont pas soumis aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale. Pour apporter des commentaires, veuillez créer une entrée dans {githubLink}.", + "visTypeVega.mapView.mapStyleNotFoundWarningMessage": "{mapStyleParam} est introuvable", + "visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage": "{minZoomPropertyName} et {maxZoomPropertyName} ont été permutés", + "visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage": "Réinitialisation de {name} sur {max}", + "visTypeVega.mapView.resettingPropertyToMinValueWarningMessage": "Réinitialisation de {name} sur {min}", + "visTypeVega.type.vegaDescription": "Utilisez Vega pour créer de nouveaux types de visualisations.", + "visTypeVega.type.vegaNote": "Requiert une connaissance de la syntaxe Vega.", + "visTypeVega.type.vegaTitleInWizard": "Visualisation personnalisée", + "visTypeVega.urlParser.dataUrlRequiresUrlParameterInFormErrorMessage": "{dataUrlParam} requiert un paramètre {urlParam} sous la forme \"{formLink}\"", + "visTypeVega.urlParser.urlShouldHaveQuerySubObjectWarningMessage": "L'utilisation d'un {urlObject} requiert un sous-objet {subObjectName}", + "visTypeVega.vegaParser.autoSizeDoesNotAllowFalse": "{autoSizeParam} est activé ; il peut uniquement être désactivé en définissant {autoSizeParam} sur {noneParam}", + "visTypeVega.vegaParser.baseView.externalUrlsAreNotEnabledErrorMessage": "Les URL externes ne sont pas activées. Ajouter {enableExternalUrls} à {kibanaConfigFileName}", + "visTypeVega.vegaParser.baseView.functionIsNotDefinedForGraphErrorMessage": "{funcName} n'est pas défini pour ce graphe", + "visTypeVega.vegaParser.baseView.indexNotFoundErrorMessage": "Impossible de trouver l'index {index}", + "visTypeVega.vegaParser.baseView.timeValuesTypeErrorMessage": "Erreur lors de la définition du filtre de temps : les deux valeurs temporelles doivent être des dates relatives ou absolues. {start}, {end}", + "visTypeVega.vegaParser.baseView.unableToFindDefaultIndexErrorMessage": "Impossible de trouver l'index par défaut", + "visTypeVega.vegaParser.centerOnMarkConfigValueTypeErrorMessage": "Les valeurs attendues pour {configName} sont {trueValue}, {falseValue} ou un nombre", + "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.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", + "visTypeVega.vegaParser.notSupportedUrlTypeErrorMessage": "{urlObject} n'est pas pris en charge", + "visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage": "Les spécifications d'entrée utilisent {schemaLibrary} {schemaVersion}, mais la version actuelle de {schemaLibrary} est {libraryVersion}.", + "visTypeVega.vegaParser.paddingConfigValueTypeErrorMessage": "La valeur attendue pour {configName} est un nombre", + "visTypeVega.vegaParser.someKibanaConfigurationIsNoValidWarningMessage": "{configName} n'est pas valide", + "visTypeVega.vegaParser.someKibanaParamValueTypeWarningMessage": "{configName} doit être une valeur booléenne", + "visTypeVega.vegaParser.textTruncateConfigValueTypeErrorMessage": "La valeur attendue pour {configName} est une valeur booléenne", + "visTypeVega.vegaParser.unexpectedValueForPositionConfigurationErrorMessage": "Valeur inattendue pour la configuration {configurationName}", + "visTypeVega.vegaParser.unrecognizedControlsLocationValueErrorMessage": "Valeur {controlsLocationParam} non reconnue. Valeur attendue parmi [{locToDirMap}]", + "visTypeVega.vegaParser.unrecognizedDirValueErrorMessage": "Valeur {dirParam} non reconnue. Valeur attendue parmi [{expectedValues}]", + "visTypeVega.vegaParser.VLCompilerShouldHaveGeneratedSingleProtectionObjectErrorMessage": "Erreur interne : le compilateur Vega-Lite aurait dû générer un objet de projection unique", + "visTypeVega.vegaParser.widthAndHeightParamsAreIgnored": "Les paramètres {widthParam} et {heightParam} sont ignorés, car {autoSizeParam} est activé. Pour le désactiver, définissez {autoSizeParam} sur {noneParam}", + "visTypeVega.vegaParser.widthAndHeightParamsAreRequired": "Aucun rendu n'est généré lorsque {autoSizeParam} est défini sur {noneParam} quand les spécifications {vegaLiteParam} à facette ou répétées sont utilisées. Pour y remédier, retirez {autoSizeParam} ou utilisez {vegaParam}.", + "visTypeVega.visualization.renderErrorTitle": "Erreur Vega", + "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "Impossible de générer un rendu sans données", + "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "Nombre maximal de groupes pouvant être renvoyés par une source de données unique. Un nombre plus élevé pourra impacter négativement les performances de rendu du navigateur", + "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "Nombre maximal de groupes pour la carte thermique", + "visTypeVislib.aggResponse.allDocsTitle": "Tous les docs", + "visTypeVislib.controls.gaugeOptions.alignmentLabel": "Alignement", + "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "Étendre automatiquement la plage", + "visTypeVislib.controls.gaugeOptions.extendRangeTooltip": "Étend la plage jusqu'à la valeur maximale de vos données.", + "visTypeVislib.controls.gaugeOptions.gaugeTypeLabel": "Type de jauge", + "visTypeVislib.controls.gaugeOptions.labelsTitle": "Étiquettes", + "visTypeVislib.controls.gaugeOptions.rangesTitle": "Plages", + "visTypeVislib.controls.gaugeOptions.showLabelsLabel": "Afficher les étiquettes", + "visTypeVislib.controls.gaugeOptions.showLegendLabel": "Afficher la légende", + "visTypeVislib.controls.gaugeOptions.showOutline": "Afficher le contour", + "visTypeVislib.controls.gaugeOptions.showScaleLabel": "Afficher l'échelle", + "visTypeVislib.controls.gaugeOptions.styleTitle": "Style", + "visTypeVislib.controls.gaugeOptions.subTextLabel": "Sous-étiquette", + "visTypeVislib.functions.pie.help": "Visualisation camembert", + "visTypeVislib.functions.vislib.help": "Visualisation Vislib", + "visTypeVislib.gauge.alignmentAutomaticTitle": "Automatique", + "visTypeVislib.gauge.alignmentHorizontalTitle": "Horizontal", + "visTypeVislib.gauge.alignmentVerticalTitle": "Vertical", + "visTypeVislib.gauge.gaugeDescription": "Affichez le statut d'un indicateur.", + "visTypeVislib.gauge.gaugeTitle": "Jauge", + "visTypeVislib.gauge.gaugeTypes.arcText": "Arc", + "visTypeVislib.gauge.gaugeTypes.circleText": "Cercle", + "visTypeVislib.gauge.groupTitle": "Diviser le groupe", + "visTypeVislib.gauge.metricTitle": "Indicateur", + "visTypeVislib.goal.goalDescription": "Suivez la progression d'un indicateur vers un objectif.", + "visTypeVislib.goal.goalTitle": "Objectif", + "visTypeVislib.goal.groupTitle": "Diviser le groupe", + "visTypeVislib.goal.metricTitle": "Indicateur", + "visTypeVislib.vislib.errors.noResultsFoundTitle": "Aucun résultat trouvé", + "visTypeVislib.vislib.heatmap.maxBucketsText": "Trop de séries sont définies ({nr}). La valeur de configuration maximale est {max}.", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "Filtrer pour la valeur {legendDataLabel}", + "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "Filtrer la valeur {legendDataLabel}", + "visTypeVislib.vislib.legend.loadingLabel": "chargement…", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "Basculer la légende", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "Basculer la légende", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}, options de basculement", + "visTypeVislib.vislib.tooltip.fieldLabel": "champ", + "visTypeVislib.vislib.tooltip.valueLabel": "valeur", + "visTypeXy.aggResponse.allDocsTitle": "Tous les docs", + "visTypeXy.area.areaDescription": "Mettez en avant les données entre un axe et une ligne.", + "visTypeXy.area.areaTitle": "Aire", + "visTypeXy.area.groupTitle": "Diviser la série", + "visTypeXy.area.metricsTitle": "Axe Y", + "visTypeXy.area.radiusTitle": "Taille du point", + "visTypeXy.area.segmentTitle": "Axe X", + "visTypeXy.area.splitTitle": "Diviser le graphique", + "visTypeXy.area.tabs.metricsAxesTitle": "Indicateurs et axes", + "visTypeXy.area.tabs.panelSettingsTitle": "Paramètres du panneau", + "visTypeXy.axisModes.normalText": "Normal", + "visTypeXy.axisModes.percentageText": "Pourcentage", + "visTypeXy.axisModes.silhouetteText": "Silhouette", + "visTypeXy.axisModes.wiggleText": "Ondulé", + "visTypeXy.categoryAxis.rotate.angledText": "En angle", + "visTypeXy.categoryAxis.rotate.horizontalText": "Horizontal", + "visTypeXy.categoryAxis.rotate.verticalText": "Vertical", + "visTypeXy.chartModes.normalText": "Normal", + "visTypeXy.chartModes.stackedText": "Empilé", + "visTypeXy.chartTypes.areaText": "Aire", + "visTypeXy.chartTypes.barText": "Barre", + "visTypeXy.chartTypes.lineText": "Ligne", + "visTypeXy.controls.pointSeries.categoryAxis.alignLabel": "Aligner", + "visTypeXy.controls.pointSeries.categoryAxis.filterLabelsLabel": "Étiquettes de filtre", + "visTypeXy.controls.pointSeries.categoryAxis.labelsTitle": "Étiquettes", + "visTypeXy.controls.pointSeries.categoryAxis.positionLabel": "Position", + "visTypeXy.controls.pointSeries.categoryAxis.showLabel": "Afficher les lignes et étiquettes de l'axe", + "visTypeXy.controls.pointSeries.categoryAxis.showLabelsLabel": "Afficher les étiquettes", + "visTypeXy.controls.pointSeries.categoryAxis.xAxisTitle": "Axe X", + "visTypeXy.controls.pointSeries.gridAxis.dontShowLabel": "Ne pas afficher", + "visTypeXy.controls.pointSeries.gridAxis.gridText": "Grille", + "visTypeXy.controls.pointSeries.gridAxis.xAxisLinesLabel": "Afficher les lignes de l'axe X", + "visTypeXy.controls.pointSeries.gridAxis.yAxisLinesLabel": "Lignes de l'axe Y", + "visTypeXy.controls.pointSeries.series.chartTypeLabel": "Type de graphique", + "visTypeXy.controls.pointSeries.series.circlesRadius": "Taille des points", + "visTypeXy.controls.pointSeries.series.lineModeLabel": "Mode ligne", + "visTypeXy.controls.pointSeries.series.lineWidthLabel": "Largeur de la ligne", + "visTypeXy.controls.pointSeries.series.metricsTitle": "Indicateurs", + "visTypeXy.controls.pointSeries.series.modeLabel": "Mode", + "visTypeXy.controls.pointSeries.series.newAxisLabel": "Nouvel axe…", + "visTypeXy.controls.pointSeries.series.showDotsLabel": "Afficher les points", + "visTypeXy.controls.pointSeries.series.showLineLabel": "Afficher la ligne", + "visTypeXy.controls.pointSeries.series.valueAxisLabel": "Axe des valeurs", + "visTypeXy.controls.pointSeries.seriesAccordionAriaLabel": "Basculer les options {agg}", + "visTypeXy.controls.pointSeries.valueAxes.addButtonTooltip": "Ajouter l'axe Y", + "visTypeXy.controls.pointSeries.valueAxes.customExtentsLabel": "Extensions personnalisées", + "visTypeXy.controls.pointSeries.valueAxes.maxLabel": "Max", + "visTypeXy.controls.pointSeries.valueAxes.minErrorMessage": "Min doit être inférieur à Max.", + "visTypeXy.controls.pointSeries.valueAxes.minLabel": "Min", + "visTypeXy.controls.pointSeries.valueAxes.minNeededScaleText": "Min doit être supérieur à 0 lorsqu'une échelle logarithmique est sélectionnée.", + "visTypeXy.controls.pointSeries.valueAxes.modeLabel": "Mode", + "visTypeXy.controls.pointSeries.valueAxes.positionLabel": "Position", + "visTypeXy.controls.pointSeries.valueAxes.removeButtonTooltip": "Retirer l'axe Y", + "visTypeXy.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "Marge des limites", + "visTypeXy.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "La marge des limites doit être supérieure ou égale à 0.", + "visTypeXy.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "Scaler sur les limites de données", + "visTypeXy.controls.pointSeries.valueAxes.scaleTypeLabel": "Type d'échelle", + "visTypeXy.controls.pointSeries.valueAxes.setAxisExtentsLabel": "Définir la portée de l'axe", + "visTypeXy.controls.pointSeries.valueAxes.showLabel": "Afficher les lignes et étiquettes de l'axe", + "visTypeXy.controls.pointSeries.valueAxes.titleLabel": "Titre", + "visTypeXy.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "Basculer la portée personnalisée", + "visTypeXy.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "Basculer les options {axisName}", + "visTypeXy.controls.pointSeries.valueAxes.yAxisTitle": "Axes Y", + "visTypeXy.controls.truncateLabel": "Tronquer", + "visTypeXy.editors.elasticChartsOptions.detailedTooltip.label": "Afficher l'infobulle détaillée", + "visTypeXy.editors.elasticChartsOptions.detailedTooltip.tooltip": "Active l'ancienne infobulle détaillée pour l'affichage d'une valeur unique. Lorsque cette option est désactivée, une nouvelle infobulle résumée affichera plusieurs valeurs.", + "visTypeXy.editors.elasticChartsOptions.fillOpacity": "Opacité de remplissage", + "visTypeXy.editors.elasticChartsOptions.missingValuesLabel": "Remplir les valeurs manquantes", + "visTypeXy.editors.pointSeries.currentTimeMarkerLabel": "Repère de temps actuel", + "visTypeXy.editors.pointSeries.orderBucketsBySumLabel": "Classer les groupes par somme", + "visTypeXy.editors.pointSeries.settingsTitle": "Paramètres", + "visTypeXy.editors.pointSeries.showLabels": "Afficher les valeurs sur le graphique", + "visTypeXy.editors.pointSeries.thresholdLine.colorLabel": "Couleur de la ligne", + "visTypeXy.editors.pointSeries.thresholdLine.showLabel": "Afficher la ligne de seuil", + "visTypeXy.editors.pointSeries.thresholdLine.styleLabel": "Style de la ligne", + "visTypeXy.editors.pointSeries.thresholdLine.valueLabel": "Valeur seuil", + "visTypeXy.editors.pointSeries.thresholdLine.widthLabel": "Largeur de la ligne", + "visTypeXy.editors.pointSeries.thresholdLineSettingsTitle": "Ligne de seuil", + "visTypeXy.fittingFunctionsTitle.carry": "Dernière (remplit les blancs avec la dernière valeur)", + "visTypeXy.fittingFunctionsTitle.linear": "Linéaire (remplit les blancs avec une ligne)", + "visTypeXy.fittingFunctionsTitle.lookahead": "Suivante (remplit les blancs avec la valeur suivante)", + "visTypeXy.fittingFunctionsTitle.none": "Masquer (ne remplit pas les blancs)", + "visTypeXy.fittingFunctionsTitle.zero": "Zéro (remplit les blancs avec des zéros)", + "visTypeXy.function.adimension.bucket": "Groupe", + "visTypeXy.function.adimension.dotSize": "Taille du point", + "visTypeXy.function.args.addLegend.help": "Afficher la légende du graphique", + "visTypeXy.function.args.addTimeMarker.help": "Afficher le repère de temps", + "visTypeXy.function.args.addTooltip.help": "Afficher l'infobulle au survol", + "visTypeXy.function.args.args.chartType.help": "Type de graphique. Peut être linéaire, en aires ou histogramme", + "visTypeXy.function.args.categoryAxes.help": "Configuration de l'axe de catégorie", + "visTypeXy.function.args.detailedTooltip.help": "Afficher l'infobulle détaillée", + "visTypeXy.function.args.fillOpacity.help": "Définit l'opacité du remplissage du graphique en aires", + "visTypeXy.function.args.fittingFunction.help": "Nom de la fonction d'adaptation", + "visTypeXy.function.args.gridCategoryLines.help": "Afficher les lignes de catégories de la grille dans le graphique", + "visTypeXy.function.args.gridValueAxis.help": "Nom de l'axe des valeurs pour lequel la grille est affichée", + "visTypeXy.function.args.isVislibVis.help": "Indicateur des anciennes visualisations vislib. Utilisé pour la rétro-compatibilité, notamment pour les couleurs", + "visTypeXy.function.args.labels.help": "Configuration des étiquettes du graphique", + "visTypeXy.function.args.legendPosition.help": "Positionner la légende en haut, en bas, à gauche ou à droite du graphique", + "visTypeXy.function.args.orderBucketsBySum.help": "Classer les groupes par somme", + "visTypeXy.function.args.palette.help": "Définit le nom de la palette du graphique", + "visTypeXy.function.args.radiusRatio.help": "Rapport de taille des points", + "visTypeXy.function.args.seriesDimension.help": "Configuration de la dimension de la série", + "visTypeXy.function.args.seriesParams.help": "Configuration des paramètres de la série", + "visTypeXy.function.args.splitColumnDimension.help": "Configuration de la dimension Diviser par colonne", + "visTypeXy.function.args.splitRowDimension.help": "Configuration de la dimension Diviser par ligne", + "visTypeXy.function.args.thresholdLine.help": "Configuration de la ligne de seuil", + "visTypeXy.function.args.times.help": "Configuration du repère de temps", + "visTypeXy.function.args.valueAxes.help": "Configuration de l'axe des valeurs", + "visTypeXy.function.args.widthDimension.help": "Configuration de la dimension en largeur", + "visTypeXy.function.args.xDimension.help": "Configuration de la dimension de l'axe X", + "visTypeXy.function.args.yDimension.help": "Configuration de la dimension de l'axe Y", + "visTypeXy.function.args.zDimension.help": "Configuration de la dimension de l'axe Z", + "visTypeXy.function.categoryAxis.help": "Génère l'objet axe de catégorie", + "visTypeXy.function.categoryAxis.id.help": "ID de l'axe de catégorie", + "visTypeXy.function.categoryAxis.labels.help": "Configuration de l'étiquette de l'axe", + "visTypeXy.function.categoryAxis.position.help": "Position de l'axe de catégorie", + "visTypeXy.function.categoryAxis.scale.help": "Configuration de l'échelle", + "visTypeXy.function.categoryAxis.show.help": "Afficher l'axe de catégorie", + "visTypeXy.function.categoryAxis.title.help": "Titre de l'axe de catégorie", + "visTypeXy.function.categoryAxis.type.help": "Type de l'axe de catégorie. Peut être une catégorie ou une valeur", + "visTypeXy.function.dimension.metric": "Indicateur", + "visTypeXy.function.dimension.splitcolumn": "Division de colonne", + "visTypeXy.function.dimension.splitrow": "Division de ligne", + "visTypeXy.function.label.color.help": "Couleur de l'étiquette", + "visTypeXy.function.label.filter.help": "Masque les étiquettes qui se chevauchent et les éléments en double sur l'axe", + "visTypeXy.function.label.help": "Génère l'objet étiquette", + "visTypeXy.function.label.overwriteColor.help": "Écraser la couleur", + "visTypeXy.function.label.rotate.help": "Faire pivoter l'angle", + "visTypeXy.function.label.show.help": "Afficher l'étiquette", + "visTypeXy.function.label.truncate.help": "Nombre de symboles avant troncature", + "visTypeXy.function.scale.boundsMargin.help": "Marge des limites", + "visTypeXy.function.scale.defaultYExtents.help": "Indicateur qui permet de scaler sur les limites de données", + "visTypeXy.function.scale.help": "Génère l'objet échelle", + "visTypeXy.function.scale.max.help": "Valeur max", + "visTypeXy.function.scale.min.help": "Valeur min", + "visTypeXy.function.scale.mode.help": "Mode échelle. Peut être normal, pourcentage, ondulé ou silhouette", + "visTypeXy.function.scale.setYExtents.help": "Indicateur qui permet de définir votre propre portée", + "visTypeXy.function.scale.type.help": "Type d'échelle. Peut être linéaire, logarithmique ou racine carrée", + "visTypeXy.function.seriesParam.circlesRadius.help": "Définit la taille des cercles (rayon)", + "visTypeXy.function.seriesParam.drawLinesBetweenPoints.help": "Trace des lignes entre des points", + "visTypeXy.function.seriesparam.help": "Génère un objet paramètres de la série", + "visTypeXy.function.seriesParam.id.help": "ID des paramètres de la série", + "visTypeXy.function.seriesParam.interpolate.help": "Mode d'interpolation. Peut être linéaire, cardinal ou palier suivant", + "visTypeXy.function.seriesParam.label.help": "Nom des paramètres de la série", + "visTypeXy.function.seriesParam.lineWidth.help": "Largeur de ligne", + "visTypeXy.function.seriesParam.mode.help": "Mode graphique. Peut être empilé ou pourcentage", + "visTypeXy.function.seriesParam.show.help": "Afficher les paramètres", + "visTypeXy.function.seriesParam.showCircles.help": "Afficher les cercles", + "visTypeXy.function.seriesParam.type.help": "Type de graphique. Peut être linéaire, en aires ou histogramme", + "visTypeXy.function.seriesParam.valueAxis.help": "Nom de l'axe des valeurs", + "visTypeXy.function.thresholdLine.color.help": "Couleur de la ligne de seuil", + "visTypeXy.function.thresholdLine.help": "Génère un objet ligne de seuil", + "visTypeXy.function.thresholdLine.show.help": "Afficher la ligne de seuil", + "visTypeXy.function.thresholdLine.style.help": "Style de la ligne de seuil. Peut être pleine, en tirets ou en point-tiret", + "visTypeXy.function.thresholdLine.value.help": "Valeur seuil", + "visTypeXy.function.thresholdLine.width.help": "Largeur de la ligne de seuil", + "visTypeXy.function.timeMarker.class.help": "Nom de classe Css", + "visTypeXy.function.timeMarker.color.help": "Couleur du repère de temps", + "visTypeXy.function.timemarker.help": "Génère un objet repère de temps", + "visTypeXy.function.timeMarker.opacity.help": "Opacité du repère de temps", + "visTypeXy.function.timeMarker.time.help": "Heure exacte", + "visTypeXy.function.timeMarker.width.help": "Largeur du repère de temps", + "visTypeXy.function.valueAxis.axisParams.help": "Paramètres de l'axe des valeurs", + "visTypeXy.function.valueaxis.help": "Génère l'objet axe des valeurs", + "visTypeXy.function.valueAxis.name.help": "Nom de l'axe des valeurs", + "visTypeXy.functions.help": "Visualisation XY", + "visTypeXy.histogram.groupTitle": "Diviser la série", + "visTypeXy.histogram.histogramDescription": "Présente les données en barres verticales sur un axe.", + "visTypeXy.histogram.histogramTitle": "Barre verticale", + "visTypeXy.histogram.metricTitle": "Axe Y", + "visTypeXy.histogram.radiusTitle": "Taille du point", + "visTypeXy.histogram.segmentTitle": "Axe X", + "visTypeXy.histogram.splitTitle": "Diviser le graphique", + "visTypeXy.horizontalBar.groupTitle": "Diviser la série", + "visTypeXy.horizontalBar.horizontalBarDescription": "Présente les données en barres horizontales sur un axe.", + "visTypeXy.horizontalBar.horizontalBarTitle": "Barre horizontale", + "visTypeXy.horizontalBar.metricTitle": "Axe Y", + "visTypeXy.horizontalBar.radiusTitle": "Taille du point", + "visTypeXy.horizontalBar.segmentTitle": "Axe X", + "visTypeXy.horizontalBar.splitTitle": "Diviser le graphique", + "visTypeXy.interpolationModes.smoothedText": "Lissé", + "visTypeXy.interpolationModes.steppedText": "Par paliers", + "visTypeXy.interpolationModes.straightText": "Droit", + "visTypeXy.legend.filterForValueButtonAriaLabel": "Filtre pour la valeur", + "visTypeXy.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", + "visTypeXy.legend.filterOutValueButtonAriaLabel": "Filtrer la valeur", + "visTypeXy.legendPositions.bottomText": "Bas", + "visTypeXy.legendPositions.leftText": "Gauche", + "visTypeXy.legendPositions.rightText": "Droite", + "visTypeXy.legendPositions.topText": "Haut", + "visTypeXy.line.groupTitle": "Diviser la série", + "visTypeXy.line.lineDescription": "Affiche les données sous forme d'une série de points.", + "visTypeXy.line.lineTitle": "Ligne", + "visTypeXy.line.metricTitle": "Axe Y", + "visTypeXy.line.radiusTitle": "Taille du point", + "visTypeXy.line.segmentTitle": "Axe X", + "visTypeXy.line.splitTitle": "Diviser le graphique", + "visTypeXy.scaleTypes.linearText": "Linéaire", + "visTypeXy.scaleTypes.logText": "Logarithmique", + "visTypeXy.scaleTypes.squareRootText": "Racine carrée", + "visTypeXy.thresholdLine.style.dashedText": "Tirets", + "visTypeXy.thresholdLine.style.dotdashedText": "Point-tiret", + "visTypeXy.thresholdLine.style.fullText": "Pleine", + "visualizations.advancedSettings.visualizeEnableLabsText": "Permet aux utilisateurs de créer, d’afficher et de modifier des visualisations expérimentales. Si la fonctionnalité est désactivée,\n seules les visualisations considérées prêtes pour la production sont disponibles pour l'utilisateur.", + "visualizations.advancedSettings.visualizeEnableLabsTitle": "Activer les visualisations expérimentales", + "visualizations.disabledLabVisualizationLink": "Lire la documentation", + "visualizations.disabledLabVisualizationMessage": "Veuillez activer le mode lab dans les paramètres avancés pour consulter les visualisations lab.", + "visualizations.disabledLabVisualizationTitle": "{title} est une visualisation lab.", + "visualizations.displayName": "Visualisation", + "visualizations.embeddable.placeholderTitle": "Titre de l'espace réservé", + "visualizations.function.range.from.help": "Début de la plage", + "visualizations.function.range.help": "Génère un objet plage", + "visualizations.function.range.to.help": "Fin de la plage", + "visualizations.function.visDimension.accessor.help": "Colonne de votre ensemble de données à utiliser (index de colonne ou nom de colonne)", + "visualizations.function.visDimension.format.help": "Format", + "visualizations.function.visDimension.formatParams.help": "Paramètres de format", + "visualizations.function.visDimension.help": "Génère un objet dimension visConfig", + "visualizations.function.xyDimension.aggType.help": "Type d'agrégation", + "visualizations.function.xydimension.help": "Génère un objet dimension xy", + "visualizations.function.xyDimension.label.help": "Étiquette", + "visualizations.function.xyDimension.params.help": "Paramètres", + "visualizations.function.xyDimension.visDimension.help": "Configuration de l'objet dimension", + "visualizations.initializeWithoutIndexPatternErrorMessage": "Tentative d'initialisation des agrégations sans modèle d'indexation", + "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.experimentalTitle": "Expérimental", + "visualizations.newVisWizard.experimentalTooltip": "Cette visualisation est susceptible d'être modifiée ou supprimée dans une version ultérieure, et n'est pas soumise à l'accord de niveau de service d'assistance.", + "visualizations.newVisWizard.exploreOptionLinkText": "Explorer les options", + "visualizations.newVisWizard.filterVisTypeAriaLabel": "Filtrer un type de visualisation", + "visualizations.newVisWizard.goBackLink": "Sélectionner une visualisation différente", + "visualizations.newVisWizard.helpTextAriaLabel": "Commencez à créer votre visualisation en sélectionnant un type pour cette visualisation. Appuyez sur Échap pour fermer ce mode. Appuyez sur Tab pour aller plus loin.", + "visualizations.newVisWizard.learnMoreText": "Envie d'en savoir plus ?", + "visualizations.newVisWizard.newVisTypeTitle": "Nouveau {visTypeName}", + "visualizations.newVisWizard.readDocumentationLink": "Lire la documentation", + "visualizations.newVisWizard.resultsFound": "{resultCount, plural, one {type trouvé} other {types trouvés}}", + "visualizations.newVisWizard.searchSelection.notFoundLabel": "Aucun index ni aucune recherche enregistrée correspondant(e) trouvé(e).", + "visualizations.newVisWizard.searchSelection.savedObjectType.search": "Recherche enregistrée", + "visualizations.newVisWizard.title": "Nouvelle visualisation", + "visualizations.newVisWizard.toolsGroupTitle": "Outils", + "visualizations.noResultsFoundTitle": "Aucun résultat trouvé", + "visualizations.savedObjectName": "Visualisation", + "visualizations.savingVisualizationFailed.errorMsg": "L'enregistrement de la visualisation a échoué", + "visualizations.visualizationTypeInvalidMessage": "Type de visualisation non valide \"{visType}\"", + "xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage": "Le type d'action \"{id}\" n'est pas enregistré.", + "xpack.actions.actionTypeRegistry.register.duplicateActionTypeErrorMessage": "Le type d'action \"{id}\" est déjà enregistré.", + "xpack.actions.alertHistoryEsIndexConnector.name": "Index Elasticsearch d'historique d'alertes", + "xpack.actions.appName": "Actions", + "xpack.actions.builtin.case.swimlaneTitle": "Swimlane", + "xpack.actions.builtin.cases.jiraTitle": "Jira", + "xpack.actions.builtin.cases.resilientTitle": "IBM Resilient", + "xpack.actions.builtin.configuration.apiAllowedHostsError": "erreur lors de la configuration de l'action du connecteur : {message}", + "xpack.actions.builtin.email.customViewInKibanaMessage": "Ce message a été envoyé par Kibana. [{kibanaFooterLinkText}]({link}).", + "xpack.actions.builtin.email.errorSendingErrorMessage": "erreur lors de l'envoi de l'e-mail", + "xpack.actions.builtin.email.kibanaFooterLinkText": "Accéder à Kibana", + "xpack.actions.builtin.email.sentByKibanaMessage": "Ce message a été envoyé par Kibana.", + "xpack.actions.builtin.emailTitle": "E-mail", + "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "erreur lors de l'indexation des documents", + "xpack.actions.builtin.esIndexTitle": "Index", + "xpack.actions.builtin.jira.configuration.apiAllowedHostsError": "erreur lors de la configuration de l'action du connecteur : {message}", + "xpack.actions.builtin.pagerduty.invalidTimestampErrorMessage": "erreur lors de l'analyse de l'horodatage \"{timestamp}\"", + "xpack.actions.builtin.pagerduty.missingDedupkeyErrorMessage": "DedupKey est requis lorsque eventAction est \"{eventAction}\"", + "xpack.actions.builtin.pagerduty.pagerdutyConfigurationError": "erreur lors de la configuration de l'action pagerduty : {message}", + "xpack.actions.builtin.pagerduty.postingErrorMessage": "erreur lors de la publication de l'événement pagerduty", + "xpack.actions.builtin.pagerduty.postingRetryErrorMessage": "erreur lors de la publication de l'événement pagerduty : statut http {status}, réessayer ultérieurement", + "xpack.actions.builtin.pagerduty.postingUnexpectedErrorMessage": "erreur lors de la publication de l'événement pagerduty : statut inattendu {status}", + "xpack.actions.builtin.pagerduty.timestampParsingFailedErrorMessage": "erreur lors de l'analyse de l'horodatage \"{timestamp}\" : {message}", + "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", + "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "erreur lors du logging du message", + "xpack.actions.builtin.serverLogTitle": "Log de serveur", + "xpack.actions.builtin.serviceNowITSMTitle": "ServiceNow ITSM", + "xpack.actions.builtin.serviceNowSIRTitle": "ServiceNow SecOps", + "xpack.actions.builtin.serviceNowTitle": "ServiceNow", + "xpack.actions.builtin.slack.errorPostingErrorMessage": "erreur lors de la publication du message slack", + "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "erreur lors de la publication d'un message slack, réessayer à cette date/heure : {retryString}", + "xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage": "erreur lors de la publication d'un message slack, réessayer ultérieurement", + "xpack.actions.builtin.slack.slackConfigurationError": "erreur lors de la configuration de l'action slack : {message}", + "xpack.actions.builtin.slack.slackConfigurationErrorNoHostname": "erreur lors de la configuration de l'action slack : impossible d'analyser le nom de l'hôte depuis webhookUrl", + "xpack.actions.builtin.slack.unexpectedHttpResponseErrorMessage": "réponse http inattendue de Slack : {httpStatus} {httpStatusText}", + "xpack.actions.builtin.slack.unexpectedNullResponseErrorMessage": "réponse nulle inattendue de Slack", + "xpack.actions.builtin.slackTitle": "Slack", + "xpack.actions.builtin.swimlane.configuration.apiAllowedHostsError": "erreur lors de la configuration de l'action du connecteur : {message}", + "xpack.actions.builtin.swimlaneTitle": "Swimlane", + "xpack.actions.builtin.teams.errorPostingRetryDateErrorMessage": "erreur lors de la publication d'un message Microsoft Teams, réessayer à cette date/heure : {retryString}", + "xpack.actions.builtin.teams.errorPostingRetryLaterErrorMessage": "erreur lors de la publication d'un message Microsoft Teams, réessayer ultérieurement", + "xpack.actions.builtin.teams.invalidResponseErrorMessage": "erreur lors de la publication sur Microsoft Teams, réponse non valide", + "xpack.actions.builtin.teams.teamsConfigurationError": "erreur lors de la configuration de l'action teams : {message}", + "xpack.actions.builtin.teams.teamsConfigurationErrorNoHostname": "erreur lors de la configuration de l'action teams : impossible d'analyser le nom de l'hôte depuis webhookUrl", + "xpack.actions.builtin.teams.unreachableErrorMessage": "erreur lors de la publication sur Microsoft Teams, erreur inattendue", + "xpack.actions.builtin.teamsTitle": "Microsoft Teams", + "xpack.actions.builtin.webhook.invalidResponseErrorMessage": "erreur lors de l'appel de webhook, réponse non valide", + "xpack.actions.builtin.webhook.invalidResponseRetryDateErrorMessage": "erreur lors de l'appel de webhook, réessayer à cette date/heure : {retryString}", + "xpack.actions.builtin.webhook.invalidResponseRetryLaterErrorMessage": "erreur lors de l'appel de webhook, réessayer ultérieurement", + "xpack.actions.builtin.webhook.invalidUsernamePassword": "l'utilisateur et le mot de passe doivent être spécifiés", + "xpack.actions.builtin.webhook.requestFailedErrorMessage": "erreur lors de l'appel de webhook, requête échouée", + "xpack.actions.builtin.webhook.unreachableErrorMessage": "erreur lors de l'appel de webhook, erreur inattendue", + "xpack.actions.builtin.webhook.webhookConfigurationError": "erreur lors de la configuration de l'action webhook : {message}", + "xpack.actions.builtin.webhook.webhookConfigurationErrorNoHostname": "erreur lors de la configuration de l'action webhook : impossible d'analyser l'url : {err}", + "xpack.actions.builtin.webhookTitle": "Webhook", + "xpack.actions.disabledActionTypeError": "le type d'action \"{actionType}\" n'est pas activé dans la configuration Kibana xpack.actions.enabledActionTypes", + "xpack.actions.featureRegistry.actionsFeatureName": "Actions et connecteurs", + "xpack.actions.savedObjects.goToConnectorsButtonText": "Accéder aux connecteurs", + "xpack.actions.savedObjects.onImportText": "{connectorsWithSecretsLength} {connectorsWithSecretsLength, plural, one {Le connecteur contient} other {Les connecteurs contiennent}} des informations sensibles qui requièrent des mises à jour.", + "xpack.actions.serverSideErrors.expirerdLicenseErrorMessage": "Le type d'action {actionTypeId} est désactivé, car votre licence {licenseType} a expiré.", + "xpack.actions.serverSideErrors.invalidLicenseErrorMessage": "Le type d'action {actionTypeId} est désactivé, car votre licence {licenseType} ne le prend pas en charge. Veuillez mettre à niveau votre licence.", + "xpack.actions.serverSideErrors.predefinedActionDeleteDisabled": "L'action préconfigurée {id} n'est pas autorisée à effectuer des suppressions.", + "xpack.actions.serverSideErrors.predefinedActionUpdateDisabled": "L'action préconfigurée {id} n'est pas autorisée à effectuer des mises à jour.", + "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "Le type d'action {actionTypeId} est désactivé, car les informations de licence ne sont pas disponibles actuellement.", + "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "Les actions sont indisponibles - les informations de licence ne sont pas disponibles actuellement.", + "xpack.actions.urlAllowedHostsConfigurationError": "Le {field} cible \"{value}\" n'est pas ajouté à la configuration Kibana xpack.actions.allowedHosts", + "xpack.alerting.alertNavigationRegistry.get.missingNavigationError": "La navigation pour le type d'alerte \"{alertType}\" dans \"{consumer}\" n'est pas enregistrée.", + "xpack.alerting.alertNavigationRegistry.register.duplicateDefaultError": "La navigation par défaut dans \"{consumer}\" est déjà enregistrée.", + "xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError": "La navigation pour le type d'alerte \"{alertType}\" dans \"{consumer}\" est déjà enregistrée.", + "xpack.alerting.api.error.disabledApiKeys": "L'alerting se base sur les clés d'API qui semblent désactivées", + "xpack.alerting.appName": "Alerting", + "xpack.alerting.builtinActionGroups.recovered": "Récupéré", + "xpack.alerting.injectActionParams.email.kibanaFooterLinkText": "Afficher la règle dans Kibana", + "xpack.alerting.rulesClient.invalidDate": "Date non valide pour le {field} de paramètre : \"{dateValue}\"", + "xpack.alerting.rulesClient.validateActions.invalidGroups": "Groupes d'actions non valides : {groups}", + "xpack.alerting.rulesClient.validateActions.misconfiguredConnector": "Connecteurs non valides : {groups}", + "xpack.alerting.ruleTypeRegistry.register.customRecoveryActionGroupUsageError": "Le type de règle [id=\"{id}\"] ne peut pas être enregistré. Le groupe d'actions [{actionGroup}] ne peut pas être utilisé à la fois comme groupe de récupération et comme groupe d'actions actif.", + "xpack.alerting.ruleTypeRegistry.register.reservedActionGroupUsageError": "Le type de règle [id=\"{id}\"] ne peut pas être enregistré. Les groupes d'actions [{actionGroups}] sont réservés par le framework.", + "xpack.alerting.savedObjects.goToRulesButtonText": "Accéder aux règles", + "xpack.alerting.savedObjects.onImportText": "{rulesSavedObjectsLength} {rulesSavedObjectsLength, plural, one {La règle doit être activée} other {Les règles doivent être activées}} après l'importation.", + "xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage": "Les alertes sont indisponibles – les informations de licence ne sont pas disponibles actuellement.", + "xpack.apm.a.thresholdMet": "Seuil atteint", + "xpack.apm.addDataButtonLabel": "Ajouter des données", + "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.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.label": "Heure de la requête API", + "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.label": "Capturer le 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.label": "Capturer les en-têtes", + "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.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.", + "xpack.apm.agentConfig.configTable.createConfigButtonLabel": "Créer une configuration", + "xpack.apm.agentConfig.configTable.emptyPromptTitle": "Aucune configuration trouvée.", + "xpack.apm.agentConfig.configTable.environmentColumnLabel": "Environnement de service", + "xpack.apm.agentConfig.configTable.lastUpdatedColumnLabel": "Dernière mise à jour", + "xpack.apm.agentConfig.configTable.notAppliedTooltipMessage": "Appliqué par aucun agent pour le moment", + "xpack.apm.agentConfig.configTable.serviceNameColumnLabel": "Nom de service", + "xpack.apm.agentConfig.configurationsPanelTitle": "Configurations", + "xpack.apm.agentConfig.configurationsPanelTitle.noPermissionTooltipLabel": "Votre rôle d'utilisateur ne dispose pas des autorisations nécessaires pour créer des configurations d'agent", + "xpack.apm.agentConfig.createConfigButtonLabel": "Créer une configuration", + "xpack.apm.agentConfig.createConfigTitle": "Créer une configuration", + "xpack.apm.agentConfig.deleteModal.cancel": "Annuler", + "xpack.apm.agentConfig.deleteModal.confirm": "Supprimer", + "xpack.apm.agentConfig.deleteModal.text": "Vous êtes sur le point de supprimer la configuration du service \"{serviceName}\" et de l'environnement \"{environment}\".", + "xpack.apm.agentConfig.deleteModal.title": "Supprimer la configuration", + "xpack.apm.agentConfig.deleteSection.deleteConfigFailedText": "Une erreur est survenue lors de la suppression d'une configuration de \"{serviceName}\". Erreur : \"{errorMessage}\"", + "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}\" avec succès. La propagation jusqu'aux agents pourra prendre un certain temps.", + "xpack.apm.agentConfig.deleteSection.deleteConfigSucceededTitle": "La configuration a été supprimée", + "xpack.apm.agentConfig.editConfigTitle": "Modifier la configuration", + "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.logLevel.description": "Définit le niveau de logging pour l'agent", + "xpack.apm.agentConfig.logLevel.label": "Niveau de log", + "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\" pour 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.", + "xpack.apm.agentConfig.profilingInferredSpansEnabled.label": "Intervalles générés par le profilage activés", + "xpack.apm.agentConfig.profilingInferredSpansExcludedClasses.description": "Exclut les classes pour lesquelles aucun intervalle généré par le profiler ne doit être créé. Cette option prend en charge le caractère générique \"*\" qui correspond à zéro caractère ou plus. 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.profilingInferredSpansExcludedClasses.label": "Classes exclues des intervalles générés par le profilage", + "xpack.apm.agentConfig.profilingInferredSpansIncludedClasses.description": "Si cette option est définie, l'agent ne créera des intervalles générés que pour les méthodes correspondant à cette liste. La définition d'une valeur peut diminuer légèrement la surcharge et réduire l'encombrement en ne créant des intervalles que pour les classes qui vous intéressent. Cette option prend en charge le caractère générique \"*\" qui correspond à zéro caractère ou plus. Par exemple : \"org.example.myapp.*\". 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.profilingInferredSpansIncludedClasses.label": "Classes incluses des intervalles générés par le profilage", + "xpack.apm.agentConfig.profilingInferredSpansMinDuration.description": "Durée minimale d'un intervalle généré. Veuillez noter que la durée minimale est également définie de façon implicite par l'intervalle d'échantillonnage. Toutefois, l'augmentation de l'intervalle d'échantillonnage diminue également la précision de la durée des intervalles générés.", + "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.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.", + "xpack.apm.agentConfig.sanitizeFiledNames.label": "Nettoyer les noms des champs", + "xpack.apm.agentConfig.saveConfig.failed.text": "Une erreur est survenue pendant l'enregistrement de la configuration de \"{serviceName}\". Erreur : \"{errorMessage}\"", + "xpack.apm.agentConfig.saveConfig.failed.title": "La configuration n'a pas pu être enregistrée", + "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.label": "Délai d'expiration du serveur", + "xpack.apm.agentConfig.servicePage.alreadyConfiguredOption": "déjà configuré", + "xpack.apm.agentConfig.servicePage.cancelButton": "Annuler", + "xpack.apm.agentConfig.servicePage.environment.description": "Seul un environnement unique par configuration est pris en charge.", + "xpack.apm.agentConfig.servicePage.environment.fieldLabel": "Environnement de service", + "xpack.apm.agentConfig.servicePage.environment.title": "Environnement", + "xpack.apm.agentConfig.servicePage.service.description": "Choisissez le service que vous souhaitez configurer.", + "xpack.apm.agentConfig.servicePage.service.fieldLabel": "Nom de service", + "xpack.apm.agentConfig.servicePage.service.title": "Service", + "xpack.apm.agentConfig.settingsPage.discardChangesButton": "Abandonner les modifications", + "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.spanFramesMinDuration.description": "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, par ex. 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.label": "Durée minimale des cadres des intervalles", + "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", + "xpack.apm.agentConfig.stressMonitorCpuDurationThreshold.description": "Durée minimale requise pour déterminer si le système est actuellement sous tension ou si la tension précédemment détectée a été allégée. Toutes les mesures réalisées pendant ce laps de temps doivent être cohérentes par rapport au seuil concerné pour pouvoir détecter un changement d'état de tension. La valeur doit être d'au moins \"1m\".", + "xpack.apm.agentConfig.stressMonitorCpuDurationThreshold.label": "Seuil de durée de tension CPU du monitoring", + "xpack.apm.agentConfig.stressMonitorGcReliefThreshold.description": "Seuil utilisé par le monitoring RM pour identifier le moment auquel le segment de mémoire n'est pas sous tension. Si le \"stress_monitor_gc_stress_threshold\" a été franchi, l'agent le considérera comme un état de tension du segment de mémoire. Pour déterminer l'état de tension comme terminé, le pourcentage de mémoire occupée dans TOUS les pools de mémoire doit être inférieur à ce seuil. Le monitoring RM ne se base que sur la consommation de mémoire mesurée après une RM récente.", + "xpack.apm.agentConfig.stressMonitorGcReliefThreshold.label": "Seuil d'allègement de la tension du monitoring RM", + "xpack.apm.agentConfig.stressMonitorGcStressThreshold.description": "Seuil utilisé par le monitoring RM pour identifier la tension du segment de mémoire. Ce même seuil sera utilisé pour tous les pools de mémoire, de sorte que si L'UN d'entre eux a un pourcentage d'utilisation qui dépasse ce seuil, l'agent l'interprétera comme une tension de segment de mémoire. Le monitoring RM ne se base que sur la consommation de mémoire mesurée après une RM récente.", + "xpack.apm.agentConfig.stressMonitorGcStressThreshold.label": "Seuil de tension du monitoring RM", + "xpack.apm.agentConfig.stressMonitorSystemCpuReliefThreshold.description": "Seuil utilisé par le monitoring du CPU système pour déterminer que le système n'est pas sous tension au niveau du processeur. Si le monitor détecte une tension de CPU, le CPU système mesuré doit être inférieur à ce seuil pour une durée d'au moins \"stress_monitor_cpu_duration_threshold\", pour que le monitoring établisse l'allègement de la tension de CPU.", + "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.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.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.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.unsavedSetting.tooltip": "Non enregistré", + "xpack.apm.agentMetrics.java.gcRate": "Taux RM", + "xpack.apm.agentMetrics.java.gcRateChartTitle": "Récupération de mémoire par minute", + "xpack.apm.agentMetrics.java.gcTime": "Durée RM", + "xpack.apm.agentMetrics.java.gcTimeChartTitle": "Durée de récupération de mémoire par minute", + "xpack.apm.agentMetrics.java.heapMemoryChartTitle": "Segment de mémoire", + "xpack.apm.agentMetrics.java.heapMemorySeriesCommitted": "Moy. allouée", + "xpack.apm.agentMetrics.java.heapMemorySeriesMax": "Limite moy.", + "xpack.apm.agentMetrics.java.heapMemorySeriesUsed": "Moy. utilisée", + "xpack.apm.agentMetrics.java.nonHeapMemoryChartTitle": "Segment de mémoire sans tas", + "xpack.apm.agentMetrics.java.nonHeapMemorySeriesCommitted": "Moy. allouée", + "xpack.apm.agentMetrics.java.nonHeapMemorySeriesUsed": "Moy. utilisée", + "xpack.apm.agentMetrics.java.threadCount": "Nombre moy.", + "xpack.apm.agentMetrics.java.threadCountChartTitle": "Nombre de threads", + "xpack.apm.agentMetrics.java.threadCountMax": "Nombre max", + "xpack.apm.aggregatedTransactions.fallback.badge": "Basé sur les transactions échantillonnées", + "xpack.apm.aggregatedTransactions.fallback.tooltip": "Cette page utilise les données d'événements de transactions lorsqu'aucun événement d'indicateur n'a été trouvé dans la plage temporelle actuelle, ou lorsqu'un filtre a été appliqué en fonction des champs indisponibles dans les documents des événements d'indicateurs.", + "xpack.apm.alertAnnotationButtonAriaLabel": "Afficher les détails de l'alerte", + "xpack.apm.alertAnnotationCriticalTitle": "Alerte critique", + "xpack.apm.alertAnnotationNoSeverityTitle": "Alerte", + "xpack.apm.alertAnnotationWarningTitle": "Alerte d'avertissement", + "xpack.apm.alerting.fields.environment": "Environnement", + "xpack.apm.alerting.fields.service": "Service", + "xpack.apm.alerting.fields.type": "Type", + "xpack.apm.alerts.action_variables.environment": "Type de transaction pour lequel l'alerte est créée", + "xpack.apm.alerts.action_variables.intervalSize": "La longueur et l'unité de la période à laquelle les conditions de l'alerte ont été remplies", + "xpack.apm.alerts.action_variables.serviceName": "Service pour lequel l'alerte est créée", + "xpack.apm.alerts.action_variables.threshold": "Toute valeur de déclenchement dépassant cette valeur lancera l'alerte", + "xpack.apm.alerts.action_variables.transactionType": "Type de transaction pour lequel l'alerte est créée", + "xpack.apm.alerts.action_variables.triggerValue": "Valeur ayant dépassé le seuil et déclenché l'alerte", + "xpack.apm.alerts.anomalySeverity.criticalLabel": "critique", + "xpack.apm.alerts.anomalySeverity.majorLabel": "majeur", + "xpack.apm.alerts.anomalySeverity.minor": "mineur", + "xpack.apm.alerts.anomalySeverity.scoreDetailsDescription": "score {value} {value, select, critical {} other {et plus}}", + "xpack.apm.alerts.anomalySeverity.warningLabel": "avertissement", + "xpack.apm.alertTypes.errorCount.defaultActionMessage": "L'alerte \\{\\{alertName\\}\\} se déclenche en raison des conditions suivantes :\n\n- Nom de service : \\{\\{context.serviceName\\}\\}\n- Environnement : \\{\\{context.environment\\}\\}\n- Seuil : \\{\\{context.threshold\\}\\} erreurs\n- Valeur de déclenchement : \\{\\{context.triggerValue\\}\\} erreurs sur la dernière période de \\{\\{context.interval\\}\\}", + "xpack.apm.alertTypes.errorCount.description": "Alerte lorsque le nombre d'erreurs d'un service dépasse un seuil défini.", + "xpack.apm.alertTypes.transactionDuration.defaultActionMessage": "L'alerte \\{\\{alertName\\}\\} se déclenche en raison des conditions suivantes :\n\n- Nom de service : \\{\\{context.serviceName\\}\\}\n- Type : \\{\\{context.transactionType\\}\\}\n- Environnement : \\{\\{context.environment\\}\\}\n- Seuil de latence : \\{\\{context.threshold\\}\\} ms\n- Latence observée : \\{\\{context.triggerValue\\}\\} sur la dernière période de \\{\\{context.interval\\}\\}", + "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.transactionDurationAnomaly.defaultActionMessage": "L'alerte \\{\\{alertName\\}\\} se déclenche en raison des conditions suivantes :\n\n- Nom de service : \\{\\{context.serviceName\\}\\}\n- Type : \\{\\{context.transactionType\\}\\}\n- Environnement : \\{\\{context.environment\\}\\}\n- Seuil de sévérité : \\{\\{context.threshold\\}\\}\n- Valeur de sévérité : \\{\\{context.triggerValue\\}\\}\n", + "xpack.apm.alertTypes.transactionDurationAnomaly.description": "Alerte lorsque la latence d'un service est anormale.", + "xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage": "L'alerte \\{\\{alertName\\}\\} se déclenche en raison des conditions suivantes :\n\n- Nom de service : \\{\\{context.serviceName\\}\\}\n- Type : \\{\\{context.transactionType\\}\\}\n- Environnement : \\{\\{context.environment\\}\\}\n- Seuil : \\{\\{context.threshold\\}\\} %\n- Valeur de déclenchement : \\{\\{context.triggerValue\\}\\} % des erreurs sur la dernière période de \\{\\{context.interval\\}\\}", + "xpack.apm.alertTypes.transactionErrorRate.description": "Alerte lorsque le taux d'erreurs de transaction d'un service dépasse un seuil défini.", + "xpack.apm.analyzeDataButton.label": "Analyser les données", + "xpack.apm.analyzeDataButton.tooltip": "EXPÉRIMENTAL - La fonctionnalité Analyser les données vous permet de sélectionner et de filtrer les données de résultat dans toute dimension et de rechercher la cause ou l'impact des problèmes de performances", + "xpack.apm.anomaly_detection.error.invalid_license": "Pour utiliser la détection des anomalies, vous devez disposer d'une licence Elastic Platinum. Cette licence vous permet de monitorer vos services à l'aide du Machine Learning.", + "xpack.apm.anomaly_detection.error.missing_read_privileges": "Vous devez disposer des privilèges \"read\" (lecture) pour le Machine Learning et l'APM pour consulter les tâches de détection des anomalies", + "xpack.apm.anomaly_detection.error.missing_write_privileges": "Vous devez disposer des privilèges \"write\" (écriture) pour le Machine Learning et l'APM pour créer des tâches de détection des anomalies", + "xpack.apm.anomaly_detection.error.not_available": "Le Machine Learning est indisponible", + "xpack.apm.anomaly_detection.error.not_available_in_space": "Le Machine Learning est indisponible dans l'espace sélectionné", + "xpack.apm.anomalyDetection.createJobs.failed.text": "Une erreur est survenue lors de la création d'une ou de plusieurs tâches de détection des anomalies pour les environnements de service APM [{environments}]. Erreur : \"{errorMessage}\"", + "xpack.apm.anomalyDetection.createJobs.failed.title": "Les tâches de détection des anomalies n'ont pas pu être créées", + "xpack.apm.anomalyDetection.createJobs.succeeded.text": "Tâches de détection des anomalies créées avec succès pour les environnements de service APM [{environments}]. Le démarrage de l'analyse du trafic à la recherche d'anomalies par le Machine Learning va prendre un certain temps.", + "xpack.apm.anomalyDetection.createJobs.succeeded.title": "Tâches de détection des anomalies créées", + "xpack.apm.anomalyDetectionSetup.linkLabel": "Détection des anomalies", + "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "La détection des anomalies n'est pas encore activée pour l'environnement \"{currentEnvironment}\". Cliquez pour continuer la configuration.", + "xpack.apm.anomalyDetectionSetup.notEnabledText": "La détection des anomalies n'est pas encore activée. Cliquez pour continuer la configuration.", + "xpack.apm.api.fleet.cloud_apm_package_policy.requiredRoleOnCloud": "Opération autorisée uniquement pour les utilisateurs Elastic Cloud disposant du rôle de superutilisateur.", + "xpack.apm.api.fleet.fleetSecurityRequired": "Les plug-ins Fleet et Security sont requis", + "xpack.apm.apmDescription": "Collecte automatiquement les indicateurs et les erreurs de performances détaillés depuis vos applications.", + "xpack.apm.apmSchema.index": "Schéma du serveur APM - Index", + "xpack.apm.apmSettings.index": "Paramètres APM - Index", + "xpack.apm.backendDetail.dependenciesTableColumnBackend": "Service", + "xpack.apm.backendDetail.dependenciesTableTitle": "Services en amont", + "xpack.apm.backendDetailFailedTransactionRateChartTitle": "Taux de transactions ayant échoué", + "xpack.apm.backendDetailLatencyChartTitle": "Latence", + "xpack.apm.backendDetailThroughputChartTitle": "Rendement", + "xpack.apm.backendErrorRateChart.chartTitle": "Taux de transactions ayant échoué", + "xpack.apm.backendErrorRateChart.previousPeriodLabel": "Période précédente", + "xpack.apm.backendLatencyChart.chartTitle": "Latence", + "xpack.apm.backendLatencyChart.previousPeriodLabel": "Période précédente", + "xpack.apm.backendThroughputChart.chartTitle": "Rendement", + "xpack.apm.backendThroughputChart.previousPeriodLabel": "Période précédente", + "xpack.apm.chart.annotation.version": "Version", + "xpack.apm.chart.cpuSeries.processAverageLabel": "Moyenne de processus", + "xpack.apm.chart.cpuSeries.processMaxLabel": "Max de processus", + "xpack.apm.chart.cpuSeries.systemAverageLabel": "Moyenne du système", + "xpack.apm.chart.cpuSeries.systemMaxLabel": "Max du système", + "xpack.apm.chart.error": "Une erreur est survenue lors de la tentative de récupération des données. Réessayez plus tard", + "xpack.apm.chart.memorySeries.systemAverageLabel": "Moyenne", + "xpack.apm.chart.memorySeries.systemMaxLabel": "Max", + "xpack.apm.compositeSpanCallsLabel": ", {count} appels, sur une moyenne de {duration}", + "xpack.apm.compositeSpanDurationLabel": "Durée moyenne", + "xpack.apm.correlations.correlationsTable.excludeDescription": "Filtrer la valeur", + "xpack.apm.correlations.correlationsTable.excludeLabel": "Exclure", + "xpack.apm.correlations.correlationsTable.filterDescription": "Filtrer par valeur", + "xpack.apm.correlations.correlationsTable.filterLabel": "Filtre", + "xpack.apm.correlations.correlationsTable.loadingText": "Chargement", + "xpack.apm.correlations.correlationsTable.noDataText": "Aucune donnée", + "xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel": "Nom du champ", + "xpack.apm.correlations.failedTransactions.correlationsTable.fieldValueLabel": "Valeur du champ", + "xpack.apm.correlations.failedTransactions.correlationsTable.impactLabel": "Impact", + "xpack.apm.correlations.failedTransactions.correlationsTable.pValueLabel": "Score", + "xpack.apm.correlations.failedTransactions.errorTitle": "Une erreur est survenue lors de l'exécution de corrélations sur les transactions ayant échoué", + "xpack.apm.correlations.failedTransactions.highImpactText": "Élevé", + "xpack.apm.correlations.failedTransactions.lowImpactText": "Bas", + "xpack.apm.correlations.failedTransactions.mediumImpactText": "Moyen", + "xpack.apm.correlations.failedTransactions.panelTitle": "Transactions ayant échoué", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "Filtre", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "Score de corrélation [0-1] d'un attribut ; plus le score est élevé, plus un attribut augmente la latence.", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel": "Corrélation", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeDescription": "Filtrer la valeur", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeLabel": "Exclure", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel": "Nom du champ", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel": "Valeur du champ", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription": "Filtrer par valeur", + "xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel": "Filtre", + "xpack.apm.correlations.latencyCorrelations.errorTitle": "Une erreur est survenue lors de la récupération des corrélations", + "xpack.apm.correlations.latencyCorrelations.panelTitle": "Distribution de la latence", + "xpack.apm.correlations.latencyCorrelations.tableTitle": "Corrélations", + "xpack.apm.correlations.latencyPopoverBasicExplanation": "Les corrélations vous aident à découvrir quels attributs contribuent à l'augmentation des temps de réponse des transactions ou de la latence.", + "xpack.apm.correlations.latencyPopoverChartExplanation": "Le graphique de distribution de la latence permet de visualiser la latence globale des transactions dans le service. Lorsque vous passez votre souris sur des attributs du tableau, leur distribution de latence est ajoutée au graphique.", + "xpack.apm.correlations.latencyPopoverFilterExplanation": "Vous pouvez également ajouter ou retirer des filtres pour modifier les requêtes dans l'application APM.", + "xpack.apm.correlations.latencyPopoverPerformanceExplanation": "Cette analyse réalise des recherches statistiques sur un grand nombre d'attributs. Pour les plages temporelles étendues et les services ayant un rendement de transactions élevé, cela peut prendre un certain temps. Réduisez la plage temporelle pour améliorer les performances.", + "xpack.apm.correlations.latencyPopoverTableExplanation": "Le tableau est trié par coefficient de corrélation, de 0 à 1. Les attributs ayant des valeurs de corrélation plus élevées sont plus susceptibles de contribuer à des transactions à haute latence.", + "xpack.apm.correlations.latencyPopoverTitle": "Corrélations de latence", + "xpack.apm.customLink.buttom.create": "Créer un lien personnalisé", + "xpack.apm.customLink.buttom.create.title": "Créer", + "xpack.apm.customLink.buttom.manage": "Gérer des liens personnalisés", + "xpack.apm.customLink.empty": "Aucun lien personnalisé trouvé. Configurez vos propres liens personnalisés, par ex. un lien vers un tableau de bord spécifique ou un lien externe.", + "xpack.apm.dependenciesTable.columnErrorRate": "Taux de transactions ayant échoué", + "xpack.apm.dependenciesTable.columnImpact": "Impact", + "xpack.apm.dependenciesTable.columnLatency": "Latence (moy.)", + "xpack.apm.dependenciesTable.columnThroughput": "Rendement", + "xpack.apm.dependenciesTable.serviceMapLinkText": "Afficher la carte des services", + "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.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.errorCountAlertTrigger.errors": " erreurs", + "xpack.apm.errorGroupDetails.culpritLabel": "Coupable", + "xpack.apm.errorGroupDetails.errorGroupTitle": "Groupe d'erreurs {errorGroupId}", + "xpack.apm.errorGroupDetails.errorOccurrenceTitle": "Occurrence d'erreur", + "xpack.apm.errorGroupDetails.exceptionMessageLabel": "Message d'exception", + "xpack.apm.errorGroupDetails.logMessageLabel": "Message log", + "xpack.apm.errorGroupDetails.occurrencesChartLabel": "Occurrences", + "xpack.apm.errorGroupDetails.relatedTransactionSample": "Échantillon de transaction associée", + "xpack.apm.errorGroupDetails.unhandledLabel": "Non géré", + "xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "Visualisez {occurrencesCount} {occurrencesCount, plural, one {occurrence} other {occurrences}} dans Discover.", + "xpack.apm.errorRate": "Taux de transactions ayant échoué", + "xpack.apm.errorRate.chart.errorRate": "Taux de transactions ayant échoué (moy.)", + "xpack.apm.errorRate.chart.errorRate.previousPeriodLabel": "Période précédente", + "xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "Message d'erreur et coupable", + "xpack.apm.errorsTable.groupIdColumnDescription": "Hachage de la trace de pile. Regroupe les erreurs similaires, même lorsque le message d'erreur est différent en raison des paramètres dynamiques.", + "xpack.apm.errorsTable.groupIdColumnLabel": "ID du groupe", + "xpack.apm.errorsTable.noErrorsLabel": "Aucune erreur n'a été trouvée", + "xpack.apm.errorsTable.occurrencesColumnLabel": "Occurrences", + "xpack.apm.errorsTable.typeColumnLabel": "Type", + "xpack.apm.errorsTable.unhandledLabel": "Non géré", + "xpack.apm.failedTransactionsCorrelations.licenseCheckText": "Pour utiliser la fonctionnalité de corrélation des transactions ayant échoué, vous devez disposer d'une licence Elastic Platinum.", + "xpack.apm.featureRegistry.apmFeatureName": "APM et expérience utilisateur", + "xpack.apm.feedbackMenu.appName": "APM", + "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", + "xpack.apm.filter.environment.allLabel": "Tous", + "xpack.apm.filter.environment.label": "Environnement", + "xpack.apm.filter.environment.notDefinedLabel": "Non défini", + "xpack.apm.filter.environment.selectEnvironmentLabel": "Sélectionner l'environnement", + "xpack.apm.fleet_integration.settings.advancedOptionsLavel": "Options avancées", + "xpack.apm.fleet_integration.settings.apm.capturePersonalDataDescription": "Capturer des données personnelles, telles que l'IP ou l'agent utilisateur", + "xpack.apm.fleet_integration.settings.apm.capturePersonalDataTitle": "Capture des données personnelles", + "xpack.apm.fleet_integration.settings.apm.defaultServiceEnvironmentDescription": "Environnement de service par défaut pour l'enregistrement des événements n'ayant aucun environnement de service défini.", + "xpack.apm.fleet_integration.settings.apm.defaultServiceEnvironmentLabel": "Environnement de service par défaut", + "xpack.apm.fleet_integration.settings.apm.defaultServiceEnvironmentTitle": "Configuration de service", + "xpack.apm.fleet_integration.settings.apm.expvarEnabledDescription": "Exposé sous /debug/vars", + "xpack.apm.fleet_integration.settings.apm.expvarEnabledTitle": "Activer la prise en charge d'expvar de Golang pour le serveur APM", + "xpack.apm.fleet_integration.settings.apm.hostDescription": "Choisissez un nom et une description pour identifier facilement le type d'utilisation de cette intégration.", + "xpack.apm.fleet_integration.settings.apm.hostLabel": "Hôte", + "xpack.apm.fleet_integration.settings.apm.hostTitle": "Configuration du serveur", + "xpack.apm.fleet_integration.settings.apm.idleTimeoutLabel": "Temps d'inactivité avant la fermeture de la connexion sous-jacente", + "xpack.apm.fleet_integration.settings.apm.maxConnectionsLabel": "Connexions acceptées simultanément", + "xpack.apm.fleet_integration.settings.apm.maxEventBytesLabel": "Taille maximale par événement (octets)", + "xpack.apm.fleet_integration.settings.apm.maxHeaderBytesDescription": "Définissez des limites pour la taille des en-têtes de requêtes et les configurations de temporisation.", + "xpack.apm.fleet_integration.settings.apm.maxHeaderBytesLabel": "Taille maximale de l'en-tête d'une requête (octets)", + "xpack.apm.fleet_integration.settings.apm.maxHeaderBytesTitle": "Limites", + "xpack.apm.fleet_integration.settings.apm.readTimeoutLabel": "Durée maximale pour la lecture d'une requête intégrale", + "xpack.apm.fleet_integration.settings.apm.responseHeadersDescription": "Définissez des limites pour la taille des en-têtes de requêtes et les configurations de temporisation.", + "xpack.apm.fleet_integration.settings.apm.responseHeadersHelpText": "Peut être utilisé pour la conformité à la politique de sécurité.", + "xpack.apm.fleet_integration.settings.apm.responseHeadersLabel": "En-têtes HTTP personnalisés ajoutés aux réponses HTTP", + "xpack.apm.fleet_integration.settings.apm.responseHeadersTitle": "En-têtes personnalisés", + "xpack.apm.fleet_integration.settings.apm.settings.subtitle": "Paramètres de l'intégration APM.", + "xpack.apm.fleet_integration.settings.apm.settings.title": "Général", + "xpack.apm.fleet_integration.settings.apm.shutdownTimeoutLabel": "Durée maximale avant la libération des ressources lors de l'arrêt", + "xpack.apm.fleet_integration.settings.apm.urlLabel": "URL", + "xpack.apm.fleet_integration.settings.apm.writeTimeoutLabel": "Durée maximale pour la rédaction d'une réponse", + "xpack.apm.fleet_integration.settings.apmAgent.description": "Configurez l'instrumentation pour les applications {title}.", + "xpack.apm.fleet_integration.settings.disabledLabel": "Désactivé", + "xpack.apm.fleet_integration.settings.enabledLabel": "Activé", + "xpack.apm.fleet_integration.settings.optionalLabel": "Facultatif", + "xpack.apm.fleet_integration.settings.requiredFieldLabel": "Champ requis", + "xpack.apm.fleet_integration.settings.requiredLabel": "Requis", + "xpack.apm.fleet_integration.settings.rum.enableRumDescription": "Activer le monitoring des utilisateurs réels (RUM)", + "xpack.apm.fleet_integration.settings.rum.enableRumTitle": "Activer RUM", + "xpack.apm.fleet_integration.settings.rum.rumAllowHeaderDescription": "Configurer l'authentification pour l'agent", + "xpack.apm.fleet_integration.settings.rum.rumAllowHeaderHelpText": "En-têtes Origin autorisés pouvant être envoyés par les agents utilisateurs.", + "xpack.apm.fleet_integration.settings.rum.rumAllowHeaderLabel": "En-têtes Origin autorisés", + "xpack.apm.fleet_integration.settings.rum.rumAllowHeaderTitle": "En-têtes personnalisés", + "xpack.apm.fleet_integration.settings.rum.rumAllowOriginsHelpText": "Access-Control-Allow-Headers pris en charge en plus de \"Content-Type\", \"Content-Encoding\" et \"Accept\".", + "xpack.apm.fleet_integration.settings.rum.rumAllowOriginsLabel": "Access-Control-Allow-Headers", + "xpack.apm.fleet_integration.settings.rum.rumLibraryPatternHelpText": "Identifiez les cadres de la bibliothèque en faisant correspondre le file_name et le abs_path du cadre de la trace de pile avec ce regexp.", + "xpack.apm.fleet_integration.settings.rum.rumLibraryPatternLabel": "Modèle du cadre de la bibliothèque", + "xpack.apm.fleet_integration.settings.rum.rumResponseHeadersHelpText": "Ajouté aux réponses RUM, par ex. à des fins de conformité à la politique de sécurité.", + "xpack.apm.fleet_integration.settings.rum.rumResponseHeadersLabel": "En-têtes de réponse HTTP personnalisés", + "xpack.apm.fleet_integration.settings.rum.settings.subtitle": "Gérez la configuration de l'agent RUM JS.", + "xpack.apm.fleet_integration.settings.rum.settings.title": "Real User Monitoring (monitoring des utilisateurs réels)", + "xpack.apm.fleet_integration.settings.selectOrCreateOptions": "Sélectionner ou créer des options", + "xpack.apm.fleet_integration.settings.tls.settings.subtitle": "Paramètres pour la certification TLS.", + "xpack.apm.fleet_integration.settings.tls.settings.title": "Paramètres TLS", + "xpack.apm.fleet_integration.settings.tls.tlsCertificateLabel": "Chemin d'accès au certificat du serveur", + "xpack.apm.fleet_integration.settings.tls.tlsCertificateTitle": "Certificat TLS", + "xpack.apm.fleet_integration.settings.tls.tlsCipherSuitesHelpText": "Ne peut pas être configuré pour TLS 1.3.", + "xpack.apm.fleet_integration.settings.tls.tlsCipherSuitesLabel": "Suites de chiffrement pour les connexions TLS", + "xpack.apm.fleet_integration.settings.tls.tlsCurveTypesLabel": "Types de courbes pour les suites de chiffrement ECDHE", + "xpack.apm.fleet_integration.settings.tls.tlsEnabledTitle": "Activer TLS", + "xpack.apm.fleet_integration.settings.tls.tlsKeyLabel": "Chemin d'accès à la clé de certificat du serveur", + "xpack.apm.fleet_integration.settings.tls.tlsSupportedProtocolsLabel": "Versions de protocoles prises en charge", + "xpack.apm.fleetIntegration.assets.description": "Consulter les traces de l'application et les cartes de service dans APM", + "xpack.apm.fleetIntegration.assets.name": "Services", + "xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentButtonText": "Installer l'agent APM", + "xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentDescription": "Une fois l'agent lancé, vous pouvez installer des agents APM sur vos hôtes pour collecter des données depuis vos applications et services.", + "xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentTitle": "Installer l'agent APM", + "xpack.apm.formatters.hoursTimeUnitLabel": "h", + "xpack.apm.formatters.microsTimeUnitLabel": "μs", + "xpack.apm.formatters.millisTimeUnitLabel": "ms", + "xpack.apm.formatters.minutesTimeUnitLabel": "min", + "xpack.apm.formatters.secondsTimeUnitLabel": "s", + "xpack.apm.header.badge.readOnly.text": "Lecture seule", + "xpack.apm.header.badge.readOnly.tooltip": "Enregistrement impossible", + "xpack.apm.helpMenu.upgradeAssistantLink": "Assistant de mise à niveau", + "xpack.apm.helpPopover.ariaLabel": "Aide", + "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": "Nombre d'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", + "xpack.apm.home.serviceLogsTabLabel": "Logs", + "xpack.apm.home.serviceMapTabLabel": "Carte des services", + "xpack.apm.instancesLatencyDistributionChartLegend": "Instances", + "xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "Période précédente", + "xpack.apm.instancesLatencyDistributionChartTitle": "Distribution de la latence des instances", + "xpack.apm.instancesLatencyDistributionChartTooltipClickToFilterDescription": "Cliquer pour filtrer par instance", + "xpack.apm.instancesLatencyDistributionChartTooltipInstancesTitle": "{instancesCount} {instancesCount, plural, one {instance} other {instances}}", + "xpack.apm.instancesLatencyDistributionChartTooltipLatencyLabel": "Latence", + "xpack.apm.instancesLatencyDistributionChartTooltipThroughputLabel": "Rendement", + "xpack.apm.invalidLicense.licenseManagementLink": "Gérer votre licence", + "xpack.apm.invalidLicense.message": "L'interface utilisateur d'APM n'est pas disponible car votre licence actuelle a expiré ou n'est plus valide.", + "xpack.apm.invalidLicense.title": "Licence non valide", + "xpack.apm.jvmsTable.cpuColumnLabel": "Moy. CPU", + "xpack.apm.jvmsTable.explainServiceNodeNameMissing": "Nous n'avons pas pu déterminer à quelles JVM ces indicateurs correspondent. Cela provient probablement du fait que vous exécutez une version du serveur APM antérieure à 7.5. La mise à niveau du serveur APM vers la version 7.5 ou supérieure devrait résoudre le problème.", + "xpack.apm.jvmsTable.heapMemoryColumnLabel": "Moy. segment de mémoire", + "xpack.apm.jvmsTable.nameColumnLabel": "Nom", + "xpack.apm.jvmsTable.nameExplanation": "Par défaut, le nom de la JVM est l'ID du conteneur (le cas échéant) ou le nom d'hôte. Vous pouvez néanmoins le configurer manuellement via la configuration \"service_node_name\" de l'agent.", + "xpack.apm.jvmsTable.noJvmsLabel": "Aucune JVM n'a été trouvée", + "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.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.betaBadge": "Version bêta", + "xpack.apm.license.betaTooltipMessage": "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.license.button": "Commencer l'essai", + "xpack.apm.license.title": "Commencer un essai gratuit de 30 jours", + "xpack.apm.localFilters.titles.browser": "Navigateur", + "xpack.apm.localFilters.titles.device": "Appareil", + "xpack.apm.localFilters.titles.location": "Lieu", + "xpack.apm.localFilters.titles.os": "Système d'exploitation", + "xpack.apm.localFilters.titles.serviceName": "Nom de service", + "xpack.apm.localFilters.titles.transactionUrl": "URL", + "xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning :", + "xpack.apm.metrics.transactionChart.machineLearningTooltip": "Le flux affiche les limites attendues de la latence moyenne. Une annotation verticale rouge signale des anomalies avec un score d'anomalie de 75 ou plus.", + "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "Les résultats de Machine Learning sont masqués lorsque la barre de recherche est utilisée comme filtre", + "xpack.apm.metrics.transactionChart.viewJob": "Afficher la tâche", + "xpack.apm.navigation.serviceMapTitle": "Carte des services", + "xpack.apm.navigation.servicesTitle": "Services", + "xpack.apm.navigation.tracesTitle": "Traces", + "xpack.apm.notAvailableLabel": "N/A", + "xpack.apm.percentOfParent": "({value} de {parentType, select, transaction { transaction } trace {trace} })", + "xpack.apm.profiling.collapseSimilarFrames": "Réduire les éléments similaires", + "xpack.apm.profiling.highlightFrames": "Rechercher", + "xpack.apm.profiling.table.name": "Nom", + "xpack.apm.profiling.table.value": "Auto", + "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "Pas de données disponibles", + "xpack.apm.propertiesTable.agentFeature.noResultFound": "Pas de résultats pour \"{value}\".", + "xpack.apm.propertiesTable.tabs.exceptionStacktraceLabel": "Trace de pile d'exception", + "xpack.apm.propertiesTable.tabs.logs.serviceName": "Nom de service", + "xpack.apm.propertiesTable.tabs.logsLabel": "Logs", + "xpack.apm.propertiesTable.tabs.logStacktraceLabel": "Trace de pile des logs", + "xpack.apm.propertiesTable.tabs.metadataLabel": "Métadonnées", + "xpack.apm.propertiesTable.tabs.timelineLabel": "Chronologie", + "xpack.apm.searchInput.filter": "Filtrer…", + "xpack.apm.selectPlaceholder": "Sélectionner une option :", + "xpack.apm.serviceDependencies.breakdownChartTitle": "Temps consacré par dépendance", + "xpack.apm.serviceDetails.dependenciesTabLabel": "Dépendances", + "xpack.apm.serviceDetails.errorsTabLabel": "Erreurs", + "xpack.apm.serviceDetails.metrics.cpuUsageChartTitle": "Utilisation CPU", + "xpack.apm.serviceDetails.metrics.errorOccurrencesChart.title": "Occurrences d'erreurs", + "xpack.apm.serviceDetails.metrics.errorsList.title": "Erreurs", + "xpack.apm.serviceDetails.metrics.memoryUsageChartTitle": "Utilisation mémoire système", + "xpack.apm.serviceDetails.metricsTabLabel": "Indicateurs", + "xpack.apm.serviceDetails.nodesTabLabel": "JVM", + "xpack.apm.serviceDetails.overviewTabLabel": "Aperçu", + "xpack.apm.serviceDetails.profilingTabExperimentalDescription": "Le profilage est à un stade hautement expérimental, dédié uniquement à une utilisation interne.", + "xpack.apm.serviceDetails.profilingTabExperimentalLabel": "Expérimental", + "xpack.apm.serviceDetails.profilingTabLabel": "Profilage", + "xpack.apm.serviceDetails.transactionsTabLabel": "Transactions", + "xpack.apm.serviceHealthStatus.critical": "Critique", + "xpack.apm.serviceHealthStatus.healthy": "Intègre", + "xpack.apm.serviceHealthStatus.unknown": "Inconnu", + "xpack.apm.serviceHealthStatus.warning": "Avertissement", + "xpack.apm.serviceIcons.cloud": "Cloud", + "xpack.apm.serviceIcons.container": "Conteneur", + "xpack.apm.serviceIcons.service": "Service", + "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.machineTypesLabel": "{machineTypes, plural, =0{Type de machine} one {Type de machine} other {Types de machines}} ", + "xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "ID projet", + "xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "Fournisseur cloud", + "xpack.apm.serviceIcons.serviceDetails.container.containerizedLabel": "Conteneurisé", + "xpack.apm.serviceIcons.serviceDetails.container.noLabel": "Non", + "xpack.apm.serviceIcons.serviceDetails.container.orchestrationLabel": "Orchestration", + "xpack.apm.serviceIcons.serviceDetails.container.osLabel": "Système d'exploitation", + "xpack.apm.serviceIcons.serviceDetails.container.totalNumberInstancesLabel": "Nombre total d'instances", + "xpack.apm.serviceIcons.serviceDetails.container.yesLabel": "Oui", + "xpack.apm.serviceIcons.serviceDetails.service.agentLabel": "Nom et version de l'agent", + "xpack.apm.serviceIcons.serviceDetails.service.frameworkLabel": "Nom du framework", + "xpack.apm.serviceIcons.serviceDetails.service.runtimeLabel": "Nom et version de l'exécution", + "xpack.apm.serviceIcons.serviceDetails.service.versionLabel": "Version du service", + "xpack.apm.serviceLogs.noInfrastructureMessage": "Il n'y a aucun message log à afficher.", + "xpack.apm.serviceMap.anomalyDetectionPopoverDisabled": "Affichez les indicateurs d'intégrité du service en activant la détection des anomalies dans les paramètres APM.", + "xpack.apm.serviceMap.anomalyDetectionPopoverLink": "Afficher les anomalies", + "xpack.apm.serviceMap.anomalyDetectionPopoverNoData": "Nous n'avons pas trouvé de score d'anomalie dans la plage temporelle sélectionnée. Consultez les détails dans l'explorateur d'anomalies.", + "xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric": "Score (max.)", + "xpack.apm.serviceMap.anomalyDetectionPopoverTitle": "Détection des anomalies", + "xpack.apm.serviceMap.anomalyDetectionPopoverTooltip": "Les indicateurs d'intégrité du service sont soutenus par la fonctionnalité de détection des anomalies dans le Machine Learning", + "xpack.apm.serviceMap.avgCpuUsagePopoverStat": "Utilisation CPU (moy.)", + "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "Utilisation de la mémoire (moy.)", + "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "Rendement (moy.)", + "xpack.apm.serviceMap.avgTransDurationPopoverStat": "Latence (moy.)", + "xpack.apm.serviceMap.center": "Centre", + "xpack.apm.serviceMap.download": "Télécharger", + "xpack.apm.serviceMap.emptyBanner.docsLink": "En savoir plus dans la documentation", + "xpack.apm.serviceMap.emptyBanner.message": "Nous démapperons les services connectés et les requêtes externes si nous parvenons à les détecter. Assurez-vous d'exécuter la dernière version de l'agent APM.", + "xpack.apm.serviceMap.emptyBanner.title": "Il semblerait qu'il n'y ait qu'un seul service.", + "xpack.apm.serviceMap.errorRatePopoverStat": "Taux de transactions ayant échoué (moy.)", + "xpack.apm.serviceMap.focusMapButtonText": "Centrer la carte", + "xpack.apm.serviceMap.invalidLicenseMessage": "Pour accéder aux cartes de service, vous devez disposer d'une licence Elastic Platinum. Elle vous permettra de visualiser l'intégralité de la suite d'applications ainsi que vos données APM.", + "xpack.apm.serviceMap.noServicesPromptDescription": "Nous ne parvenons pas à trouver des services à mapper dans la plage temporelle et l'environnement actuellement sélectionnés. Veuillez essayer une autre plage ou vérifier l'environnement sélectionné. Si vous ne disposez d'aucun service, utilisez nos instructions de configuration pour vous aider à vous lancer.", + "xpack.apm.serviceMap.noServicesPromptTitle": "Aucun service disponible", + "xpack.apm.serviceMap.popover.noDataText": "Aucune donnée pour l'environnement sélectionné. Essayez de passer à un autre environnement.", + "xpack.apm.serviceMap.resourceCountLabel": "{count} ressources", + "xpack.apm.serviceMap.serviceDetailsButtonText": "Détails du service", + "xpack.apm.serviceMap.subtypePopoverStat": "Sous-type", + "xpack.apm.serviceMap.timeoutPrompt.docsLink": "En savoir plus sur les paramètres APM dans la documentation", + "xpack.apm.serviceMap.timeoutPromptDescription": "Délai expiré lors de la récupération des données pour la carte de services. Limitez la portée en sélectionnant une plage temporelle plus restreinte, ou utilisez le paramètre de configuration \"{configName}\" avec une valeur réduite.", + "xpack.apm.serviceMap.timeoutPromptTitle": "Expiration de la carte de services", + "xpack.apm.serviceMap.typePopoverStat": "Type", + "xpack.apm.serviceMap.viewFullMap": "Afficher la carte de services entière", + "xpack.apm.serviceMap.zoomIn": "Zoom avant", + "xpack.apm.serviceMap.zoomOut": "Zoom arrière", + "xpack.apm.serviceNodeMetrics.containerId": "ID conteneur", + "xpack.apm.serviceNodeMetrics.host": "Hôte", + "xpack.apm.serviceNodeMetrics.serviceName": "Nom de service", + "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningDocumentationLink": "documentation du serveur APM", + "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningText": "Nous n'avons pas pu déterminer à quelles JVM ces indicateurs correspondent. Cela provient probablement du fait que vous exécutez une version du serveur APM antérieure à 7.5. La mise à niveau du serveur APM vers la version 7.5 ou supérieure devrait résoudre le problème. Pour plus d'informations sur la mise à niveau, consultez {link}. Vous pouvez également utiliser la barre de recherche de Kibana pour filtrer par nom d'hôte, par ID de conteneur ou en fonction d'autres champs.", + "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "Impossible d'identifier les JVM", + "xpack.apm.serviceNodeNameMissing": "(vide)", + "xpack.apm.serviceOverview.dependenciesTableTabLink": "Afficher les dépendances", + "xpack.apm.serviceOverview.dependenciesTableTitle": "Services en aval et back-ends", + "xpack.apm.serviceOverview.errorsTableColumnLastSeen": "Vu en dernier", + "xpack.apm.serviceOverview.errorsTableColumnName": "Nom", + "xpack.apm.serviceOverview.errorsTableColumnOccurrences": "Occurrences", + "xpack.apm.serviceOverview.errorsTableLinkText": "Afficher les erreurs", + "xpack.apm.serviceOverview.errorsTableTitle": "Erreurs", + "xpack.apm.serviceOverview.instancesTable.actionMenus.container.subtitle": "Affichez les logs et les indicateurs de ce conteneur pour plus de détails.", + "xpack.apm.serviceOverview.instancesTable.actionMenus.container.title": "Détails du conteneur", + "xpack.apm.serviceOverview.instancesTable.actionMenus.containerLogs": "Logs du conteneur", + "xpack.apm.serviceOverview.instancesTable.actionMenus.containerMetrics": "Indicateurs du conteneur", + "xpack.apm.serviceOverview.instancesTable.actionMenus.filterByInstance": "Filtrer l'aperçu par instance", + "xpack.apm.serviceOverview.instancesTable.actionMenus.metrics": "Indicateurs", + "xpack.apm.serviceOverview.instancesTable.actionMenus.pod.subtitle": "Affichez les logs et indicateurs de ce pod pour plus de détails.", + "xpack.apm.serviceOverview.instancesTable.actionMenus.pod.title": "Détails du pod", + "xpack.apm.serviceOverview.instancesTable.actionMenus.podLogs": "Logs du pod", + "xpack.apm.serviceOverview.instancesTable.actionMenus.podMetrics": "Indicateurs du pod", + "xpack.apm.serviceOverview.instancesTableColumnCpuUsage": "Utilisation CPU (moy.)", + "xpack.apm.serviceOverview.instancesTableColumnErrorRate": "Taux de transactions ayant échoué", + "xpack.apm.serviceOverview.instancesTableColumnMemoryUsage": "Utilisation de la mémoire (moy.)", + "xpack.apm.serviceOverview.instancesTableColumnNodeName": "Nom du nœud", + "xpack.apm.serviceOverview.instancesTableColumnThroughput": "Rendement", + "xpack.apm.serviceOverview.instancesTableTitle": "Instances", + "xpack.apm.serviceOverview.instanceTable.details.cloudTitle": "Cloud", + "xpack.apm.serviceOverview.instanceTable.details.containerTitle": "Conteneur", + "xpack.apm.serviceOverview.instanceTable.details.serviceTitle": "Service", + "xpack.apm.serviceOverview.latencyChartTitle": "Latence", + "xpack.apm.serviceOverview.latencyChartTitle.prepend": "Indicateur", + "xpack.apm.serviceOverview.latencyChartTitle.previousPeriodLabel": "Période précédente", + "xpack.apm.serviceOverview.latencyColumnAvgLabel": "Latence (moy.)", + "xpack.apm.serviceOverview.latencyColumnDefaultLabel": "Latence", + "xpack.apm.serviceOverview.latencyColumnP95Label": "Latence (95e)", + "xpack.apm.serviceOverview.latencyColumnP99Label": "Latence (99e)", + "xpack.apm.serviceOverview.throughtputChart.previousPeriodLabel": "Période précédente", + "xpack.apm.serviceOverview.throughtputChartTitle": "Rendement", + "xpack.apm.serviceOverview.tpmHelp": "Le rendement est mesuré en transactions par minute (tpm)", + "xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "Taux de transactions ayant échoué", + "xpack.apm.serviceOverview.transactionsTableColumnImpact": "Impact", + "xpack.apm.serviceOverview.transactionsTableColumnName": "Nom", + "xpack.apm.serviceOverview.transactionsTableColumnThroughput": "Rendement", + "xpack.apm.serviceProfiling.valueTypeLabel.allocObjects": "Objets alloués", + "xpack.apm.serviceProfiling.valueTypeLabel.allocSpace": "Espace alloué", + "xpack.apm.serviceProfiling.valueTypeLabel.cpuTime": "Sur CPU", + "xpack.apm.serviceProfiling.valueTypeLabel.inuseObjects": "Objets utilisés", + "xpack.apm.serviceProfiling.valueTypeLabel.inuseSpace": "Espace utilisé", + "xpack.apm.serviceProfiling.valueTypeLabel.samples": "Échantillons", + "xpack.apm.serviceProfiling.valueTypeLabel.unknown": "Autre", + "xpack.apm.serviceProfiling.valueTypeLabel.wallTime": "Mur", + "xpack.apm.servicesTable.environmentColumnLabel": "Environnement", + "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 environnement} other {# environnements}}", + "xpack.apm.servicesTable.healthColumnLabel": "Intégrité", + "xpack.apm.servicesTable.latencyAvgColumnLabel": "Latence (moy.)", + "xpack.apm.servicesTable.metricsExplanationLabel": "Que sont ces indicateurs ?", + "xpack.apm.servicesTable.nameColumnLabel": "Nom", + "xpack.apm.servicesTable.notFoundLabel": "Aucun service trouvé", + "xpack.apm.servicesTable.throughputColumnLabel": "Rendement", + "xpack.apm.servicesTable.tooltip.metricsExplanation": "Les indicateurs des services sont agrégés selon le type de transaction \"request\", \"page-load\", ou en fonction du type de transaction de niveau supérieur disponible.", + "xpack.apm.servicesTable.transactionColumnLabel": "Type de transaction", + "xpack.apm.servicesTable.transactionErrorRate": "Taux de transactions ayant échoué", + "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.anomaly_detection.legacy_jobs.button": "Consulter les tâches", + "xpack.apm.settings.anomalyDetection": "Détection des anomalies", + "xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText": "Annuler", + "xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText": "Créer des tâches", + "xpack.apm.settings.anomalyDetection.addEnvironments.descriptionText": "Sélectionnez les environnements de services dans lesquels vous souhaitez activer la détection des anomalies. Les anomalies seront mises en évidence pour tous les services et types de transactions dans les environnements sélectionnés.", + "xpack.apm.settings.anomalyDetection.addEnvironments.selectorLabel": "Environnements", + "xpack.apm.settings.anomalyDetection.addEnvironments.selectorPlaceholder": "Sélectionner ou ajouter des environnements", + "xpack.apm.settings.anomalyDetection.addEnvironments.titleText": "Sélectionner des environnements", + "xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel": "Action", + "xpack.apm.settings.anomalyDetection.jobList.addEnvironments": "Créer une tâche de ML", + "xpack.apm.settings.anomalyDetection.jobList.emptyListText": "Aucune tâche de détection des anomalies.", + "xpack.apm.settings.anomalyDetection.jobList.environmentColumnLabel": "Environnement", + "xpack.apm.settings.anomalyDetection.jobList.environments": "Environnements", + "xpack.apm.settings.anomalyDetection.jobList.failedFetchText": "Impossible de récupérer les tâches de détection des anomalies.", + "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": "Afficher une tâche dans ML", + "xpack.apm.settings.apmIndices.applyButton": "Appliquer les modifications", + "xpack.apm.settings.apmIndices.applyChanges.failed.text": "Un problème est survenu lors de l'application des index. Erreur : {errorMessage}", + "xpack.apm.settings.apmIndices.applyChanges.failed.title": "Impossible d'appliquer les index.", + "xpack.apm.settings.apmIndices.applyChanges.succeeded.text": "Les modifications apportées aux index ont été correctement appliquées. Ces modifications sont immédiatement appliquées dans l'interface utilisateur APM", + "xpack.apm.settings.apmIndices.applyChanges.succeeded.title": "Index appliqués", + "xpack.apm.settings.apmIndices.cancelButton": "Annuler", + "xpack.apm.settings.apmIndices.description": "L'interface utilisateur APM utilise des modèles d'indexation pour interroger vos index APM. Si vous avez personnalisé les noms des index sur lesquels le serveur APM écrit les événements, vous devrez peut-être mettre à jour ces modèles pour que l'interface utilisateur APM fonctionne. Dans ce cas précis, les paramètres prévalent sur ceux définis dans kibana.yml.", + "xpack.apm.settings.apmIndices.errorIndicesLabel": "Index des erreurs", + "xpack.apm.settings.apmIndices.helpText": "Remplace {configurationName} : {defaultValue}", + "xpack.apm.settings.apmIndices.metricsIndicesLabel": "Index des indicateurs", + "xpack.apm.settings.apmIndices.noPermissionTooltipLabel": "Votre rôle d'utilisateur ne dispose pas d'autorisations pour changer les index APM", + "xpack.apm.settings.apmIndices.onboardingIndicesLabel": "Intégration des index", + "xpack.apm.settings.apmIndices.sourcemapIndicesLabel": "Index des source maps", + "xpack.apm.settings.apmIndices.spanIndicesLabel": "Index des intervalles", + "xpack.apm.settings.apmIndices.title": "Index", + "xpack.apm.settings.apmIndices.transactionIndicesLabel": "Index des transactions", + "xpack.apm.settings.createApmPackagePolicy.errorToast.title": "Impossible de créer une politique de package APM sur la politique d'agent cloud", + "xpack.apm.settings.customizeApp": "Personnaliser l'application", + "xpack.apm.settings.indices": "Index", + "xpack.apm.settings.schema": "Schéma", + "xpack.apm.settings.schema.confirm.apmServerSettingsCloudLinkText": "Accéder aux paramètres du serveur APM dans Elastic Cloud", + "xpack.apm.settings.schema.confirm.cancelText": "Annuler", + "xpack.apm.settings.schema.confirm.checkboxLabel": "Je confirme vouloir basculer vers les flux de données", + "xpack.apm.settings.schema.confirm.irreversibleWarning.message": "Il est possible que cela affecte temporairement votre collecte de données APM pendant la progression de la migration. Le processus de migration ne devrait prendre que quelques minutes.", + "xpack.apm.settings.schema.confirm.irreversibleWarning.title": "Le basculement vers les flux de données est une action irréversible", + "xpack.apm.settings.schema.confirm.switchButtonText": "Basculer vers les flux de données", + "xpack.apm.settings.schema.confirm.title": "Veuillez confirmer votre choix", + "xpack.apm.settings.schema.confirm.unsupportedConfigs.descriptionText": "Les paramètres utilisateur apm-server.yml personnalisés compatibles seront déplacés vers le serveur Fleet à votre place. Nous vous informerons des paramètres incompatibles avant de les supprimer.", + "xpack.apm.settings.schema.confirm.unsupportedConfigs.title": "Les paramètres utilisateur apm-server.yml suivants sont incompatibles et seront supprimés", + "xpack.apm.settings.schema.descriptionText.irreversibleEmphasisText": "irréversible", + "xpack.apm.settings.schema.descriptionText.superuserEmphasisText": "superutilisateur", + "xpack.apm.settings.schema.disabledReason": "L'option Basculer vers les flux de données est indisponible : {reasons}", + "xpack.apm.settings.schema.disabledReason.cloudApmMigrationEnabled": "La migration vers le cloud n'est pas activée", + "xpack.apm.settings.schema.disabledReason.hasCloudAgentPolicy": "La politique d'agent cloud n'existe pas", + "xpack.apm.settings.schema.disabledReason.hasRequiredRole": "L'utilisateur ne dispose pas du rôle de superutilisateur", + "xpack.apm.settings.schema.migrate.classicIndices.currentSetup": "Configuration actuelle", + "xpack.apm.settings.schema.migrate.classicIndices.description": "Vous utilisez actuellement des index APM classiques pour vos données. Ce schéma de données est sur le point de disparaître et sera remplacé par des flux de données dans la version 8.0 d'Elastic Stack.", + "xpack.apm.settings.schema.migrate.classicIndices.title": "Index APM classiques", + "xpack.apm.settings.schema.migrate.dataStreams.buttonText": "Basculer vers les flux de données", + "xpack.apm.settings.schema.migrate.dataStreams.description": "À partir de maintenant, toutes les nouvelles données ingérées seront stockées dans les flux de données. Les données précédemment ingérées restent dans les index APM classiques. Les applications APM et UX continueront à prendre en charge les deux types d'index.", + "xpack.apm.settings.schema.migrate.dataStreams.title": "Flux de données", + "xpack.apm.settings.schema.migrationInProgressPanelDescription": "Nous créons actuellement une instance de serveur Fleet pour contenir le nouveau serveur APM pendant la fermeture de l'ancienne instance du serveur APM. Dans quelques minutes, vous devriez voir vos données réintégrer l'application.", + "xpack.apm.settings.schema.migrationInProgressPanelTitle": "Basculement vers les flux de données…", + "xpack.apm.settings.schema.success.description": "Votre intégration APM est à présent configurée et prête à recevoir des données de vos agents actuellement instrumentés. N'hésitez pas à consulter les politiques appliquées à votre intégration.", + "xpack.apm.settings.schema.success.returnText": "ou revenez simplement à l'{serviceInventoryLink}.", + "xpack.apm.settings.schema.success.returnText.serviceInventoryLink": "Inventaire de service", + "xpack.apm.settings.schema.success.title": "Les flux de données ont été configurés avec succès !", + "xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText": "Afficher l'intégration APM dans Fleet", + "xpack.apm.settings.title": "Paramètres", + "xpack.apm.settings.unsupportedConfigs.errorToast.title": "Impossible de récupérer les paramètres du serveur APM", + "xpack.apm.settingsLinkLabel": "Paramètres", + "xpack.apm.setupInstructionsButtonLabel": "Instructions de configuration", + "xpack.apm.stacktraceTab.causedByFramesToogleButtonLabel": "Provoqué par", + "xpack.apm.stacktraceTab.libraryFramesToogleButtonLabel": "{count, plural, one {# cadre de bibliothèque} other {# cadres de bibliothèque}}", + "xpack.apm.stacktraceTab.localVariablesToogleButtonLabel": "Variables locales", + "xpack.apm.stacktraceTab.noStacktraceAvailableLabel": "Aucune trace de pile disponible.", + "xpack.apm.timeComparison.label": "Comparaison", + "xpack.apm.timeComparison.select.dayBefore": "Jour précédent", + "xpack.apm.timeComparison.select.weekBefore": "Semaine précédente", + "xpack.apm.toggleHeight.showLessButtonLabel": "Afficher moins de lignes", + "xpack.apm.toggleHeight.showMoreButtonLabel": "Afficher plus de lignes", + "xpack.apm.tracesTable.avgResponseTimeColumnLabel": "Latence (moy.)", + "xpack.apm.tracesTable.impactColumnDescription": "Points de terminaison les plus utilisés et les plus lents de votre service. C'est le résultat de la multiplication de la latence et du rendement", + "xpack.apm.tracesTable.impactColumnLabel": "Impact", + "xpack.apm.tracesTable.nameColumnLabel": "Nom", + "xpack.apm.tracesTable.notFoundLabel": "Aucune trace trouvée pour cette recherche", + "xpack.apm.tracesTable.originatingServiceColumnLabel": "Service d'origine", + "xpack.apm.tracesTable.tracesPerMinuteColumnLabel": "Traces par minute", + "xpack.apm.transactionActionMenu.actionsButtonLabel": "Investiguer", + "xpack.apm.transactionActionMenu.container.subtitle": "Affichez les logs et les indicateurs de ce conteneur pour plus de détails.", + "xpack.apm.transactionActionMenu.container.title": "Détails du conteneur", + "xpack.apm.transactionActionMenu.customLink.section": "Liens personnalisés", + "xpack.apm.transactionActionMenu.customLink.showAll": "Afficher tout", + "xpack.apm.transactionActionMenu.customLink.showFewer": "Afficher moins", + "xpack.apm.transactionActionMenu.customLink.subtitle": "Les liens s'ouvriront dans une nouvelle fenêtre.", + "xpack.apm.transactionActionMenu.host.subtitle": "Affichez les logs et les indicateurs de l'hôte pour plus de détails.", + "xpack.apm.transactionActionMenu.host.title": "Détails de l'hôte", + "xpack.apm.transactionActionMenu.pod.subtitle": "Affichez les logs et indicateurs de ce pod pour plus de détails.", + "xpack.apm.transactionActionMenu.pod.title": "Détails du pod", + "xpack.apm.transactionActionMenu.showContainerLogsLinkLabel": "Logs du conteneur", + "xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel": "Indicateurs du conteneur", + "xpack.apm.transactionActionMenu.showHostLogsLinkLabel": "Logs de l'hôte", + "xpack.apm.transactionActionMenu.showHostMetricsLinkLabel": "Indicateurs de l'hôte", + "xpack.apm.transactionActionMenu.showPodLogsLinkLabel": "Logs du pod", + "xpack.apm.transactionActionMenu.showPodMetricsLinkLabel": "Indicateurs du pod", + "xpack.apm.transactionActionMenu.showTraceLogsLinkLabel": "Logs de trace", + "xpack.apm.transactionActionMenu.status.subtitle": "Affichez le statut pour plus de détails.", + "xpack.apm.transactionActionMenu.status.title": "Détails de statut", + "xpack.apm.transactionActionMenu.trace.subtitle": "Afficher les logs de trace pour plus de détails.", + "xpack.apm.transactionActionMenu.trace.title": "Détails de la trace", + "xpack.apm.transactionActionMenu.viewInUptime": "Statut", + "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "Afficher l'exemple de document", + "xpack.apm.transactionBreakdown.chartTitle": "Temps consacré par type d'intervalle", + "xpack.apm.transactionDetails.clearSelectionAriaLabel": "Effacer la sélection", + "xpack.apm.transactionDetails.distribution.panelTitle": "Distribution de la latence", + "xpack.apm.transactionDetails.emptySelectionText": "Glisser et déposer pour sélectionner une plage", + "xpack.apm.transactionDetails.errorCount": "{errorCount, number} {errorCount, plural, one {erreur} other {erreurs}}", + "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "Le parent de la trace n'a pas pu être trouvé", + "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "Le % de {parentType, select, transaction {transaction} trace {trace} } dépasse 100 %, car {childType, select, span {cet intervalle} transaction {cette transaction} } prend plus de temps que la transaction racine.", + "xpack.apm.transactionDetails.requestMethodLabel": "Méthode de requête", + "xpack.apm.transactionDetails.resultLabel": "Résultat", + "xpack.apm.transactionDetails.serviceLabel": "Service", + "xpack.apm.transactionDetails.servicesTitle": "Services", + "xpack.apm.transactionDetails.spanFlyout.compositeExampleWarning": "Il s'agit d'un exemple de document pour un groupe d'intervalles similaires consécutifs", + "xpack.apm.transactionDetails.spanFlyout.databaseStatementTitle": "Déclaration de la base de données", + "xpack.apm.transactionDetails.spanFlyout.nameLabel": "Nom", + "xpack.apm.transactionDetails.spanFlyout.spanAction": "Action", + "xpack.apm.transactionDetails.spanFlyout.spanDetailsTitle": "Détails de l'intervalle", + "xpack.apm.transactionDetails.spanFlyout.spanSubtype": "Sous-type", + "xpack.apm.transactionDetails.spanFlyout.spanType": "Type", + "xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "Temporisation de la navigation", + "xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "Trace de pile", + "xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "Afficher l'intervalle dans Discover", + "xpack.apm.transactionDetails.spanTypeLegendTitle": "Type", + "xpack.apm.transactionDetails.statusCode": "Code du statut", + "xpack.apm.transactionDetails.syncBadgeAsync": "async", + "xpack.apm.transactionDetails.syncBadgeBlocking": "blocage", + "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel": "Corrélations des transactions ayant échoué", + "xpack.apm.transactionDetails.tabs.latencyLabel": "Corrélations de latence", + "xpack.apm.transactionDetails.tabs.traceSamplesLabel": "Échantillons de traces", + "xpack.apm.transactionDetails.traceNotFound": "La trace sélectionnée n'a pas pu être trouvée", + "xpack.apm.transactionDetails.traceSampleTitle": "Échantillon de trace", + "xpack.apm.transactionDetails.transactionLabel": "Transaction", + "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "L'agent APM qui a signalé cette transaction a abandonné {dropped} intervalles ou plus, d'après sa configuration.", + "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "Découvrir plus d'informations sur les intervalles abandonnés.", + "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "Détails de la transaction", + "xpack.apm.transactionDetails.userAgentAndVersionLabel": "Agent utilisateur et version", + "xpack.apm.transactionDetails.viewFullTraceButtonLabel": "Afficher la trace complète", + "xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "Affichage actuel de la trace complète", + "xpack.apm.transactionDistribution.chart.allTransactionsLabel": "Toutes les transactions", + "xpack.apm.transactionDistribution.chart.currentTransactionMarkerLabel": "Échantillon actuel", + "xpack.apm.transactionDistribution.chart.numberOfTransactionsLabel": "Nb de transactions", + "xpack.apm.transactionDistribution.chart.percentileMarkerLabel": "{markerPercentile}e centile", + "xpack.apm.transactionDurationAlert.aggregationType.95th": "95e centile", + "xpack.apm.transactionDurationAlert.aggregationType.99th": "99e centile", + "xpack.apm.transactionDurationAlert.aggregationType.avg": "Moyenne", + "xpack.apm.transactionDurationAlert.name": "Seuil de latence", + "xpack.apm.transactionDurationAlertTrigger.ms": "ms", + "xpack.apm.transactionDurationAlertTrigger.when": "Quand", + "xpack.apm.transactionDurationAnomalyAlert.name": "Anomalie de latence", + "xpack.apm.transactionDurationAnomalyAlertTrigger.anomalySeverity": "Comporte une anomalie avec sévérité", + "xpack.apm.transactionDurationLabel": "Durée", + "xpack.apm.transactionErrorRateAlert.name": "Seuil du taux de transactions ayant échoué", + "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "est supérieur à", + "xpack.apm.transactionRateLabel": "{displayedValue} tpm", + "xpack.apm.transactions.latency.chart.95thPercentileLabel": "95e centile", + "xpack.apm.transactions.latency.chart.99thPercentileLabel": "99e centile", + "xpack.apm.transactions.latency.chart.averageLabel": "Moyenne", + "xpack.apm.tutorial.agent_config.choosePolicy.helper": "Ajoute la configuration de la politique sélectionnée à l'extrait ci-dessous.", + "xpack.apm.tutorial.agent_config.choosePolicyLabel": "Choix de la politique", + "xpack.apm.tutorial.agent_config.defaultStandaloneConfig": "Configuration autonome par défaut", + "xpack.apm.tutorial.agent_config.fleetPoliciesLabel": "Politiques Fleet", + "xpack.apm.tutorial.agent_config.getStartedWithFleet": "Démarrer avec Fleet", + "xpack.apm.tutorial.agent_config.manageFleetPolicies": "Gérer les politiques Fleet", + "xpack.apm.tutorial.apmAgents.statusCheck.btnLabel": "Vérifier le statut de l'agent", + "xpack.apm.tutorial.apmAgents.statusCheck.errorMessage": "Aucune donnée n'a encore été reçue des agents", + "xpack.apm.tutorial.apmAgents.statusCheck.successMessage": "Les données ont été correctement reçues d'un ou de plusieurs agents", + "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.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.manageApmIntegration.button": "Gérer l'intégration APM dans Fleet", + "xpack.apm.tutorial.apmServer.fleet.message": "L'intégration d'APM installe les modèles Elasticsearch et les pipelines de nœuds d'ingestion pour les données APM.", + "xpack.apm.tutorial.apmServer.fleet.title": "Elastic APM (version bêta) est maintenant disponible dans Fleet !", + "xpack.apm.tutorial.apmServer.statusCheck.btnLabel": "Vérifier le statut du serveur APM", + "xpack.apm.tutorial.apmServer.statusCheck.errorMessage": "Aucun serveur APM détecté. Vérifiez qu'il est en cours d'exécution et que vous avez effectué la mise à jour vers la version 7.0 ou supérieure.", + "xpack.apm.tutorial.apmServer.statusCheck.successMessage": "Vous avez correctement configuré le serveur APM", + "xpack.apm.tutorial.apmServer.statusCheck.text": "Vérifiez que le serveur APM est en cours d'exécution avant de commencer à mettre en œuvre les agents APM.", + "xpack.apm.tutorial.apmServer.statusCheck.title": "Statut du serveur APM", + "xpack.apm.tutorial.apmServer.title": "Serveur APM", + "xpack.apm.tutorial.djangoClient.configure.commands.addAgentComment": "Ajouter l'agent aux applications installées", + "xpack.apm.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment": "Pour envoyer les indicateurs de performance, ajoutez notre intergiciel de traçage :", + "xpack.apm.tutorial.djangoClient.configure.commands.allowedCharactersComment": "a-z, A-Z, 0-9, -, _ et espace", + "xpack.apm.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment": "Définir l'URL personnalisée du serveur APM (par défaut : {defaultApmServerUrl})", + "xpack.apm.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment": "Définissez le nom de service obligatoire. Caractères autorisés :", + "xpack.apm.tutorial.djangoClient.configure.commands.setServiceEnvironmentComment": "Définir l'environnement de service", + "xpack.apm.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment": "À utiliser si le serveur APM requiert un token secret", + "xpack.apm.tutorial.djangoClient.configure.textPost": "Consultez la [documentation]({documentationLink}) pour une utilisation avancée.", + "xpack.apm.tutorial.djangoClient.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 \"SERVICE_NAME\".", + "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 Consultez [the documentation]({documentationLink}) pour une utilisation avancée.", + "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.title": "Ajouter l'agent à l'application", + "xpack.apm.tutorial.dotNetClient.download.textPre": "Ajoutez le(s) package(s) 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.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 32 bits ? Consultez la [Download page]({downloadPageLink}).", + "xpack.apm.tutorial.downloadServerTitle": "Vous cherchez les packages 32 bits ? Consultez la [Download page]({downloadPageLink}).", + "xpack.apm.tutorial.editConfig.textPre": "Si vous utilisez une version sécurisée X-Pack d'Elastic Stack, vous devez spécifier les informations d'identification dans le fichier de configuration \"apm-server.yml\".", + "xpack.apm.tutorial.editConfig.title": "Modifier la configuration", + "xpack.apm.tutorial.elasticCloud.textPre": "Pour activer le serveur APM, accédez à [the Elastic Cloud console](https://cloud.elastic.co/deployments/{deploymentId}/edit) et activez APM dans les paramètres de déploiement. Une fois activé, actualisez la page.", + "xpack.apm.tutorial.elasticCloudInstructions.title": "Agents APM", + "xpack.apm.tutorial.flaskClient.configure.commands.allowedCharactersComment": "a-z, A-Z, 0-9, -, _ et espace", + "xpack.apm.tutorial.flaskClient.configure.commands.configureElasticApmComment": "ou configurer l'utilisation d'ELASTIC_APM dans les paramètres de votre application", + "xpack.apm.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment": "initialiser à l'aide des variables d'environnement", + "xpack.apm.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment": "Définir l'URL personnalisée du serveur APM (par défaut : {defaultApmServerUrl})", + "xpack.apm.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment": "Définissez le nom de service obligatoire. Caractères autorisés :", + "xpack.apm.tutorial.flaskClient.configure.commands.setServiceEnvironmentComment": "Définir l'environnement de service", + "xpack.apm.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment": "À utiliser si le serveur APM requiert un token secret", + "xpack.apm.tutorial.flaskClient.configure.textPost": "Consultez la [documentation]({documentationLink}) pour une utilisation avancée.", + "xpack.apm.tutorial.flaskClient.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 \"SERVICE_NAME\".", + "xpack.apm.tutorial.flaskClient.configure.title": "Configurer l'agent", + "xpack.apm.tutorial.flaskClient.install.textPre": "Installez l'agent APM pour Python en tant que dépendance.", + "xpack.apm.tutorial.flaskClient.install.title": "Installer l'agent APM", + "xpack.apm.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "Initialisez à l'aide des variables d'environnement :", + "xpack.apm.tutorial.goClient.configure.commands.setCustomApmServerUrlComment": "Définir l'URL de serveur APM personnalisée (par défaut : {defaultApmServerUrl})", + "xpack.apm.tutorial.goClient.configure.commands.setServiceEnvironment": "Définir l'environnement de service", + "xpack.apm.tutorial.goClient.configure.commands.setServiceNameComment": "Configurez le nom de service. Caractères autorisés : # a-z, A-Z, 0-9, -, _ et espace.", + "xpack.apm.tutorial.goClient.configure.commands.usedExecutableNameComment": "Si ELASTIC_APM_SERVICE_NAME n'est pas spécifié, le nom de l'exécutable sera utilisé.", + "xpack.apm.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment": "À utiliser si le serveur APM requiert un token secret", + "xpack.apm.tutorial.goClient.configure.textPost": "Consultez la [documentation]({documentationLink}) pour une configuration avancée.", + "xpack.apm.tutorial.goClient.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 nom du fichier exécutable, ou de la variable d'environnement \"ELASTIC_APM_SERVICE_NAME\".", + "xpack.apm.tutorial.goClient.configure.title": "Configurer l'agent", + "xpack.apm.tutorial.goClient.install.textPre": "Installez les packages d'agent APM pour Go.", + "xpack.apm.tutorial.goClient.install.title": "Installer l'agent APM", + "xpack.apm.tutorial.goClient.instrument.textPost": "Consultez la [documentation]({documentationLink}) pour obtenir un guide détaillé pour l'instrumentation du code source Go.", + "xpack.apm.tutorial.goClient.instrument.textPre": "Pour instrumenter votre application Go, utilisez l'un des modules d'instrumentation proposés ou directement l'API de traçage.", + "xpack.apm.tutorial.goClient.instrument.title": "Instrumenter votre application", + "xpack.apm.tutorial.introduction": "Collectez les indicateurs et les erreurs de performances approfondies depuis vos applications.", + "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.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.", + "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.setCustomApmServerUrlComment": "Définir l'URL de serveur APM personnalisée (par défaut : {defaultApmServerUrl})", + "xpack.apm.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment": "Définir le nom de service requis (caractères autorisés : a-z, A-Z, 0-9, -, _ et espace)", + "xpack.apm.tutorial.jsClient.installDependency.commands.setServiceEnvironmentComment": "Définir l'environnement de service", + "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.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 \"