diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 78c57ff3bd1285..8c1162954cfacb 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -19,7 +19,7 @@ steps: label: 'Default CI Group' parallelism: 27 agents: - queue: n2-4 + queue: n2-4-spot depends_on: build timeout_in_minutes: 250 key: default-cigroup @@ -31,7 +31,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: n2-4 + queue: n2-4-spot depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -44,7 +44,7 @@ steps: label: 'OSS CI Group' parallelism: 11 agents: - queue: ci-group-4d + queue: n2-4-spot depends_on: build timeout_in_minutes: 120 key: oss-cigroup @@ -56,29 +56,49 @@ 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' agents: - queue: ci-group-4d + queue: n2-4-spot depends_on: build timeout_in_minutes: 120 retry: @@ -89,18 +109,28 @@ 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' agents: - queue: n2-4 + queue: n2-4-spot depends_on: build timeout_in_minutes: 120 retry: @@ -111,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' @@ -138,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' diff --git a/.buildkite/scripts/steps/functional/uptime.sh b/.buildkite/scripts/steps/functional/uptime.sh index 5a59f4dfa48bd7..a1c8c2bf6c85b1 100755 --- a/.buildkite/scripts/steps/functional/uptime.sh +++ b/.buildkite/scripts/steps/functional/uptime.sh @@ -14,4 +14,4 @@ echo "--- Uptime @elastic/synthetics Tests" cd "$XPACK_DIR" checks-reporter-with-killswitch "Uptime @elastic/synthetics Tests" \ - node plugins/uptime/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" + node plugins/uptime/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" ${GREP:+--grep \"${GREP}\"} diff --git a/.eslintignore b/.eslintignore index 7b9b7f77e83792..8f1fcff422d0e0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ **/*.js.snap +__tmp__ /.es /.chromium /build diff --git a/.eslintrc.js b/.eslintrc.js index 6c98a016469f7b..fc6d6201d1fc0f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -104,6 +104,7 @@ const DEV_PACKAGES = [ 'kbn-storybook', 'kbn-telemetry-tools', 'kbn-test', + 'kbn-type-summarizer', ]; /** Directories (at any depth) which include dev-only code. */ @@ -1632,28 +1633,6 @@ module.exports = { }, }, - /** - * Prettier disables all conflicting rules, listing as last override so it takes precedence - */ - { - files: ['**/*'], - rules: { - ...require('eslint-config-prettier').rules, - ...require('eslint-config-prettier/react').rules, - ...require('eslint-config-prettier/@typescript-eslint').rules, - }, - }, - /** - * Enterprise Search Prettier override - * Lints unnecessary backticks - @see https://github.com/prettier/eslint-config-prettier/blob/main/README.md#forbid-unnecessary-backticks - */ - { - files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], - rules: { - quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }], - }, - }, - /** * Platform Security Team overrides */ @@ -1768,5 +1747,34 @@ module.exports = { '@kbn/eslint/no_export_all': 'error', }, }, + + { + files: ['packages/kbn-type-summarizer/**/*.ts'], + rules: { + 'no-bitwise': 'off', + }, + }, + + /** + * Prettier disables all conflicting rules, listing as last override so it takes precedence + */ + { + files: ['**/*'], + rules: { + ...require('eslint-config-prettier').rules, + ...require('eslint-config-prettier/react').rules, + ...require('eslint-config-prettier/@typescript-eslint').rules, + }, + }, + /** + * Enterprise Search Prettier override + * Lints unnecessary backticks - @see https://github.com/prettier/eslint-config-prettier/blob/main/README.md#forbid-unnecessary-backticks + */ + { + files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], + rules: { + quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }], + }, + }, ], }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 63e335067199d0..691daa042bba95 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -416,6 +416,9 @@ x-pack/plugins/security_solution/cypress/upgrade_integration @elastic/security-e x-pack/plugins/security_solution/cypress/README.md @elastic/security-engineering-productivity x-pack/test/security_solution_cypress @elastic/security-engineering-productivity +## Security Solution sub teams - adaptive-workload-protection +x-pack/plugins/session_view @elastic/awp-platform + # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/.github/workflows/add-to-imui-project.yml b/.github/workflows/add-to-imui-project.yml new file mode 100644 index 00000000000000..3cf120b2e81bc5 --- /dev/null +++ b/.github/workflows/add-to-imui-project.yml @@ -0,0 +1,31 @@ +name: Add to Infra Monitoring UI project +on: + issues: + types: + - labeled +jobs: + add_to_project: + runs-on: ubuntu-latest + if: | + contains(github.event.issue.labels.*.name, 'Team:Infra Monitoring UI') || + contains(github.event.issue.labels.*.name, 'Feature:Stack Monitoring') || + contains(github.event.issue.labels.*.name, 'Feature:Logs UI') || + contains(github.event.issue.labels.*.name, 'Feature:Metrics UI') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PN_kwDOAGc3Zs1EEA" + GITHUB_TOKEN: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} diff --git a/.github/workflows/label-qa-fixed-in.yml b/.github/workflows/label-qa-fixed-in.yml index 836aa308e92c7a..99803c2c4e8800 100644 --- a/.github/workflows/label-qa-fixed-in.yml +++ b/.github/workflows/label-qa-fixed-in.yml @@ -19,7 +19,7 @@ jobs: github.event.pull_request.merged_at && contains(github.event.pull_request.labels.*.name, 'Team:Fleet') outputs: - matrix: ${{ steps.issues_to_label.outputs.value }} + issue_ids: ${{ steps.issues_to_label.outputs.value }} label_ids: ${{ steps.label_ids.outputs.value }} steps: - uses: octokit/graphql-action@v2.x @@ -66,22 +66,28 @@ jobs: label_issues: needs: fetch_issues_to_label runs-on: ubuntu-latest - # For each issue closed by the PR run this job + + # For each issue closed by the PR x each label to apply, run this job + if: | + fromJSON(needs.fetch_issues_to_label.outputs.issue_ids).length > 0 && + fromJSON(needs.fetch_issues_to_label.outputs.label_ids).length > 0 strategy: matrix: - issueNodeId: ${{ fromJSON(needs.fetch_issues_to_label.outputs.matrix) }} - name: Label issue ${{ matrix.issueNodeId }} + issueId: ${{ fromJSON(needs.fetch_issues_to_label.outputs.issue_ids) }} + labelId: ${{ fromJSON(needs.fetch_issues_to_label.outputs.label_ids) }} + + name: Label issue ${{ matrix.issueId }} with ${{ matrix.labelId }} steps: - uses: octokit/graphql-action@v2.x id: add_labels_to_closed_issue with: query: | - mutation add_label($issueid: ID!, $labelids:[ID!]!) { - addLabelsToLabelable(input: {labelableId: $issueid, labelIds: $labelids}) { + mutation add_label($issueid: ID!, $labelid:ID!) { + addLabelsToLabelable(input: {labelableId: $issueid, labelIds: [$labelid]}) { clientMutationId } } - issueid: ${{ matrix.issueNodeId }} - labelids: ${{ fromJSON(needs.fetch_issues_to_label.outputs.label_ids) }} + issueid: ${{ matrix.issueId }} + labelid: ${{ matrix.labelId }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 65808dffd801f5..8c381dd1ecdefa 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -18,8 +18,6 @@ jobs: {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, - {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}, - {"label": "Team:Security", "projectNumber": 320, "columnName": "Awaiting triage", "projectScope": "org"}, - {"label": "Team:Operations", "projectNumber": 314, "columnName": "Triage", "projectScope": "org"} + {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"} ] ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} diff --git a/.gitignore b/.gitignore index 7e451584582380..4704247e6f548d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ target *.iml *.log types.eslint.config.js +__tmp__ # Ignore example plugin builds /examples/*/build diff --git a/.i18nrc.json b/.i18nrc.json index 7ec704aab3a7ae..eeb2578ef3472f 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -68,6 +68,7 @@ "usageCollection": "src/plugins/usage_collection", "utils": "packages/kbn-securitysolution-utils/src", "visDefaultEditor": "src/plugins/vis_default_editor", + "visTypeGauge": "src/plugins/vis_types/gauge", "visTypeHeatmap": "src/plugins/vis_types/heatmap", "visTypeMarkdown": "src/plugins/vis_type_markdown", "visTypeMetric": "src/plugins/vis_types/metric", diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 76c516ecc605ae..04f61b7f950648 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -64,4 +64,8 @@ yarn_install( symlink_node_modules = True, quiet = False, frozen_lockfile = False, + environment = { + "SASS_BINARY_SITE": "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-sass", + "RE2_DOWNLOAD_MIRROR": "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2", + } ) 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/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/contributing/development-documentation.asciidoc b/docs/developer/contributing/development-documentation.asciidoc index 801d0527cc2b7d..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 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/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 2dd78be3c1012f..c26a748839daf1 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -297,6 +297,10 @@ It acts as a container for a particular visualization and options tabs. Contains The plugin exposes the static DefaultEditorController class to consume. +|{kib-repo}blob/{branch}/src/plugins/vis_types/gauge[visTypeGauge] +|WARNING: Missing README. + + |{kib-repo}blob/{branch}/src/plugins/vis_types/heatmap[visTypeHeatmap] |WARNING: Missing README. @@ -580,6 +584,10 @@ Kibana. |Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. +|{kib-repo}blob/{branch}/x-pack/plugins/session_view/README.md[sessionView] +|Session View is meant to provide a visualization into what is going on in a particular Linux environment where the agent is running. It looks likes a terminal emulator; however, it is a tool for introspecting process activity and understanding user and service behaviour in your Linux servers and infrastructure. It is a time-ordered series of process executions displayed in a tree over time. + + |{kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore] |or diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md index 0e3bfb2bd896b5..0cbfe4fcdead6e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md @@ -9,7 +9,7 @@ Update multiple documents at once Signature: ```typescript -bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; +bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; ``` ## Parameters @@ -20,7 +20,7 @@ bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): PromiseReturns: -Promise<SavedObjectsBatchResponse<unknown>> +Promise<SavedObjectsBatchResponse<T>> The result of the update operation containing both failed and updated saved objects. diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index 8944414f6bfbc8..cf501518ea5342 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -79,6 +79,7 @@ cluster. * The deprecation API is disabled. * SQL support is disabled. * Aggregations provided by the analytics plugin are no longer usable. +* All searchable snapshots indices are unassigned and cannot be searched. [discrete] [[expiration-watcher]] 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/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/fleet_packages.json b/fleet_packages.json index 69fd83f12037ca..3657057ad3431f 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -12,25 +12,4 @@ in order to verify package integrity. */ -[ - { - "name": "apm", - "version": "8.1.0" - }, - { - "name": "elastic_agent", - "version": "1.3.0" - }, - { - "name": "endpoint", - "version": "1.5.0" - }, - { - "name": "fleet_server", - "version": "1.1.0" - }, - { - "name": "synthetics", - "version": "0.9.2" - } -] +[] 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 bfd4c07a8fd23d..baf1103a8ef5cc 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept", "es": "node scripts/es", "preinstall": "node ./preinstall_check", - "postinstall": "node scripts/kbn patch_native_modules", "kbn": "node scripts/kbn", "lint": "yarn run lint:es && yarn run lint:style", "lint:es": "node scripts/eslint", @@ -107,7 +106,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", @@ -333,7 +332,7 @@ "raw-loader": "^3.1.0", "rbush": "^3.0.1", "re-resizable": "^6.1.1", - "re2": "^1.16.0", + "re2": "1.17.4", "react": "^16.12.0", "react-ace": "^7.0.5", "react-beautiful-dnd": "^13.1.0", @@ -348,7 +347,7 @@ "react-moment-proptypes": "^1.7.0", "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", - "react-query": "^3.34.0", + "react-query": "^3.34.7", "react-redux": "^7.2.0", "react-resizable": "^1.7.5", "react-resize-detector": "^4.2.0", @@ -480,6 +479,7 @@ "@kbn/test": "link:bazel-bin/packages/kbn-test", "@kbn/test-jest-helpers": "link:bazel-bin/packages/kbn-test-jest-helpers", "@kbn/test-subj-selector": "link:bazel-bin/packages/kbn-test-subj-selector", + "@kbn/type-summarizer": "link:bazel-bin/packages/kbn-type-summarizer", "@loaders.gl/polyfills": "^2.3.5", "@mapbox/vector-tile": "1.3.1", "@microsoft/api-documenter": "7.13.68", @@ -842,7 +842,7 @@ "multimatch": "^4.0.0", "mutation-observer": "^1.0.3", "ncp": "^2.0.0", - "node-sass": "^6.0.1", + "node-sass": "6.0.1", "null-loader": "^3.0.0", "nyc": "^15.1.0", "oboe": "^2.1.4", @@ -870,6 +870,7 @@ "simple-git": "1.116.0", "sinon": "^7.4.2", "sort-package-json": "^1.53.1", + "source-map": "^0.7.3", "spawn-sync": "^1.0.15", "string-replace-loader": "^2.2.0", "strong-log-transformer": "^2.1.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 02e82476cd88dd..77ec3b0c17295b 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -66,6 +66,7 @@ filegroup( "//packages/kbn-test-subj-selector:build", "//packages/kbn-timelion-grammar:build", "//packages/kbn-tinymath:build", + "//packages/kbn-type-summarizer:build", "//packages/kbn-typed-react-router-config:build", "//packages/kbn-ui-framework:build", "//packages/kbn-ui-shared-deps-npm:build", @@ -132,6 +133,7 @@ filegroup( "//packages/kbn-telemetry-tools:build_types", "//packages/kbn-test:build_types", "//packages/kbn-test-jest-helpers:build_types", + "//packages/kbn-type-summarizer:build_types", "//packages/kbn-typed-react-router-config:build_types", "//packages/kbn-ui-shared-deps-npm:build_types", "//packages/kbn-ui-shared-deps-src:build_types", diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index de8c97ed3b713a..09c5fbb47e3aaf 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -61,6 +61,7 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, + declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", diff --git a/packages/kbn-crypto/tsconfig.json b/packages/kbn-crypto/tsconfig.json index 272363e976ba1e..fc929cba6868ec 100644 --- a/packages/kbn-crypto/tsconfig.json +++ b/packages/kbn-crypto/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, + "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "src", 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-flot-charts/lib/jquery_flot.js b/packages/kbn-flot-charts/lib/jquery_flot.js index 43db1cc3d93db1..5252356279e51c 100644 --- a/packages/kbn-flot-charts/lib/jquery_flot.js +++ b/packages/kbn-flot-charts/lib/jquery_flot.js @@ -351,7 +351,7 @@ Licensed under the MIT license. if (info == null) { - var element = $("
").html(text) + var element = $("
").text(text) .css({ position: "absolute", 'max-width': width, diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 3a999272c3a4dd..afe7fcd9ddc867 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -121,4 +121,6 @@ pageLoadAssetSize: expressionPartitionVis: 26338 sharedUX: 16225 ux: 20784 + sessionView: 77750 cloudSecurityPosture: 19109 + visTypeGauge: 24113 \ No newline at end of file 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-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 647d421819597c..d22f3add0e2f4b 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(564); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(563); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildBazelProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); @@ -108,7 +108,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(343); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return _utils_package_json__WEBPACK_IMPORTED_MODULE_4__["transformDependencies"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(563); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(562); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -141,7 +141,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(129); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(558); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(557); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(220); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one @@ -8822,7 +8822,6 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _reset__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(553); /* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(554); /* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(556); -/* harmony import */ var _patch_native_modules__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(557); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -8836,15 +8835,13 @@ __webpack_require__.r(__webpack_exports__); - const commands = { bootstrap: _bootstrap__WEBPACK_IMPORTED_MODULE_0__["BootstrapCommand"], build: _build__WEBPACK_IMPORTED_MODULE_1__["BuildCommand"], clean: _clean__WEBPACK_IMPORTED_MODULE_2__["CleanCommand"], reset: _reset__WEBPACK_IMPORTED_MODULE_3__["ResetCommand"], run: _run__WEBPACK_IMPORTED_MODULE_4__["RunCommand"], - watch: _watch__WEBPACK_IMPORTED_MODULE_5__["WatchCommand"], - patch_native_modules: _patch_native_modules__WEBPACK_IMPORTED_MODULE_6__["PatchNativeModulesCommand"] + watch: _watch__WEBPACK_IMPORTED_MODULE_5__["WatchCommand"] }; /***/ }), @@ -41163,7 +41160,11 @@ async function installInDir(directory, extraArgs = []) { // given time (e.g. to avoid conflicts). await Object(_child_process__WEBPACK_IMPORTED_MODULE_0__["spawn"])(YARN_EXEC, options, { - cwd: directory + cwd: directory, + env: { + SASS_BINARY_SITE: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-sass', + RE2_DOWNLOAD_MIRROR: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2' + } }); } /** @@ -61768,86 +61769,6 @@ const WatchCommand = { /* 557 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PatchNativeModulesCommand", function() { return PatchNativeModulesCommand; }); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(132); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(131); -/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); -/* harmony import */ var _utils_child_process__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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. - */ - - - - - -const PatchNativeModulesCommand = { - description: 'Patch native modules by running build commands on M1 Macs', - name: 'patch_native_modules', - - async run(projects, _, { - kbn - }) { - var _projects$get; - - const kibanaProjectPath = ((_projects$get = projects.get('kibana')) === null || _projects$get === void 0 ? void 0 : _projects$get.path) || ''; - const reporter = _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__["CiStatsReporter"].fromEnv(_utils_log__WEBPACK_IMPORTED_MODULE_3__["log"]); - - if (process.platform !== 'darwin' || process.arch !== 'arm64') { - return; - } - - const startTime = Date.now(); - const nodeSassDir = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(kibanaProjectPath, 'node_modules/node-sass'); - const nodeSassNativeDist = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(nodeSassDir, `vendor/darwin-arm64-${process.versions.modules}/binding.node`); - - if (!fs__WEBPACK_IMPORTED_MODULE_1___default.a.existsSync(nodeSassNativeDist)) { - _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].info('Running build script for node-sass'); - await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_4__["spawn"])('npm', ['run', 'build'], { - cwd: nodeSassDir - }); - } - - const re2Dir = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(kibanaProjectPath, 'node_modules/re2'); - const re2NativeDist = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(re2Dir, 'build/Release/re2.node'); - - if (!fs__WEBPACK_IMPORTED_MODULE_1___default.a.existsSync(re2NativeDist)) { - _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].info('Running build script for re2'); - await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_4__["spawn"])('npm', ['run', 'rebuild'], { - cwd: re2Dir - }); - } - - _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].success('native modules should be setup for native ARM Mac development'); // send timings - - await reporter.timings({ - upstreamBranch: kbn.kibanaProject.json.branch, - // prevent loading @kbn/utils by passing null - kibanaUuid: kbn.getUuid() || null, - timings: [{ - group: 'scripts/kbn bootstrap', - id: 'patch native modudles for arm macs', - ms: Date.now() - startTime - }] - }); - } - -}; - -/***/ }), -/* 558 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); @@ -61857,7 +61778,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(220); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(340); /* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(420); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(559); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(558); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } @@ -61976,7 +61897,7 @@ function toArray(value) { } /***/ }), -/* 559 */ +/* 558 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -61986,13 +61907,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(132); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(560); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(559); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(333); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(408); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(340); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(563); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(562); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } @@ -62156,15 +62077,15 @@ class Kibana { } /***/ }), -/* 560 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(247); const arrayUnion = __webpack_require__(242); -const arrayDiffer = __webpack_require__(561); -const arrify = __webpack_require__(562); +const arrayDiffer = __webpack_require__(560); +const arrify = __webpack_require__(561); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -62188,7 +62109,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 561 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62203,7 +62124,7 @@ module.exports = arrayDiffer; /***/ }), -/* 562 */ +/* 561 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62233,7 +62154,7 @@ module.exports = arrify; /***/ }), -/* 563 */ +/* 562 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -62293,15 +62214,15 @@ function getProjectPaths({ } /***/ }), -/* 564 */ +/* 563 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(565); +/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(564); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(812); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(811); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); /* @@ -62315,19 +62236,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 565 */ +/* 564 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return buildBazelProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(566); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(565); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(778); +/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(777); /* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(812); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(811); /* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(421); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(231); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(220); @@ -62422,7 +62343,7 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { } /***/ }), -/* 566 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62430,14 +62351,14 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(164); const path = __webpack_require__(4); const os = __webpack_require__(122); -const pMap = __webpack_require__(567); -const arrify = __webpack_require__(562); -const globby = __webpack_require__(570); -const hasGlob = __webpack_require__(762); -const cpFile = __webpack_require__(764); -const junk = __webpack_require__(774); -const pFilter = __webpack_require__(775); -const CpyError = __webpack_require__(777); +const pMap = __webpack_require__(566); +const arrify = __webpack_require__(561); +const globby = __webpack_require__(569); +const hasGlob = __webpack_require__(761); +const cpFile = __webpack_require__(763); +const junk = __webpack_require__(773); +const pFilter = __webpack_require__(774); +const CpyError = __webpack_require__(776); const defaultOptions = { ignoreJunk: true @@ -62588,12 +62509,12 @@ module.exports = (source, destination, { /***/ }), -/* 567 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(568); +const AggregateError = __webpack_require__(567); module.exports = async ( iterable, @@ -62676,12 +62597,12 @@ module.exports = async ( /***/ }), -/* 568 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(569); +const indentString = __webpack_require__(568); const cleanStack = __webpack_require__(338); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -62730,7 +62651,7 @@ module.exports = AggregateError; /***/ }), -/* 569 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62772,17 +62693,17 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 570 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const arrayUnion = __webpack_require__(571); +const arrayUnion = __webpack_require__(570); const glob = __webpack_require__(244); -const fastGlob = __webpack_require__(573); -const dirGlob = __webpack_require__(756); -const gitignore = __webpack_require__(759); +const fastGlob = __webpack_require__(572); +const dirGlob = __webpack_require__(755); +const gitignore = __webpack_require__(758); const DEFAULT_FILTER = () => false; @@ -62927,12 +62848,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 571 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(572); +var arrayUniq = __webpack_require__(571); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -62940,7 +62861,7 @@ module.exports = function () { /***/ }), -/* 572 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63009,10 +62930,10 @@ if ('Set' in global) { /***/ }), -/* 573 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(574); +const pkg = __webpack_require__(573); module.exports = pkg.async; module.exports.default = pkg.async; @@ -63025,19 +62946,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 574 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(575); -var taskManager = __webpack_require__(576); -var reader_async_1 = __webpack_require__(727); -var reader_stream_1 = __webpack_require__(751); -var reader_sync_1 = __webpack_require__(752); -var arrayUtils = __webpack_require__(754); -var streamUtils = __webpack_require__(755); +var optionsManager = __webpack_require__(574); +var taskManager = __webpack_require__(575); +var reader_async_1 = __webpack_require__(726); +var reader_stream_1 = __webpack_require__(750); +var reader_sync_1 = __webpack_require__(751); +var arrayUtils = __webpack_require__(753); +var streamUtils = __webpack_require__(754); /** * Synchronous API. */ @@ -63103,7 +63024,7 @@ function isString(source) { /***/ }), -/* 575 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63141,13 +63062,13 @@ exports.prepare = prepare; /***/ }), -/* 576 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(577); +var patternUtils = __webpack_require__(576); /** * Generate tasks based on parent directory of each pattern. */ @@ -63238,16 +63159,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 577 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(578); +var globParent = __webpack_require__(577); var isGlob = __webpack_require__(266); -var micromatch = __webpack_require__(581); +var micromatch = __webpack_require__(580); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -63393,15 +63314,15 @@ exports.matchAny = matchAny; /***/ }), -/* 578 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(579); -var pathDirname = __webpack_require__(580); +var isglob = __webpack_require__(578); +var pathDirname = __webpack_require__(579); var isWin32 = __webpack_require__(122).platform() === 'win32'; module.exports = function globParent(str) { @@ -63424,7 +63345,7 @@ module.exports = function globParent(str) { /***/ }), -/* 579 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -63455,7 +63376,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 580 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63605,7 +63526,7 @@ module.exports.win32 = win32; /***/ }), -/* 581 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63616,18 +63537,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(113); -var braces = __webpack_require__(582); -var toRegex = __webpack_require__(583); -var extend = __webpack_require__(695); +var braces = __webpack_require__(581); +var toRegex = __webpack_require__(582); +var extend = __webpack_require__(694); /** * Local dependencies */ -var compilers = __webpack_require__(697); -var parsers = __webpack_require__(723); -var cache = __webpack_require__(724); -var utils = __webpack_require__(725); +var compilers = __webpack_require__(696); +var parsers = __webpack_require__(722); +var cache = __webpack_require__(723); +var utils = __webpack_require__(724); var MAX_LENGTH = 1024 * 64; /** @@ -64489,7 +64410,7 @@ module.exports = micromatch; /***/ }), -/* 582 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64499,18 +64420,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(583); -var unique = __webpack_require__(603); -var extend = __webpack_require__(604); +var toRegex = __webpack_require__(582); +var unique = __webpack_require__(602); +var extend = __webpack_require__(603); /** * Local dependencies */ -var compilers = __webpack_require__(606); -var parsers = __webpack_require__(621); -var Braces = __webpack_require__(626); -var utils = __webpack_require__(607); +var compilers = __webpack_require__(605); +var parsers = __webpack_require__(620); +var Braces = __webpack_require__(625); +var utils = __webpack_require__(606); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -64814,16 +64735,16 @@ module.exports = braces; /***/ }), -/* 583 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(584); -var define = __webpack_require__(590); -var extend = __webpack_require__(596); -var not = __webpack_require__(600); +var safe = __webpack_require__(583); +var define = __webpack_require__(589); +var extend = __webpack_require__(595); +var not = __webpack_require__(599); var MAX_LENGTH = 1024 * 64; /** @@ -64976,10 +64897,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 584 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(585); +var parse = __webpack_require__(584); var types = parse.types; module.exports = function (re, opts) { @@ -65025,13 +64946,13 @@ function isRegExp (x) { /***/ }), -/* 585 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(586); -var types = __webpack_require__(587); -var sets = __webpack_require__(588); -var positions = __webpack_require__(589); +var util = __webpack_require__(585); +var types = __webpack_require__(586); +var sets = __webpack_require__(587); +var positions = __webpack_require__(588); module.exports = function(regexpStr) { @@ -65313,11 +65234,11 @@ module.exports.types = types; /***/ }), -/* 586 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(587); -var sets = __webpack_require__(588); +var types = __webpack_require__(586); +var sets = __webpack_require__(587); // All of these are private and only used by randexp. @@ -65430,7 +65351,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 587 */ +/* 586 */ /***/ (function(module, exports) { module.exports = { @@ -65446,10 +65367,10 @@ module.exports = { /***/ }), -/* 588 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(587); +var types = __webpack_require__(586); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -65534,10 +65455,10 @@ exports.anyChar = function() { /***/ }), -/* 589 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(587); +var types = __webpack_require__(586); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -65557,7 +65478,7 @@ exports.end = function() { /***/ }), -/* 590 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65570,8 +65491,8 @@ exports.end = function() { -var isobject = __webpack_require__(591); -var isDescriptor = __webpack_require__(592); +var isobject = __webpack_require__(590); +var isDescriptor = __webpack_require__(591); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -65602,7 +65523,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 591 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65621,7 +65542,7 @@ module.exports = function isObject(val) { /***/ }), -/* 592 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65634,9 +65555,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(593); -var isAccessor = __webpack_require__(594); -var isData = __webpack_require__(595); +var typeOf = __webpack_require__(592); +var isAccessor = __webpack_require__(593); +var isData = __webpack_require__(594); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -65650,7 +65571,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 593 */ +/* 592 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -65785,7 +65706,7 @@ function isBuffer(val) { /***/ }), -/* 594 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65798,7 +65719,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(593); +var typeOf = __webpack_require__(592); // accessor descriptor properties var accessor = { @@ -65861,7 +65782,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 595 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65874,7 +65795,7 @@ module.exports = isAccessorDescriptor; -var typeOf = __webpack_require__(593); +var typeOf = __webpack_require__(592); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -65917,14 +65838,14 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 596 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(597); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(596); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -65984,7 +65905,7 @@ function isEnum(obj, key) { /***/ }), -/* 597 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65997,7 +65918,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -66005,7 +65926,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 598 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66018,7 +65939,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(591); +var isObject = __webpack_require__(590); function isObjectObject(o) { return isObject(o) === true @@ -66049,7 +65970,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 599 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66096,14 +66017,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 600 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(601); -var safe = __webpack_require__(584); +var extend = __webpack_require__(600); +var safe = __webpack_require__(583); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -66175,14 +66096,14 @@ module.exports = toRegex; /***/ }), -/* 601 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(602); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(601); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -66242,7 +66163,7 @@ function isEnum(obj, key) { /***/ }), -/* 602 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66255,7 +66176,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -66263,7 +66184,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 603 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66313,13 +66234,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 604 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(605); +var isObject = __webpack_require__(604); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -66353,7 +66274,7 @@ function hasOwn(obj, key) { /***/ }), -/* 605 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66373,13 +66294,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 606 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(607); +var utils = __webpack_require__(606); module.exports = function(braces, options) { braces.compiler @@ -66662,25 +66583,25 @@ function hasQueue(node) { /***/ }), -/* 607 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(608); +var splitString = __webpack_require__(607); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(604); -utils.flatten = __webpack_require__(611); -utils.isObject = __webpack_require__(591); -utils.fillRange = __webpack_require__(612); -utils.repeat = __webpack_require__(620); -utils.unique = __webpack_require__(603); +utils.extend = __webpack_require__(603); +utils.flatten = __webpack_require__(610); +utils.isObject = __webpack_require__(590); +utils.fillRange = __webpack_require__(611); +utils.repeat = __webpack_require__(619); +utils.unique = __webpack_require__(602); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -67012,7 +66933,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 608 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67025,7 +66946,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(609); +var extend = __webpack_require__(608); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -67190,14 +67111,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 609 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(610); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(609); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67257,7 +67178,7 @@ function isEnum(obj, key) { /***/ }), -/* 610 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67270,7 +67191,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67278,7 +67199,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 611 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67307,7 +67228,7 @@ function flat(arr, res) { /***/ }), -/* 612 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67321,10 +67242,10 @@ function flat(arr, res) { var util = __webpack_require__(113); -var isNumber = __webpack_require__(613); -var extend = __webpack_require__(616); -var repeat = __webpack_require__(618); -var toRegex = __webpack_require__(619); +var isNumber = __webpack_require__(612); +var extend = __webpack_require__(615); +var repeat = __webpack_require__(617); +var toRegex = __webpack_require__(618); /** * Return a range of numbers or letters. @@ -67522,7 +67443,7 @@ module.exports = fillRange; /***/ }), -/* 613 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67535,7 +67456,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(614); +var typeOf = __webpack_require__(613); module.exports = function isNumber(num) { var type = typeOf(num); @@ -67551,10 +67472,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 614 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -67673,7 +67594,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 615 */ +/* 614 */ /***/ (function(module, exports) { /*! @@ -67700,13 +67621,13 @@ function isSlowBuffer (obj) { /***/ }), -/* 616 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(617); +var isObject = __webpack_require__(616); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -67740,7 +67661,7 @@ function hasOwn(obj, key) { /***/ }), -/* 617 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67760,7 +67681,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 618 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67837,7 +67758,7 @@ function repeat(str, num) { /***/ }), -/* 619 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67850,8 +67771,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(618); -var isNumber = __webpack_require__(613); +var repeat = __webpack_require__(617); +var isNumber = __webpack_require__(612); var cache = {}; function toRegexRange(min, max, options) { @@ -68138,7 +68059,7 @@ module.exports = toRegexRange; /***/ }), -/* 620 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68163,14 +68084,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 621 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(622); -var utils = __webpack_require__(607); +var Node = __webpack_require__(621); +var utils = __webpack_require__(606); /** * Braces parsers @@ -68530,15 +68451,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 622 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(591); -var define = __webpack_require__(623); -var utils = __webpack_require__(624); +var isObject = __webpack_require__(590); +var define = __webpack_require__(622); +var utils = __webpack_require__(623); var ownNames; /** @@ -69029,7 +68950,7 @@ exports = module.exports = Node; /***/ }), -/* 623 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69042,7 +68963,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(592); +var isDescriptor = __webpack_require__(591); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -69067,13 +68988,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 624 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(625); +var typeOf = __webpack_require__(624); var utils = module.exports; /** @@ -70093,10 +70014,10 @@ function assert(val, message) { /***/ }), -/* 625 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -70215,17 +70136,17 @@ module.exports = function kindOf(val) { /***/ }), -/* 626 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(604); -var Snapdragon = __webpack_require__(627); -var compilers = __webpack_require__(606); -var parsers = __webpack_require__(621); -var utils = __webpack_require__(607); +var extend = __webpack_require__(603); +var Snapdragon = __webpack_require__(626); +var compilers = __webpack_require__(605); +var parsers = __webpack_require__(620); +var utils = __webpack_require__(606); /** * Customize Snapdragon parser and renderer @@ -70326,17 +70247,17 @@ module.exports = Braces; /***/ }), -/* 627 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(628); -var define = __webpack_require__(658); -var Compiler = __webpack_require__(669); -var Parser = __webpack_require__(692); -var utils = __webpack_require__(672); +var Base = __webpack_require__(627); +var define = __webpack_require__(657); +var Compiler = __webpack_require__(668); +var Parser = __webpack_require__(691); +var utils = __webpack_require__(671); var regexCache = {}; var cache = {}; @@ -70507,20 +70428,20 @@ module.exports.Parser = Parser; /***/ }), -/* 628 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var define = __webpack_require__(629); -var CacheBase = __webpack_require__(630); -var Emitter = __webpack_require__(631); -var isObject = __webpack_require__(591); -var merge = __webpack_require__(652); -var pascal = __webpack_require__(655); -var cu = __webpack_require__(656); +var define = __webpack_require__(628); +var CacheBase = __webpack_require__(629); +var Emitter = __webpack_require__(630); +var isObject = __webpack_require__(590); +var merge = __webpack_require__(651); +var pascal = __webpack_require__(654); +var cu = __webpack_require__(655); /** * Optionally define a custom `cache` namespace to use. @@ -70949,7 +70870,7 @@ module.exports.namespace = namespace; /***/ }), -/* 629 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70962,7 +70883,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(592); +var isDescriptor = __webpack_require__(591); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -70987,21 +70908,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 630 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(591); -var Emitter = __webpack_require__(631); -var visit = __webpack_require__(632); -var toPath = __webpack_require__(635); -var union = __webpack_require__(637); -var del = __webpack_require__(643); -var get = __webpack_require__(640); -var has = __webpack_require__(648); -var set = __webpack_require__(651); +var isObject = __webpack_require__(590); +var Emitter = __webpack_require__(630); +var visit = __webpack_require__(631); +var toPath = __webpack_require__(634); +var union = __webpack_require__(636); +var del = __webpack_require__(642); +var get = __webpack_require__(639); +var has = __webpack_require__(647); +var set = __webpack_require__(650); /** * Create a `Cache` constructor that when instantiated will @@ -71255,7 +71176,7 @@ module.exports.namespace = namespace; /***/ }), -/* 631 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { @@ -71424,7 +71345,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 632 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71437,8 +71358,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(633); -var mapVisit = __webpack_require__(634); +var visit = __webpack_require__(632); +var mapVisit = __webpack_require__(633); module.exports = function(collection, method, val) { var result; @@ -71461,7 +71382,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 633 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71474,7 +71395,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(591); +var isObject = __webpack_require__(590); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -71501,14 +71422,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 634 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var visit = __webpack_require__(633); +var visit = __webpack_require__(632); /** * Map `visit` over an array of objects. @@ -71545,7 +71466,7 @@ function isObject(val) { /***/ }), -/* 635 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71558,7 +71479,7 @@ function isObject(val) { -var typeOf = __webpack_require__(636); +var typeOf = __webpack_require__(635); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -71585,10 +71506,10 @@ function filter(arr) { /***/ }), -/* 636 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -71707,16 +71628,16 @@ module.exports = function kindOf(val) { /***/ }), -/* 637 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(638); -var union = __webpack_require__(639); -var get = __webpack_require__(640); -var set = __webpack_require__(641); +var isObject = __webpack_require__(637); +var union = __webpack_require__(638); +var get = __webpack_require__(639); +var set = __webpack_require__(640); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -71744,7 +71665,7 @@ function arrayify(val) { /***/ }), -/* 638 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71764,7 +71685,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 639 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71800,7 +71721,7 @@ module.exports = function union(init) { /***/ }), -/* 640 */ +/* 639 */ /***/ (function(module, exports) { /*! @@ -71856,7 +71777,7 @@ function toString(val) { /***/ }), -/* 641 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71869,10 +71790,10 @@ function toString(val) { -var split = __webpack_require__(608); -var extend = __webpack_require__(642); -var isPlainObject = __webpack_require__(598); -var isObject = __webpack_require__(638); +var split = __webpack_require__(607); +var extend = __webpack_require__(641); +var isPlainObject = __webpack_require__(597); +var isObject = __webpack_require__(637); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -71918,13 +71839,13 @@ function isValidKey(key) { /***/ }), -/* 642 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(638); +var isObject = __webpack_require__(637); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -71958,7 +71879,7 @@ function hasOwn(obj, key) { /***/ }), -/* 643 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71971,8 +71892,8 @@ function hasOwn(obj, key) { -var isObject = __webpack_require__(591); -var has = __webpack_require__(644); +var isObject = __webpack_require__(590); +var has = __webpack_require__(643); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -71997,7 +71918,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 644 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72010,9 +71931,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(645); -var hasValues = __webpack_require__(647); -var get = __webpack_require__(640); +var isObject = __webpack_require__(644); +var hasValues = __webpack_require__(646); +var get = __webpack_require__(639); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -72023,7 +71944,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 645 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72036,7 +71957,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(646); +var isArray = __webpack_require__(645); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -72044,7 +71965,7 @@ module.exports = function isObject(val) { /***/ }), -/* 646 */ +/* 645 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -72055,7 +71976,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 647 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72098,7 +72019,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 648 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72111,9 +72032,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(591); -var hasValues = __webpack_require__(649); -var get = __webpack_require__(640); +var isObject = __webpack_require__(590); +var hasValues = __webpack_require__(648); +var get = __webpack_require__(639); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -72121,7 +72042,7 @@ module.exports = function(val, prop) { /***/ }), -/* 649 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72134,8 +72055,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(650); -var isNumber = __webpack_require__(613); +var typeOf = __webpack_require__(649); +var isNumber = __webpack_require__(612); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -72188,10 +72109,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 650 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -72313,7 +72234,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 651 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72326,10 +72247,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(608); -var extend = __webpack_require__(642); -var isPlainObject = __webpack_require__(598); -var isObject = __webpack_require__(638); +var split = __webpack_require__(607); +var extend = __webpack_require__(641); +var isPlainObject = __webpack_require__(597); +var isObject = __webpack_require__(637); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -72375,14 +72296,14 @@ function isValidKey(key) { /***/ }), -/* 652 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(653); -var forIn = __webpack_require__(654); +var isExtendable = __webpack_require__(652); +var forIn = __webpack_require__(653); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -72446,7 +72367,7 @@ module.exports = mixinDeep; /***/ }), -/* 653 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72459,7 +72380,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -72467,7 +72388,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 654 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72490,7 +72411,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 655 */ +/* 654 */ /***/ (function(module, exports) { /*! @@ -72517,14 +72438,14 @@ module.exports = pascalcase; /***/ }), -/* 656 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var utils = __webpack_require__(657); +var utils = __webpack_require__(656); /** * Expose class utils @@ -72889,7 +72810,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 657 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72903,10 +72824,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(639); -utils.define = __webpack_require__(658); -utils.isObj = __webpack_require__(591); -utils.staticExtend = __webpack_require__(665); +utils.union = __webpack_require__(638); +utils.define = __webpack_require__(657); +utils.isObj = __webpack_require__(590); +utils.staticExtend = __webpack_require__(664); /** @@ -72917,7 +72838,7 @@ module.exports = utils; /***/ }), -/* 658 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72930,7 +72851,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(659); +var isDescriptor = __webpack_require__(658); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -72955,7 +72876,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 659 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72968,9 +72889,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(660); -var isAccessor = __webpack_require__(661); -var isData = __webpack_require__(663); +var typeOf = __webpack_require__(659); +var isAccessor = __webpack_require__(660); +var isData = __webpack_require__(662); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -72984,7 +72905,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 660 */ +/* 659 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -73137,7 +73058,7 @@ function isBuffer(val) { /***/ }), -/* 661 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73150,7 +73071,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(662); +var typeOf = __webpack_require__(661); // accessor descriptor properties var accessor = { @@ -73213,10 +73134,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 662 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -73335,7 +73256,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 663 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73348,7 +73269,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(664); +var typeOf = __webpack_require__(663); // data descriptor properties var data = { @@ -73397,10 +73318,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 664 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -73519,7 +73440,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 665 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73532,8 +73453,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(666); -var define = __webpack_require__(658); +var copy = __webpack_require__(665); +var define = __webpack_require__(657); var util = __webpack_require__(113); /** @@ -73616,15 +73537,15 @@ module.exports = extend; /***/ }), -/* 666 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(667); -var copyDescriptor = __webpack_require__(668); -var define = __webpack_require__(658); +var typeOf = __webpack_require__(666); +var copyDescriptor = __webpack_require__(667); +var define = __webpack_require__(657); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -73797,10 +73718,10 @@ module.exports.has = has; /***/ }), -/* 667 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -73919,7 +73840,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 668 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74007,16 +73928,16 @@ function isObject(val) { /***/ }), -/* 669 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(670); -var define = __webpack_require__(658); +var use = __webpack_require__(669); +var define = __webpack_require__(657); var debug = __webpack_require__(205)('snapdragon:compiler'); -var utils = __webpack_require__(672); +var utils = __webpack_require__(671); /** * Create a new `Compiler` with the given `options`. @@ -74170,7 +74091,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(691); + var sourcemaps = __webpack_require__(690); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -74191,7 +74112,7 @@ module.exports = Compiler; /***/ }), -/* 670 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74204,7 +74125,7 @@ module.exports = Compiler; -var utils = __webpack_require__(671); +var utils = __webpack_require__(670); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -74319,7 +74240,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 671 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74333,8 +74254,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(658); -utils.isObject = __webpack_require__(591); +utils.define = __webpack_require__(657); +utils.isObject = __webpack_require__(590); utils.isString = function(val) { @@ -74349,7 +74270,7 @@ module.exports = utils; /***/ }), -/* 672 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74359,9 +74280,9 @@ module.exports = utils; * Module dependencies */ -exports.extend = __webpack_require__(642); -exports.SourceMap = __webpack_require__(673); -exports.sourceMapResolve = __webpack_require__(684); +exports.extend = __webpack_require__(641); +exports.SourceMap = __webpack_require__(672); +exports.sourceMapResolve = __webpack_require__(683); /** * Convert backslash in the given string to forward slashes @@ -74404,7 +74325,7 @@ exports.last = function(arr, n) { /***/ }), -/* 673 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -74412,13 +74333,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(674).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(680).SourceMapConsumer; -exports.SourceNode = __webpack_require__(683).SourceNode; +exports.SourceMapGenerator = __webpack_require__(673).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(679).SourceMapConsumer; +exports.SourceNode = __webpack_require__(682).SourceNode; /***/ }), -/* 674 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74428,10 +74349,10 @@ exports.SourceNode = __webpack_require__(683).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(675); -var util = __webpack_require__(677); -var ArraySet = __webpack_require__(678).ArraySet; -var MappingList = __webpack_require__(679).MappingList; +var base64VLQ = __webpack_require__(674); +var util = __webpack_require__(676); +var ArraySet = __webpack_require__(677).ArraySet; +var MappingList = __webpack_require__(678).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -74840,7 +74761,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 675 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74880,7 +74801,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(676); +var base64 = __webpack_require__(675); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -74986,7 +74907,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 676 */ +/* 675 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75059,7 +74980,7 @@ exports.decode = function (charCode) { /***/ }), -/* 677 */ +/* 676 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75482,7 +75403,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 678 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75492,7 +75413,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(677); +var util = __webpack_require__(676); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -75609,7 +75530,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 679 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75619,7 +75540,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(677); +var util = __webpack_require__(676); /** * Determine whether mappingB is after mappingA with respect to generated @@ -75694,7 +75615,7 @@ exports.MappingList = MappingList; /***/ }), -/* 680 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75704,11 +75625,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(677); -var binarySearch = __webpack_require__(681); -var ArraySet = __webpack_require__(678).ArraySet; -var base64VLQ = __webpack_require__(675); -var quickSort = __webpack_require__(682).quickSort; +var util = __webpack_require__(676); +var binarySearch = __webpack_require__(680); +var ArraySet = __webpack_require__(677).ArraySet; +var base64VLQ = __webpack_require__(674); +var quickSort = __webpack_require__(681).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -76782,7 +76703,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 681 */ +/* 680 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76899,7 +76820,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 682 */ +/* 681 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -77019,7 +76940,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 683 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -77029,8 +76950,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(674).SourceMapGenerator; -var util = __webpack_require__(677); +var SourceMapGenerator = __webpack_require__(673).SourceMapGenerator; +var util = __webpack_require__(676); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -77438,17 +77359,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 684 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(685) -var resolveUrl = __webpack_require__(686) -var decodeUriComponent = __webpack_require__(687) -var urix = __webpack_require__(689) -var atob = __webpack_require__(690) +var sourceMappingURL = __webpack_require__(684) +var resolveUrl = __webpack_require__(685) +var decodeUriComponent = __webpack_require__(686) +var urix = __webpack_require__(688) +var atob = __webpack_require__(689) @@ -77746,7 +77667,7 @@ module.exports = { /***/ }), -/* 685 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -77809,7 +77730,7 @@ void (function(root, factory) { /***/ }), -/* 686 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -77827,13 +77748,13 @@ module.exports = resolveUrl /***/ }), -/* 687 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(688) +var decodeUriComponent = __webpack_require__(687) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -77844,7 +77765,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 688 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77945,7 +77866,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 689 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -77968,7 +77889,7 @@ module.exports = urix /***/ }), -/* 690 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77982,7 +77903,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 691 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77990,8 +77911,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(132); var path = __webpack_require__(4); -var define = __webpack_require__(658); -var utils = __webpack_require__(672); +var define = __webpack_require__(657); +var utils = __webpack_require__(671); /** * Expose `mixin()`. @@ -78134,19 +78055,19 @@ exports.comment = function(node) { /***/ }), -/* 692 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(670); +var use = __webpack_require__(669); var util = __webpack_require__(113); -var Cache = __webpack_require__(693); -var define = __webpack_require__(658); +var Cache = __webpack_require__(692); +var define = __webpack_require__(657); var debug = __webpack_require__(205)('snapdragon:parser'); -var Position = __webpack_require__(694); -var utils = __webpack_require__(672); +var Position = __webpack_require__(693); +var utils = __webpack_require__(671); /** * Create a new `Parser` with the given `input` and `options`. @@ -78674,7 +78595,7 @@ module.exports = Parser; /***/ }), -/* 693 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78781,13 +78702,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 694 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(658); +var define = __webpack_require__(657); /** * Store position for a node @@ -78802,14 +78723,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 695 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(696); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(695); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -78869,7 +78790,7 @@ function isEnum(obj, key) { /***/ }), -/* 696 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78882,7 +78803,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -78890,14 +78811,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 697 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(698); -var extglob = __webpack_require__(712); +var nanomatch = __webpack_require__(697); +var extglob = __webpack_require__(711); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -78974,7 +78895,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 698 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78985,17 +78906,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(113); -var toRegex = __webpack_require__(583); -var extend = __webpack_require__(699); +var toRegex = __webpack_require__(582); +var extend = __webpack_require__(698); /** * Local dependencies */ -var compilers = __webpack_require__(701); -var parsers = __webpack_require__(702); -var cache = __webpack_require__(705); -var utils = __webpack_require__(707); +var compilers = __webpack_require__(700); +var parsers = __webpack_require__(701); +var cache = __webpack_require__(704); +var utils = __webpack_require__(706); var MAX_LENGTH = 1024 * 64; /** @@ -79819,14 +79740,14 @@ module.exports = nanomatch; /***/ }), -/* 699 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(700); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(699); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -79886,7 +79807,7 @@ function isEnum(obj, key) { /***/ }), -/* 700 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79899,7 +79820,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -79907,7 +79828,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 701 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80253,15 +80174,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 702 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(600); -var toRegex = __webpack_require__(583); -var isOdd = __webpack_require__(703); +var regexNot = __webpack_require__(599); +var toRegex = __webpack_require__(582); +var isOdd = __webpack_require__(702); /** * Characters to use in negation regex (we want to "not" match @@ -80647,7 +80568,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 703 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80660,7 +80581,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(704); +var isNumber = __webpack_require__(703); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -80674,7 +80595,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 704 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80702,14 +80623,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 705 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(706))(); +module.exports = new (__webpack_require__(705))(); /***/ }), -/* 706 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80722,7 +80643,7 @@ module.exports = new (__webpack_require__(706))(); -var MapCache = __webpack_require__(693); +var MapCache = __webpack_require__(692); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -80844,7 +80765,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 707 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80857,14 +80778,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(708)(); -var Snapdragon = __webpack_require__(627); -utils.define = __webpack_require__(709); -utils.diff = __webpack_require__(710); -utils.extend = __webpack_require__(699); -utils.pick = __webpack_require__(711); -utils.typeOf = __webpack_require__(593); -utils.unique = __webpack_require__(603); +var isWindows = __webpack_require__(707)(); +var Snapdragon = __webpack_require__(626); +utils.define = __webpack_require__(708); +utils.diff = __webpack_require__(709); +utils.extend = __webpack_require__(698); +utils.pick = __webpack_require__(710); +utils.typeOf = __webpack_require__(592); +utils.unique = __webpack_require__(602); /** * Returns true if the given value is effectively an empty string @@ -81230,7 +81151,7 @@ utils.unixify = function(options) { /***/ }), -/* 708 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -81258,7 +81179,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 709 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81271,8 +81192,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(591); -var isDescriptor = __webpack_require__(592); +var isobject = __webpack_require__(590); +var isDescriptor = __webpack_require__(591); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -81303,7 +81224,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 710 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81357,7 +81278,7 @@ function diffArray(one, two) { /***/ }), -/* 711 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81370,7 +81291,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(591); +var isObject = __webpack_require__(590); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -81399,7 +81320,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 712 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81409,18 +81330,18 @@ module.exports = function pick(obj, keys) { * Module dependencies */ -var extend = __webpack_require__(642); -var unique = __webpack_require__(603); -var toRegex = __webpack_require__(583); +var extend = __webpack_require__(641); +var unique = __webpack_require__(602); +var toRegex = __webpack_require__(582); /** * Local dependencies */ -var compilers = __webpack_require__(713); -var parsers = __webpack_require__(719); -var Extglob = __webpack_require__(722); -var utils = __webpack_require__(721); +var compilers = __webpack_require__(712); +var parsers = __webpack_require__(718); +var Extglob = __webpack_require__(721); +var utils = __webpack_require__(720); var MAX_LENGTH = 1024 * 64; /** @@ -81737,13 +81658,13 @@ module.exports = extglob; /***/ }), -/* 713 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(714); +var brackets = __webpack_require__(713); /** * Extglob compilers @@ -81913,7 +81834,7 @@ module.exports = function(extglob) { /***/ }), -/* 714 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81923,17 +81844,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(715); -var parsers = __webpack_require__(717); +var compilers = __webpack_require__(714); +var parsers = __webpack_require__(716); /** * Module dependencies */ var debug = __webpack_require__(205)('expand-brackets'); -var extend = __webpack_require__(642); -var Snapdragon = __webpack_require__(627); -var toRegex = __webpack_require__(583); +var extend = __webpack_require__(641); +var Snapdragon = __webpack_require__(626); +var toRegex = __webpack_require__(582); /** * Parses the given POSIX character class `pattern` and returns a @@ -82131,13 +82052,13 @@ module.exports = brackets; /***/ }), -/* 715 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(716); +var posix = __webpack_require__(715); module.exports = function(brackets) { brackets.compiler @@ -82225,7 +82146,7 @@ module.exports = function(brackets) { /***/ }), -/* 716 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82254,14 +82175,14 @@ module.exports = { /***/ }), -/* 717 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(718); -var define = __webpack_require__(658); +var utils = __webpack_require__(717); +var define = __webpack_require__(657); /** * Text regex @@ -82480,14 +82401,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 718 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(583); -var regexNot = __webpack_require__(600); +var toRegex = __webpack_require__(582); +var regexNot = __webpack_require__(599); var cached; /** @@ -82521,15 +82442,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 719 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(714); -var define = __webpack_require__(720); -var utils = __webpack_require__(721); +var brackets = __webpack_require__(713); +var define = __webpack_require__(719); +var utils = __webpack_require__(720); /** * Characters to use in text regex (we want to "not" match @@ -82684,7 +82605,7 @@ module.exports = parsers; /***/ }), -/* 720 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82697,7 +82618,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(592); +var isDescriptor = __webpack_require__(591); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -82722,14 +82643,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 721 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(600); -var Cache = __webpack_require__(706); +var regex = __webpack_require__(599); +var Cache = __webpack_require__(705); /** * Utils @@ -82798,7 +82719,7 @@ utils.createRegex = function(str) { /***/ }), -/* 722 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82808,16 +82729,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(627); -var define = __webpack_require__(720); -var extend = __webpack_require__(642); +var Snapdragon = __webpack_require__(626); +var define = __webpack_require__(719); +var extend = __webpack_require__(641); /** * Local dependencies */ -var compilers = __webpack_require__(713); -var parsers = __webpack_require__(719); +var compilers = __webpack_require__(712); +var parsers = __webpack_require__(718); /** * Customize Snapdragon parser and renderer @@ -82883,16 +82804,16 @@ module.exports = Extglob; /***/ }), -/* 723 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(712); -var nanomatch = __webpack_require__(698); -var regexNot = __webpack_require__(600); -var toRegex = __webpack_require__(583); +var extglob = __webpack_require__(711); +var nanomatch = __webpack_require__(697); +var regexNot = __webpack_require__(599); +var toRegex = __webpack_require__(582); var not; /** @@ -82973,14 +82894,14 @@ function textRegex(pattern) { /***/ }), -/* 724 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(706))(); +module.exports = new (__webpack_require__(705))(); /***/ }), -/* 725 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82993,13 +82914,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(627); -utils.define = __webpack_require__(726); -utils.diff = __webpack_require__(710); -utils.extend = __webpack_require__(695); -utils.pick = __webpack_require__(711); -utils.typeOf = __webpack_require__(593); -utils.unique = __webpack_require__(603); +var Snapdragon = __webpack_require__(626); +utils.define = __webpack_require__(725); +utils.diff = __webpack_require__(709); +utils.extend = __webpack_require__(694); +utils.pick = __webpack_require__(710); +utils.typeOf = __webpack_require__(592); +utils.unique = __webpack_require__(602); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -83296,7 +83217,7 @@ utils.unixify = function(options) { /***/ }), -/* 726 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83309,8 +83230,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(591); -var isDescriptor = __webpack_require__(592); +var isobject = __webpack_require__(590); +var isDescriptor = __webpack_require__(591); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -83341,7 +83262,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 727 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83360,9 +83281,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(728); -var reader_1 = __webpack_require__(741); -var fs_stream_1 = __webpack_require__(745); +var readdir = __webpack_require__(727); +var reader_1 = __webpack_require__(740); +var fs_stream_1 = __webpack_require__(744); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -83423,15 +83344,15 @@ exports.default = ReaderAsync; /***/ }), -/* 728 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(729); -const readdirAsync = __webpack_require__(737); -const readdirStream = __webpack_require__(740); +const readdirSync = __webpack_require__(728); +const readdirAsync = __webpack_require__(736); +const readdirStream = __webpack_require__(739); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -83515,7 +83436,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 729 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83523,11 +83444,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(730); +const DirectoryReader = __webpack_require__(729); let syncFacade = { - fs: __webpack_require__(735), - forEach: __webpack_require__(736), + fs: __webpack_require__(734), + forEach: __webpack_require__(735), sync: true }; @@ -83556,7 +83477,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 730 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83565,9 +83486,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(173).Readable; const EventEmitter = __webpack_require__(164).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(731); -const stat = __webpack_require__(733); -const call = __webpack_require__(734); +const normalizeOptions = __webpack_require__(730); +const stat = __webpack_require__(732); +const call = __webpack_require__(733); /** * Asynchronously reads the contents of a directory and streams the results @@ -83943,14 +83864,14 @@ module.exports = DirectoryReader; /***/ }), -/* 731 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(732); +const globToRegExp = __webpack_require__(731); module.exports = normalizeOptions; @@ -84127,7 +84048,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 732 */ +/* 731 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -84264,13 +84185,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 733 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(734); +const call = __webpack_require__(733); module.exports = stat; @@ -84345,7 +84266,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 734 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84406,14 +84327,14 @@ function callOnce (fn) { /***/ }), -/* 735 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const call = __webpack_require__(734); +const call = __webpack_require__(733); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -84477,7 +84398,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 736 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84506,7 +84427,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 737 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84514,12 +84435,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(738); -const DirectoryReader = __webpack_require__(730); +const maybe = __webpack_require__(737); +const DirectoryReader = __webpack_require__(729); let asyncFacade = { fs: __webpack_require__(132), - forEach: __webpack_require__(739), + forEach: __webpack_require__(738), async: true }; @@ -84561,7 +84482,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 738 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84588,7 +84509,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 739 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84624,7 +84545,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 740 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84632,11 +84553,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(730); +const DirectoryReader = __webpack_require__(729); let streamFacade = { fs: __webpack_require__(132), - forEach: __webpack_require__(739), + forEach: __webpack_require__(738), async: true }; @@ -84656,16 +84577,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 741 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(742); -var entry_1 = __webpack_require__(744); -var pathUtil = __webpack_require__(743); +var deep_1 = __webpack_require__(741); +var entry_1 = __webpack_require__(743); +var pathUtil = __webpack_require__(742); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -84731,14 +84652,14 @@ exports.default = Reader; /***/ }), -/* 742 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(743); -var patternUtils = __webpack_require__(577); +var pathUtils = __webpack_require__(742); +var patternUtils = __webpack_require__(576); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -84821,7 +84742,7 @@ exports.default = DeepFilter; /***/ }), -/* 743 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84852,14 +84773,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 744 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(743); -var patternUtils = __webpack_require__(577); +var pathUtils = __webpack_require__(742); +var patternUtils = __webpack_require__(576); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -84944,7 +84865,7 @@ exports.default = EntryFilter; /***/ }), -/* 745 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84964,8 +84885,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(173); -var fsStat = __webpack_require__(746); -var fs_1 = __webpack_require__(750); +var fsStat = __webpack_require__(745); +var fs_1 = __webpack_require__(749); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -85015,14 +84936,14 @@ exports.default = FileSystemStream; /***/ }), -/* 746 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(747); -const statProvider = __webpack_require__(749); +const optionsManager = __webpack_require__(746); +const statProvider = __webpack_require__(748); /** * Asynchronous API. */ @@ -85053,13 +84974,13 @@ exports.statSync = statSync; /***/ }), -/* 747 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(748); +const fsAdapter = __webpack_require__(747); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -85072,7 +84993,7 @@ exports.prepare = prepare; /***/ }), -/* 748 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85095,7 +85016,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 749 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85147,7 +85068,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 750 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85178,7 +85099,7 @@ exports.default = FileSystem; /***/ }), -/* 751 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85198,9 +85119,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(173); -var readdir = __webpack_require__(728); -var reader_1 = __webpack_require__(741); -var fs_stream_1 = __webpack_require__(745); +var readdir = __webpack_require__(727); +var reader_1 = __webpack_require__(740); +var fs_stream_1 = __webpack_require__(744); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -85268,7 +85189,7 @@ exports.default = ReaderStream; /***/ }), -/* 752 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85287,9 +85208,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(728); -var reader_1 = __webpack_require__(741); -var fs_sync_1 = __webpack_require__(753); +var readdir = __webpack_require__(727); +var reader_1 = __webpack_require__(740); +var fs_sync_1 = __webpack_require__(752); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -85349,7 +85270,7 @@ exports.default = ReaderSync; /***/ }), -/* 753 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85368,8 +85289,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(746); -var fs_1 = __webpack_require__(750); +var fsStat = __webpack_require__(745); +var fs_1 = __webpack_require__(749); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -85415,7 +85336,7 @@ exports.default = FileSystemSync; /***/ }), -/* 754 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85431,7 +85352,7 @@ exports.flatten = flatten; /***/ }), -/* 755 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85452,13 +85373,13 @@ exports.merge = merge; /***/ }), -/* 756 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(757); +const pathType = __webpack_require__(756); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -85524,13 +85445,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 757 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const pify = __webpack_require__(758); +const pify = __webpack_require__(757); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -85573,7 +85494,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 758 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85664,17 +85585,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 759 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(573); -const gitIgnore = __webpack_require__(760); +const fastGlob = __webpack_require__(572); +const gitIgnore = __webpack_require__(759); const pify = __webpack_require__(404); -const slash = __webpack_require__(761); +const slash = __webpack_require__(760); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -85772,7 +85693,7 @@ module.exports.sync = options => { /***/ }), -/* 760 */ +/* 759 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -86241,7 +86162,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 761 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86259,7 +86180,7 @@ module.exports = input => { /***/ }), -/* 762 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86272,7 +86193,7 @@ module.exports = input => { -var isGlob = __webpack_require__(763); +var isGlob = __webpack_require__(762); module.exports = function hasGlob(val) { if (val == null) return false; @@ -86292,7 +86213,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 763 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -86323,17 +86244,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 764 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(132); -const pEvent = __webpack_require__(765); -const CpFileError = __webpack_require__(768); -const fs = __webpack_require__(770); -const ProgressEmitter = __webpack_require__(773); +const pEvent = __webpack_require__(764); +const CpFileError = __webpack_require__(767); +const fs = __webpack_require__(769); +const ProgressEmitter = __webpack_require__(772); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -86447,12 +86368,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 765 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(766); +const pTimeout = __webpack_require__(765); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -86743,12 +86664,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 766 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(767); +const pFinally = __webpack_require__(766); class TimeoutError extends Error { constructor(message) { @@ -86794,7 +86715,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 767 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86816,12 +86737,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 768 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(769); +const NestedError = __webpack_require__(768); class CpFileError extends NestedError { constructor(message, nested) { @@ -86835,7 +86756,7 @@ module.exports = CpFileError; /***/ }), -/* 769 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(113).inherits; @@ -86891,16 +86812,16 @@ module.exports = NestedError; /***/ }), -/* 770 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(113); const fs = __webpack_require__(233); -const makeDir = __webpack_require__(771); -const pEvent = __webpack_require__(765); -const CpFileError = __webpack_require__(768); +const makeDir = __webpack_require__(770); +const pEvent = __webpack_require__(764); +const CpFileError = __webpack_require__(767); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -86997,7 +86918,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 771 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87005,7 +86926,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(132); const path = __webpack_require__(4); const {promisify} = __webpack_require__(113); -const semver = __webpack_require__(772); +const semver = __webpack_require__(771); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -87160,7 +87081,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 772 */ +/* 771 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -88762,7 +88683,7 @@ function coerce (version, options) { /***/ }), -/* 773 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88803,7 +88724,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 774 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88849,12 +88770,12 @@ exports.default = module.exports; /***/ }), -/* 775 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(776); +const pMap = __webpack_require__(775); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -88871,7 +88792,7 @@ module.exports.default = pFilter; /***/ }), -/* 776 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88950,12 +88871,12 @@ module.exports.default = pMap; /***/ }), -/* 777 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(769); +const NestedError = __webpack_require__(768); class CpyError extends NestedError { constructor(message, nested) { @@ -88969,7 +88890,7 @@ module.exports = CpyError; /***/ }), -/* 778 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88977,10 +88898,10 @@ module.exports = CpyError; const fs = __webpack_require__(132); const arrayUnion = __webpack_require__(242); const merge2 = __webpack_require__(243); -const fastGlob = __webpack_require__(779); +const fastGlob = __webpack_require__(778); const dirGlob = __webpack_require__(326); -const gitignore = __webpack_require__(810); -const {FilterStream, UniqueStream} = __webpack_require__(811); +const gitignore = __webpack_require__(809); +const {FilterStream, UniqueStream} = __webpack_require__(810); const DEFAULT_FILTER = () => false; @@ -89157,17 +89078,17 @@ module.exports.gitignore = gitignore; /***/ }), -/* 779 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(780); -const async_1 = __webpack_require__(796); -const stream_1 = __webpack_require__(806); -const sync_1 = __webpack_require__(807); -const settings_1 = __webpack_require__(809); -const utils = __webpack_require__(781); +const taskManager = __webpack_require__(779); +const async_1 = __webpack_require__(795); +const stream_1 = __webpack_require__(805); +const sync_1 = __webpack_require__(806); +const settings_1 = __webpack_require__(808); +const utils = __webpack_require__(780); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -89231,14 +89152,14 @@ module.exports = FastGlob; /***/ }), -/* 780 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; -const utils = __webpack_require__(781); +const utils = __webpack_require__(780); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -89318,31 +89239,31 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 781 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; -const array = __webpack_require__(782); +const array = __webpack_require__(781); exports.array = array; -const errno = __webpack_require__(783); +const errno = __webpack_require__(782); exports.errno = errno; -const fs = __webpack_require__(784); +const fs = __webpack_require__(783); exports.fs = fs; -const path = __webpack_require__(785); +const path = __webpack_require__(784); exports.path = path; -const pattern = __webpack_require__(786); +const pattern = __webpack_require__(785); exports.pattern = pattern; -const stream = __webpack_require__(794); +const stream = __webpack_require__(793); exports.stream = stream; -const string = __webpack_require__(795); +const string = __webpack_require__(794); exports.string = string; /***/ }), -/* 782 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89371,7 +89292,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 783 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89385,7 +89306,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 784 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89411,7 +89332,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 785 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89451,7 +89372,7 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 786 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89460,7 +89381,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.isPatternRelatedToParentDirectory = exports.getPatternsOutsideCurrentDirectory = exports.getPatternsInsideCurrentDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; const path = __webpack_require__(4); const globParent = __webpack_require__(265); -const micromatch = __webpack_require__(787); +const micromatch = __webpack_require__(786); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; @@ -89615,7 +89536,7 @@ exports.matchAny = matchAny; /***/ }), -/* 787 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89623,8 +89544,8 @@ exports.matchAny = matchAny; const util = __webpack_require__(113); const braces = __webpack_require__(269); -const picomatch = __webpack_require__(788); -const utils = __webpack_require__(791); +const picomatch = __webpack_require__(787); +const utils = __webpack_require__(790); const isEmptyString = val => val === '' || val === './'; /** @@ -90089,27 +90010,27 @@ module.exports = micromatch; /***/ }), -/* 788 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(789); +module.exports = __webpack_require__(788); /***/ }), -/* 789 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const scan = __webpack_require__(790); -const parse = __webpack_require__(793); -const utils = __webpack_require__(791); -const constants = __webpack_require__(792); +const scan = __webpack_require__(789); +const parse = __webpack_require__(792); +const utils = __webpack_require__(790); +const constants = __webpack_require__(791); const isObject = val => val && typeof val === 'object' && !Array.isArray(val); /** @@ -90448,13 +90369,13 @@ module.exports = picomatch; /***/ }), -/* 790 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(791); +const utils = __webpack_require__(790); const { CHAR_ASTERISK, /* * */ CHAR_AT, /* @ */ @@ -90471,7 +90392,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(792); +} = __webpack_require__(791); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -90846,7 +90767,7 @@ module.exports = scan; /***/ }), -/* 791 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90859,7 +90780,7 @@ const { REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL -} = __webpack_require__(792); +} = __webpack_require__(791); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -90917,7 +90838,7 @@ exports.wrapOutput = (input, state = {}, options = {}) => { /***/ }), -/* 792 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91103,14 +91024,14 @@ module.exports = { /***/ }), -/* 793 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const constants = __webpack_require__(792); -const utils = __webpack_require__(791); +const constants = __webpack_require__(791); +const utils = __webpack_require__(790); /** * Constants @@ -92194,7 +92115,7 @@ module.exports = parse; /***/ }), -/* 794 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92218,7 +92139,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 795 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92236,14 +92157,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 796 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(797); -const provider_1 = __webpack_require__(799); +const stream_1 = __webpack_require__(796); +const provider_1 = __webpack_require__(798); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -92271,7 +92192,7 @@ exports.default = ProviderAsync; /***/ }), -/* 797 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92280,7 +92201,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(173); const fsStat = __webpack_require__(289); const fsWalk = __webpack_require__(294); -const reader_1 = __webpack_require__(798); +const reader_1 = __webpack_require__(797); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -92333,7 +92254,7 @@ exports.default = ReaderStream; /***/ }), -/* 798 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92341,7 +92262,7 @@ exports.default = ReaderStream; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); const fsStat = __webpack_require__(289); -const utils = __webpack_require__(781); +const utils = __webpack_require__(780); class Reader { constructor(_settings) { this._settings = _settings; @@ -92373,17 +92294,17 @@ exports.default = Reader; /***/ }), -/* 799 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(800); -const entry_1 = __webpack_require__(803); -const error_1 = __webpack_require__(804); -const entry_2 = __webpack_require__(805); +const deep_1 = __webpack_require__(799); +const entry_1 = __webpack_require__(802); +const error_1 = __webpack_require__(803); +const entry_2 = __webpack_require__(804); class Provider { constructor(_settings) { this._settings = _settings; @@ -92428,14 +92349,14 @@ exports.default = Provider; /***/ }), -/* 800 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(781); -const partial_1 = __webpack_require__(801); +const utils = __webpack_require__(780); +const partial_1 = __webpack_require__(800); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -92497,13 +92418,13 @@ exports.default = DeepFilter; /***/ }), -/* 801 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(802); +const matcher_1 = __webpack_require__(801); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -92542,13 +92463,13 @@ exports.default = PartialMatcher; /***/ }), -/* 802 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(781); +const utils = __webpack_require__(780); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -92599,13 +92520,13 @@ exports.default = Matcher; /***/ }), -/* 803 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(781); +const utils = __webpack_require__(780); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -92662,13 +92583,13 @@ exports.default = EntryFilter; /***/ }), -/* 804 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(781); +const utils = __webpack_require__(780); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -92684,13 +92605,13 @@ exports.default = ErrorFilter; /***/ }), -/* 805 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(781); +const utils = __webpack_require__(780); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -92717,15 +92638,15 @@ exports.default = EntryTransformer; /***/ }), -/* 806 */ +/* 805 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(173); -const stream_2 = __webpack_require__(797); -const provider_1 = __webpack_require__(799); +const stream_2 = __webpack_require__(796); +const provider_1 = __webpack_require__(798); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -92755,14 +92676,14 @@ exports.default = ProviderStream; /***/ }), -/* 807 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(808); -const provider_1 = __webpack_require__(799); +const sync_1 = __webpack_require__(807); +const provider_1 = __webpack_require__(798); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -92785,7 +92706,7 @@ exports.default = ProviderSync; /***/ }), -/* 808 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92793,7 +92714,7 @@ exports.default = ProviderSync; Object.defineProperty(exports, "__esModule", { value: true }); const fsStat = __webpack_require__(289); const fsWalk = __webpack_require__(294); -const reader_1 = __webpack_require__(798); +const reader_1 = __webpack_require__(797); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -92835,7 +92756,7 @@ exports.default = ReaderSync; /***/ }), -/* 809 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92899,7 +92820,7 @@ exports.default = Settings; /***/ }), -/* 810 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92907,7 +92828,7 @@ exports.default = Settings; const {promisify} = __webpack_require__(113); const fs = __webpack_require__(132); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(779); +const fastGlob = __webpack_require__(778); const gitIgnore = __webpack_require__(329); const slash = __webpack_require__(330); @@ -93026,7 +92947,7 @@ module.exports.sync = options => { /***/ }), -/* 811 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93079,7 +93000,7 @@ module.exports = { /***/ }), -/* 812 */ +/* 811 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -93087,13 +93008,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return buildNonBazelProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getProductionProjects", function() { return getProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProject", function() { return buildProject; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(566); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(565); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(240); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(563); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(562); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(220); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(343); diff --git a/packages/kbn-pm/src/commands/index.ts b/packages/kbn-pm/src/commands/index.ts index 70e641f1e93518..4c7992859ebdd9 100644 --- a/packages/kbn-pm/src/commands/index.ts +++ b/packages/kbn-pm/src/commands/index.ts @@ -32,7 +32,6 @@ import { CleanCommand } from './clean'; import { ResetCommand } from './reset'; import { RunCommand } from './run'; import { WatchCommand } from './watch'; -import { PatchNativeModulesCommand } from './patch_native_modules'; import { Kibana } from '../utils/kibana'; export const commands: { [key: string]: ICommand } = { @@ -42,5 +41,4 @@ export const commands: { [key: string]: ICommand } = { reset: ResetCommand, run: RunCommand, watch: WatchCommand, - patch_native_modules: PatchNativeModulesCommand, }; diff --git a/packages/kbn-pm/src/commands/patch_native_modules.ts b/packages/kbn-pm/src/commands/patch_native_modules.ts deleted file mode 100644 index 30fd599b83be3c..00000000000000 --- a/packages/kbn-pm/src/commands/patch_native_modules.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 Path from 'path'; -import Fs from 'fs'; - -import { CiStatsReporter } from '@kbn/dev-utils/ci_stats_reporter'; - -import { log } from '../utils/log'; -import { spawn } from '../utils/child_process'; -import { ICommand } from './index'; - -export const PatchNativeModulesCommand: ICommand = { - description: 'Patch native modules by running build commands on M1 Macs', - name: 'patch_native_modules', - - async run(projects, _, { kbn }) { - const kibanaProjectPath = projects.get('kibana')?.path || ''; - const reporter = CiStatsReporter.fromEnv(log); - - if (process.platform !== 'darwin' || process.arch !== 'arm64') { - return; - } - - const startTime = Date.now(); - const nodeSassDir = Path.resolve(kibanaProjectPath, 'node_modules/node-sass'); - const nodeSassNativeDist = Path.resolve( - nodeSassDir, - `vendor/darwin-arm64-${process.versions.modules}/binding.node` - ); - if (!Fs.existsSync(nodeSassNativeDist)) { - log.info('Running build script for node-sass'); - await spawn('npm', ['run', 'build'], { - cwd: nodeSassDir, - }); - } - - const re2Dir = Path.resolve(kibanaProjectPath, 'node_modules/re2'); - const re2NativeDist = Path.resolve(re2Dir, 'build/Release/re2.node'); - if (!Fs.existsSync(re2NativeDist)) { - log.info('Running build script for re2'); - await spawn('npm', ['run', 'rebuild'], { - cwd: re2Dir, - }); - } - - log.success('native modules should be setup for native ARM Mac development'); - - // send timings - await reporter.timings({ - upstreamBranch: kbn.kibanaProject.json.branch, - // prevent loading @kbn/utils by passing null - kibanaUuid: kbn.getUuid() || null, - timings: [ - { - group: 'scripts/kbn bootstrap', - id: 'patch native modudles for arm macs', - ms: Date.now() - startTime, - }, - ], - }); - }, -}; diff --git a/packages/kbn-pm/src/utils/scripts.ts b/packages/kbn-pm/src/utils/scripts.ts index ab013495a326a1..6e99285e145f82 100644 --- a/packages/kbn-pm/src/utils/scripts.ts +++ b/packages/kbn-pm/src/utils/scripts.ts @@ -21,6 +21,12 @@ export async function installInDir(directory: string, extraArgs: string[] = []) // given time (e.g. to avoid conflicts). await spawn(YARN_EXEC, options, { cwd: directory, + env: { + SASS_BINARY_SITE: + 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-sass', + RE2_DOWNLOAD_MIRROR: + 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2', + }, }); } diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 9dad901f5eb272..ba515865e53230 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -9,6 +9,8 @@ // For a detailed explanation regarding each configuration property, visit: // https://jestjs.io/docs/en/configuration.html +/** @typedef {import("@jest/types").Config.InitialOptions} JestConfig */ +/** @type {JestConfig} */ module.exports = { // The directory where Jest should output its coverage files coverageDirectory: '/target/kibana-coverage/jest', @@ -128,4 +130,6 @@ module.exports = { // A custom resolver to preserve symlinks by default resolver: '/node_modules/@kbn/test/target_node/jest/setup/preserve_symlinks_resolver.js', + + watchPathIgnorePatterns: ['.*/__tmp__/.*'], }; diff --git a/packages/kbn-test/src/jest/run.ts b/packages/kbn-test/src/jest/run.ts index 26fa12497357c6..b8617317d6d9f9 100644 --- a/packages/kbn-test/src/jest/run.ts +++ b/packages/kbn-test/src/jest/run.ts @@ -22,6 +22,7 @@ import { existsSync } from 'fs'; import { run } from 'jest'; import { buildArgv } from 'jest-cli/build/cli'; import { ToolingLog, getTimeReporter } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { map } from 'lodash'; // yarn test:jest src/core/server/saved_objects @@ -67,23 +68,47 @@ export function runJest(configName = 'jest.config.js') { log.verbose('commonTestFiles:', commonTestFiles); let configPath; - let devConfigPath; // sets the working directory to the cwd or the common // base directory of the provided test files let wd = testFilesProvided ? commonTestFiles : cwd; - - devConfigPath = resolve(wd, devConfigName); - configPath = resolve(wd, configName); - - while (!existsSync(configPath) && !existsSync(devConfigPath)) { - wd = resolve(wd, '..'); - devConfigPath = resolve(wd, devConfigName); - configPath = resolve(wd, configName); + while (true) { + const dev = resolve(wd, devConfigName); + if (existsSync(dev)) { + configPath = dev; + break; + } + + const actual = resolve(wd, configName); + if (existsSync(actual)) { + configPath = actual; + break; + } + + if (wd === REPO_ROOT) { + break; + } + + const parent = resolve(wd, '..'); + if (parent === wd) { + break; + } + + wd = parent; } - if (existsSync(devConfigPath)) { - configPath = devConfigPath; + if (!configPath) { + if (testFilesProvided) { + log.error( + `unable to find a ${configName} file in ${commonTestFiles} or any parent directory up to the root of the repo. This CLI can only run Jest tests which resolve to a single ${configName} file, and that file must exist in a parent directory of all the paths you pass.` + ); + } else { + log.error( + `we no longer ship a root config file so you either need to pass a path to a test file, a folder where tests can be found, or a --config argument pointing to one of the many ${configName} files in the repository` + ); + } + + process.exit(1); } log.verbose(`no config provided, found ${configPath}`); diff --git a/packages/kbn-type-summarizer/BUILD.bazel b/packages/kbn-type-summarizer/BUILD.bazel new file mode 100644 index 00000000000000..13a89e0669b80f --- /dev/null +++ b/packages/kbn-type-summarizer/BUILD.bazel @@ -0,0 +1,136 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") +load("@build_bazel_rules_nodejs//internal/node:node.bzl", "nodejs_binary") +load("@build_bazel_rules_nodejs//:index.bzl", "directory_file_path") + +PKG_BASE_NAME = "kbn-type-summarizer" +PKG_REQUIRE_NAME = "@kbn/type-summarizer" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +RUNTIME_DEPS = [ + "@npm//@babel/runtime", + "@npm//@microsoft/api-extractor", + "@npm//source-map-support", + "@npm//chalk", + "@npm//getopts", + "@npm//is-path-inside", + "@npm//normalize-path", + "@npm//source-map", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@microsoft/api-extractor", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/normalize-path", + "@npm//getopts", + "@npm//is-path-inside", + "@npm//normalize-path", + "@npm//source-map", + "@npm//tslib", +] + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +directory_file_path( + name = "bazel-cli-path", + directory = ":target_node", + path = "bazel_cli.js", +) + +nodejs_binary( + name = "bazel-cli", + data = [ + ":%s" % PKG_BASE_NAME + ], + entry_point = ":bazel-cli-path", + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ], +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-type-summarizer/README.md b/packages/kbn-type-summarizer/README.md new file mode 100644 index 00000000000000..fdd58886a0a691 --- /dev/null +++ b/packages/kbn-type-summarizer/README.md @@ -0,0 +1,17 @@ +# @kbn/type-summarizer + +Consume the .d.ts files for a package, produced by `tsc`, and generate a single `.d.ts` file of the public types along with a source map that points back to the original source. + +## You mean like API Extractor? + +Yeah, except with source map support and without all the legacy features and other features we disable to generate our current type summaries. + +I first attempted to implement this in api-extractor but I (@spalger) hit a wall when dealing with the `Span` class. This class handles all the text output which ends up becoming source code, and I wasn't able to find a way to associate specific spans with source locations without getting 12 headaches. Instead I decided to try implementing this from scratch, reducing our reliance on the api-extractor project and putting us in control of how we generate type summaries. + +This package is missing some critical features for wider adoption, but rather than build the entire product in a branch I decided to implement support for a small number of TS features and put this to use in the `@kbn/crypto` module ASAP. + +The plan is to expand to other packages in the Kibana repo, adding support for language features as we go. + +## Something isn't working and I'm blocked! + +If there's a problem with the implmentation blocking another team at any point we can move the package back to using api-extractor by removing the package from the `TYPE_SUMMARIZER_PACKAGES` list at the top of [packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts](./src/lib/bazel_cli_config.ts). \ No newline at end of file diff --git a/packages/kbn-type-summarizer/jest.config.js b/packages/kbn-type-summarizer/jest.config.js new file mode 100644 index 00000000000000..84b10626e82c87 --- /dev/null +++ b/packages/kbn-type-summarizer/jest.config.js @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** @typedef {import("@jest/types").Config.InitialOptions} JestConfig */ +/** @type {JestConfig} */ +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-type-summarizer'], +}; diff --git a/packages/kbn-type-summarizer/jest.integration.config.js b/packages/kbn-type-summarizer/jest.integration.config.js new file mode 100644 index 00000000000000..ae7b80073b9359 --- /dev/null +++ b/packages/kbn-type-summarizer/jest.integration.config.js @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** @typedef {import("@jest/types").Config.InitialOptions} JestConfig */ +/** @type {JestConfig} */ +module.exports = { + preset: '@kbn/test/jest_integration_node', + rootDir: '../..', + roots: ['/packages/kbn-type-summarizer'], +}; diff --git a/packages/kbn-type-summarizer/package.json b/packages/kbn-type-summarizer/package.json new file mode 100644 index 00000000000000..531928ce788429 --- /dev/null +++ b/packages/kbn-type-summarizer/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/type-summarizer", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target_node/index.js", + "private": true +} diff --git a/packages/kbn-type-summarizer/src/bazel_cli.ts b/packages/kbn-type-summarizer/src/bazel_cli.ts new file mode 100644 index 00000000000000..af6b13ebfc09ca --- /dev/null +++ b/packages/kbn-type-summarizer/src/bazel_cli.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 Fsp from 'fs/promises'; +import Path from 'path'; + +import { run } from './lib/run'; +import { parseBazelCliConfig } from './lib/bazel_cli_config'; + +import { summarizePackage } from './summarize_package'; +import { runApiExtractor } from './run_api_extractor'; + +const HELP = ` +Script called from bazel to create the summarized version of a package. When called by Bazel +config is passed as a JSON encoded object. + +When called via "node scripts/build_type_summarizer_output" pass a path to a package and that +package's types will be read from node_modules and written to data/type-summarizer-output. + +`; + +run( + async ({ argv, log }) => { + log.debug('cwd:', process.cwd()); + log.debug('argv', process.argv); + + const config = parseBazelCliConfig(argv); + await Fsp.mkdir(config.outputDir, { recursive: true }); + + // generate pkg json output + await Fsp.writeFile( + Path.resolve(config.outputDir, 'package.json'), + JSON.stringify( + { + name: `@types/${config.packageName.replaceAll('@', '').replaceAll('/', '__')}`, + description: 'Generated by @kbn/type-summarizer', + types: './index.d.ts', + private: true, + license: 'MIT', + version: '1.1.0', + }, + null, + 2 + ) + ); + + if (config.use === 'type-summarizer') { + await summarizePackage(log, { + dtsDir: Path.dirname(config.inputPath), + inputPaths: [config.inputPath], + outputDir: config.outputDir, + tsconfigPath: config.tsconfigPath, + repoRelativePackageDir: config.repoRelativePackageDir, + }); + log.success('type summary created for', config.repoRelativePackageDir); + } else { + await runApiExtractor( + config.tsconfigPath, + config.inputPath, + Path.resolve(config.outputDir, 'index.d.ts') + ); + } + }, + { + helpText: HELP, + defaultLogLevel: 'quiet', + } +); diff --git a/packages/kbn-type-summarizer/src/index.ts b/packages/kbn-type-summarizer/src/index.ts new file mode 100644 index 00000000000000..1667ab5cd8d2fb --- /dev/null +++ b/packages/kbn-type-summarizer/src/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { Logger } from './lib/log'; +export type { SummarizePacakgeOptions } from './summarize_package'; +export { summarizePackage } from './summarize_package'; diff --git a/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts b/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts new file mode 100644 index 00000000000000..a0fdb3e4685b1f --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts @@ -0,0 +1,151 @@ +/* + * Copyright 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 Path from 'path'; +import Fs from 'fs'; + +import { CliError } from './cli_error'; +import { parseCliFlags } from './cli_flags'; + +const TYPE_SUMMARIZER_PACKAGES = ['@kbn/type-summarizer', '@kbn/crypto']; + +const isString = (i: any): i is string => typeof i === 'string' && i.length > 0; + +interface BazelCliConfig { + packageName: string; + outputDir: string; + tsconfigPath: string; + inputPath: string; + repoRelativePackageDir: string; + use: 'api-extractor' | 'type-summarizer'; +} + +export function parseBazelCliFlags(argv: string[]): BazelCliConfig { + const { rawFlags, unknownFlags } = parseCliFlags(argv, { + string: ['use'], + default: { + use: 'api-extractor', + }, + }); + + if (unknownFlags.length) { + throw new CliError(`Unknown flags: ${unknownFlags.join(', ')}`, { + showHelp: true, + }); + } + + let REPO_ROOT; + try { + const name = 'utils'; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const utils = require('@kbn/' + name); + REPO_ROOT = utils.REPO_ROOT as string; + } catch (error) { + if (error && error.code === 'MODULE_NOT_FOUND') { + throw new CliError('type-summarizer bazel cli only works after bootstrap'); + } + + throw error; + } + + const [relativePackagePath, ...extraPositional] = rawFlags._; + if (typeof relativePackagePath !== 'string') { + throw new CliError(`missing path to package as first positional argument`, { showHelp: true }); + } + if (extraPositional.length) { + throw new CliError(`extra positional arguments`, { showHelp: true }); + } + + const use = rawFlags.use; + if (use !== 'api-extractor' && use !== 'type-summarizer') { + throw new CliError(`invalid --use flag, expected "api-extractor" or "type-summarizer"`); + } + + const packageDir = Path.resolve(relativePackagePath); + const packageName: string = JSON.parse( + Fs.readFileSync(Path.join(packageDir, 'package.json'), 'utf8') + ).name; + const repoRelativePackageDir = Path.relative(REPO_ROOT, packageDir); + + return { + use, + packageName, + tsconfigPath: Path.join(REPO_ROOT, repoRelativePackageDir, 'tsconfig.json'), + inputPath: Path.resolve(REPO_ROOT, 'node_modules', packageName, 'target_types/index.d.ts'), + repoRelativePackageDir, + outputDir: Path.resolve(REPO_ROOT, 'data/type-summarizer-output', use), + }; +} + +export function parseBazelCliJson(json: string): BazelCliConfig { + let config; + try { + config = JSON.parse(json); + } catch (error) { + throw new CliError('unable to parse first positional argument as JSON'); + } + + if (typeof config !== 'object' || config === null) { + throw new CliError('config JSON must be an object'); + } + + const packageName = config.packageName; + if (!isString(packageName)) { + throw new CliError('packageName config must be a non-empty string'); + } + + const outputDir = config.outputDir; + if (!isString(outputDir)) { + throw new CliError('outputDir config must be a non-empty string'); + } + if (Path.isAbsolute(outputDir)) { + throw new CliError(`outputDir [${outputDir}] must be a relative path`); + } + + const tsconfigPath = config.tsconfigPath; + if (!isString(tsconfigPath)) { + throw new CliError('tsconfigPath config must be a non-empty string'); + } + if (Path.isAbsolute(tsconfigPath)) { + throw new CliError(`tsconfigPath [${tsconfigPath}] must be a relative path`); + } + + const inputPath = config.inputPath; + if (!isString(inputPath)) { + throw new CliError('inputPath config must be a non-empty string'); + } + if (Path.isAbsolute(inputPath)) { + throw new CliError(`inputPath [${inputPath}] must be a relative path`); + } + + const buildFilePath = config.buildFilePath; + if (!isString(buildFilePath)) { + throw new CliError('buildFilePath config must be a non-empty string'); + } + if (Path.isAbsolute(buildFilePath)) { + throw new CliError(`buildFilePath [${buildFilePath}] must be a relative path`); + } + + const repoRelativePackageDir = Path.dirname(buildFilePath); + + return { + packageName, + outputDir: Path.resolve(outputDir), + tsconfigPath: Path.resolve(tsconfigPath), + inputPath: Path.resolve(inputPath), + repoRelativePackageDir, + use: TYPE_SUMMARIZER_PACKAGES.includes(packageName) ? 'type-summarizer' : 'api-extractor', + }; +} + +export function parseBazelCliConfig(argv: string[]) { + if (typeof argv[0] === 'string' && argv[0].startsWith('{')) { + return parseBazelCliJson(argv[0]); + } + return parseBazelCliFlags(argv); +} diff --git a/packages/kbn-type-summarizer/src/lib/cli_error.ts b/packages/kbn-type-summarizer/src/lib/cli_error.ts new file mode 100644 index 00000000000000..143d790612f619 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/cli_error.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface CliErrorOptions { + exitCode?: number; + showHelp?: boolean; +} + +export class CliError extends Error { + public readonly exitCode: number; + public readonly showHelp: boolean; + + constructor(message: string, options: CliErrorOptions = {}) { + super(message); + + this.exitCode = options.exitCode ?? 1; + this.showHelp = options.showHelp ?? false; + } +} diff --git a/packages/kbn-type-summarizer/src/lib/cli_flags.ts b/packages/kbn-type-summarizer/src/lib/cli_flags.ts new file mode 100644 index 00000000000000..0f616dca873beb --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/cli_flags.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 getopts from 'getopts'; + +interface ParseCliFlagsOptions { + alias?: Record; + boolean?: string[]; + string?: string[]; + default?: Record; +} + +export function parseCliFlags(argv = process.argv.slice(2), options: ParseCliFlagsOptions = {}) { + const unknownFlags: string[] = []; + + const string = options.string ?? []; + const boolean = ['help', 'verbose', 'debug', 'quiet', 'silent', ...(options.boolean ?? [])]; + const alias = { + v: 'verbose', + d: 'debug', + h: 'help', + ...options.alias, + }; + + const rawFlags = getopts(argv, { + alias, + boolean, + string, + default: options.default, + unknown(name) { + unknownFlags.push(name); + return false; + }, + }); + + return { + rawFlags, + unknownFlags, + }; +} diff --git a/packages/kbn-type-summarizer/src/lib/export_collector/collector_results.ts b/packages/kbn-type-summarizer/src/lib/export_collector/collector_results.ts new file mode 100644 index 00000000000000..f8f4e131f83868 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/export_collector/collector_results.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as ts from 'typescript'; +import { ValueNode, ExportFromDeclaration } from '../ts_nodes'; +import { ResultValue } from './result_value'; +import { ImportedSymbols } from './imported_symbols'; +import { Reference, ReferenceKey } from './reference'; +import { SourceMapper } from '../source_mapper'; + +export type CollectorResult = Reference | ImportedSymbols | ResultValue; + +export class CollectorResults { + imports: ImportedSymbols[] = []; + importsByPath = new Map(); + + nodes: ResultValue[] = []; + nodesByAst = new Map(); + + constructor(private readonly sourceMapper: SourceMapper) {} + + addNode(exported: boolean, node: ValueNode) { + const existing = this.nodesByAst.get(node); + if (existing) { + existing.exported = existing.exported || exported; + return; + } + + const result = new ResultValue(exported, node); + this.nodesByAst.set(node, result); + this.nodes.push(result); + } + + ensureExported(node: ValueNode) { + this.addNode(true, node); + } + + addImport( + exported: boolean, + node: ts.ImportDeclaration | ExportFromDeclaration, + symbol: ts.Symbol + ) { + const literal = node.moduleSpecifier; + if (!ts.isStringLiteral(literal)) { + throw new Error('import statement with non string literal module identifier'); + } + + const existing = this.importsByPath.get(literal.text); + if (existing) { + existing.symbols.push(symbol); + return; + } + + const result = new ImportedSymbols(exported, node, [symbol]); + this.importsByPath.set(literal.text, result); + this.imports.push(result); + } + + private getReferencesFromNodes() { + // collect the references from all the sourcefiles of all the resulting nodes + const sourceFiles = new Set(); + for (const { node } of this.nodes) { + sourceFiles.add(this.sourceMapper.getSourceFile(node)); + } + + const references: Record> = { + lib: new Set(), + types: new Set(), + }; + for (const sourceFile of sourceFiles) { + for (const ref of sourceFile.libReferenceDirectives) { + references.lib.add(ref.fileName); + } + for (const ref of sourceFile.typeReferenceDirectives) { + references.types.add(ref.fileName); + } + } + + return [ + ...Array.from(references.lib).map((name) => new Reference('lib', name)), + ...Array.from(references.types).map((name) => new Reference('types', name)), + ]; + } + + getAll(): CollectorResult[] { + return [...this.getReferencesFromNodes(), ...this.imports, ...this.nodes]; + } +} diff --git a/packages/kbn-type-summarizer/src/lib/export_collector/exports_collector.ts b/packages/kbn-type-summarizer/src/lib/export_collector/exports_collector.ts new file mode 100644 index 00000000000000..3f46ceda70e1fd --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/export_collector/exports_collector.ts @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as ts from 'typescript'; + +import { Logger } from '../log'; +import { + assertExportedValueNode, + isExportedValueNode, + DecSymbol, + assertDecSymbol, + toDecSymbol, + ExportFromDeclaration, + isExportFromDeclaration, + isAliasSymbol, +} from '../ts_nodes'; + +import { ExportInfo } from '../export_info'; +import { CollectorResults } from './collector_results'; +import { SourceMapper } from '../source_mapper'; +import { isNodeModule } from '../is_node_module'; + +interface ResolvedNmImport { + type: 'import'; + node: ts.ImportDeclaration | ExportFromDeclaration; + targetPath: string; +} +interface ResolvedSymbol { + type: 'symbol'; + symbol: DecSymbol; +} + +export class ExportCollector { + constructor( + private readonly log: Logger, + private readonly typeChecker: ts.TypeChecker, + private readonly sourceFile: ts.SourceFile, + private readonly dtsDir: string, + private readonly sourceMapper: SourceMapper + ) {} + + private getParentImport( + symbol: DecSymbol + ): ts.ImportDeclaration | ExportFromDeclaration | undefined { + for (const node of symbol.declarations) { + let cursor: ts.Node = node; + while (true) { + if (ts.isImportDeclaration(cursor) || isExportFromDeclaration(cursor)) { + return cursor; + } + + if (ts.isSourceFile(cursor)) { + break; + } + + cursor = cursor.parent; + } + } + } + + private getAllChildSymbols( + node: ts.Node, + results = new Set(), + seen = new Set() + ) { + node.forEachChild((child) => { + const childSymbol = this.typeChecker.getSymbolAtLocation(child); + if (childSymbol) { + results.add(toDecSymbol(childSymbol)); + } + if (!seen.has(child)) { + seen.add(child); + this.getAllChildSymbols(child, results, seen); + } + }); + return results; + } + + private resolveAliasSymbolStep(alias: ts.Symbol): DecSymbol { + // get the symbol this symbol references + const aliased = this.typeChecker.getImmediateAliasedSymbol(alias); + if (!aliased) { + throw new Error(`symbol [${alias.escapedName}] is an alias without aliased symbol`); + } + assertDecSymbol(aliased); + return aliased; + } + + private getImportFromNodeModules(symbol: DecSymbol): undefined | ResolvedNmImport { + const parentImport = this.getParentImport(symbol); + if (parentImport) { + // this symbol is within an import statement, is it an import from a node_module? + const aliased = this.resolveAliasSymbolStep(symbol); + + // symbol is in an import or export-from statement, make sure we want to traverse to that file + const targetPaths = [ + ...new Set(aliased.declarations.map((d) => this.sourceMapper.getSourceFile(d).fileName)), + ]; + + if (targetPaths.length > 1) { + throw new Error('importing a symbol from multiple locations is unsupported at this time'); + } + + const targetPath = targetPaths[0]; + if (isNodeModule(this.dtsDir, targetPath)) { + return { + type: 'import', + node: parentImport, + targetPath, + }; + } + } + } + + private resolveAliasSymbol(alias: DecSymbol): ResolvedNmImport | ResolvedSymbol { + let symbol = alias; + + while (isAliasSymbol(symbol)) { + const nmImport = this.getImportFromNodeModules(symbol); + if (nmImport) { + return nmImport; + } + + symbol = this.resolveAliasSymbolStep(symbol); + } + + return { + type: 'symbol', + symbol, + }; + } + + private traversedSymbols = new Set(); + private collectResults( + results: CollectorResults, + exportInfo: ExportInfo | undefined, + symbol: DecSymbol + ): void { + const seen = this.traversedSymbols.has(symbol); + if (seen && !exportInfo) { + return; + } + this.traversedSymbols.add(symbol); + + const source = this.resolveAliasSymbol(symbol); + if (source.type === 'import') { + results.addImport(!!exportInfo, source.node, symbol); + return; + } + + symbol = source.symbol; + if (seen) { + for (const node of symbol.declarations) { + assertExportedValueNode(node); + results.ensureExported(node); + } + return; + } + + const globalDecs: ts.Declaration[] = []; + const localDecs: ts.Declaration[] = []; + for (const node of symbol.declarations) { + const sourceFile = this.sourceMapper.getSourceFile(node); + (isNodeModule(this.dtsDir, sourceFile.fileName) ? globalDecs : localDecs).push(node); + } + + if (globalDecs.length) { + this.log.debug( + `Ignoring ${globalDecs.length} global declarations for "${source.symbol.escapedName}"` + ); + } + + for (const node of localDecs) { + // iterate through the child nodes to find nodes we need to export to make this useful + const childSymbols = this.getAllChildSymbols(node); + childSymbols.delete(symbol); + + for (const childSymbol of childSymbols) { + this.collectResults(results, undefined, childSymbol); + } + + if (isExportedValueNode(node)) { + results.addNode(!!exportInfo, node); + } + } + } + + run(): CollectorResults { + const results = new CollectorResults(this.sourceMapper); + + const moduleSymbol = this.typeChecker.getSymbolAtLocation(this.sourceFile); + if (!moduleSymbol) { + this.log.warn('Source file has no symbol in the type checker, is it empty?'); + return results; + } + + for (const symbol of this.typeChecker.getExportsOfModule(moduleSymbol)) { + assertDecSymbol(symbol); + this.collectResults(results, new ExportInfo(`${symbol.escapedName}`), symbol); + } + + return results; + } +} diff --git a/packages/kbn-type-summarizer/src/lib/export_collector/imported_symbols.ts b/packages/kbn-type-summarizer/src/lib/export_collector/imported_symbols.ts new file mode 100644 index 00000000000000..1c9fa800baaab2 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/export_collector/imported_symbols.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as ts from 'typescript'; +import { ExportFromDeclaration } from '../ts_nodes'; + +export class ImportedSymbols { + type = 'import' as const; + + constructor( + public readonly exported: boolean, + public readonly importNode: ts.ImportDeclaration | ExportFromDeclaration, + // TODO: I'm going to need to keep track of local names for these... unless that's embedded in the symbols + public readonly symbols: ts.Symbol[] + ) {} +} diff --git a/packages/kbn-type-summarizer/src/lib/export_collector/index.ts b/packages/kbn-type-summarizer/src/lib/export_collector/index.ts new file mode 100644 index 00000000000000..87f6630d2fcfac --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/export_collector/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './exports_collector'; +export * from './collector_results'; diff --git a/packages/kbn-type-summarizer/src/lib/export_collector/reference.ts b/packages/kbn-type-summarizer/src/lib/export_collector/reference.ts new file mode 100644 index 00000000000000..b664a457a24ada --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/export_collector/reference.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type ReferenceKey = 'types' | 'lib'; + +export class Reference { + type = 'reference' as const; + constructor(public readonly key: ReferenceKey, public readonly name: string) {} +} diff --git a/src/plugins/vis_types/vislib/public/vis_type_vislib_vis_types.ts b/packages/kbn-type-summarizer/src/lib/export_collector/result_value.ts similarity index 51% rename from src/plugins/vis_types/vislib/public/vis_type_vislib_vis_types.ts rename to packages/kbn-type-summarizer/src/lib/export_collector/result_value.ts index 220c69afb21d2a..91249eea68e140 100644 --- a/src/plugins/vis_types/vislib/public/vis_type_vislib_vis_types.ts +++ b/packages/kbn-type-summarizer/src/lib/export_collector/result_value.ts @@ -6,13 +6,10 @@ * Side Public License, v 1. */ -import { VisTypeDefinition } from 'src/plugins/visualizations/public'; -import { gaugeVisTypeDefinition } from './gauge'; -import { goalVisTypeDefinition } from './goal'; +import { ValueNode } from '../ts_nodes'; -export { pieVisTypeDefinition } from './pie'; +export class ResultValue { + type = 'value' as const; -export const visLibVisTypeDefinitions: Array> = [ - gaugeVisTypeDefinition, - goalVisTypeDefinition, -]; + constructor(public exported: boolean, public readonly node: ValueNode) {} +} diff --git a/packages/kbn-type-summarizer/src/lib/export_info.ts b/packages/kbn-type-summarizer/src/lib/export_info.ts new file mode 100644 index 00000000000000..3dee04121d3225 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/export_info.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export class ExportInfo { + constructor(public readonly name: string) {} +} diff --git a/packages/kbn-type-summarizer/src/lib/helpers/error.ts b/packages/kbn-type-summarizer/src/lib/helpers/error.ts new file mode 100644 index 00000000000000..f78eb29083b04e --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/helpers/error.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export function toError(thrown: unknown) { + if (thrown instanceof Error) { + return thrown; + } + + return new Error(`${thrown} thrown`); +} + +export function isSystemError(error: Error): error is NodeJS.ErrnoException { + return typeof (error as any).code === 'string'; +} diff --git a/packages/kbn-type-summarizer/src/lib/helpers/fs.ts b/packages/kbn-type-summarizer/src/lib/helpers/fs.ts new file mode 100644 index 00000000000000..092310c1e5db08 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/helpers/fs.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 Fsp from 'fs/promises'; +import { toError, isSystemError } from './error'; + +export async function tryReadFile( + path: string, + encoding: 'utf-8' | 'utf8' +): Promise; +export async function tryReadFile(path: string, encoding?: BufferEncoding) { + try { + return await Fsp.readFile(path, encoding); + } catch (_) { + const error = toError(_); + if (isSystemError(error) && error.code === 'ENOENT') { + return undefined; + } + throw error; + } +} diff --git a/packages/kbn-type-summarizer/src/lib/helpers/json.test.ts b/packages/kbn-type-summarizer/src/lib/helpers/json.test.ts new file mode 100644 index 00000000000000..4bb86652221d9e --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/helpers/json.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { parseJson } from './json'; + +it('parses JSON', () => { + expect(parseJson('{"foo": "bar"}')).toMatchInlineSnapshot(` + Object { + "foo": "bar", + } + `); +}); + +it('throws more helpful errors', () => { + expect(() => parseJson('{"foo": bar}')).toThrowErrorMatchingInlineSnapshot( + `"Failed to parse JSON: Unexpected token b in JSON at position 8"` + ); +}); diff --git a/packages/kbn-type-summarizer/src/lib/helpers/json.ts b/packages/kbn-type-summarizer/src/lib/helpers/json.ts new file mode 100644 index 00000000000000..ee2403bd9422cb --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/helpers/json.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { toError } from './error'; + +export function parseJson(json: string, from?: string) { + try { + return JSON.parse(json); + } catch (_) { + const error = toError(_); + throw new Error(`Failed to parse JSON${from ? ` from ${from}` : ''}: ${error.message}`); + } +} diff --git a/packages/kbn-type-summarizer/src/lib/is_node_module.ts b/packages/kbn-type-summarizer/src/lib/is_node_module.ts new file mode 100644 index 00000000000000..67efde569a1b4e --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/is_node_module.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 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 Path from 'path'; + +import isPathInside from 'is-path-inside'; + +export function isNodeModule(dtsDir: string, path: string) { + return (isPathInside(path, dtsDir) ? Path.relative(dtsDir, path) : path) + .split(Path.sep) + .includes('node_modules'); +} diff --git a/packages/kbn-type-summarizer/src/lib/log/cli_log.ts b/packages/kbn-type-summarizer/src/lib/log/cli_log.ts new file mode 100644 index 00000000000000..1121dfae3606a1 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/log/cli_log.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 { format } from 'util'; +import { dim, blueBright, yellowBright, redBright, gray } from 'chalk'; +import getopts from 'getopts'; + +import { Logger } from './logger'; + +const LOG_LEVEL_RANKS = { + silent: 0, + quiet: 1, + info: 2, + debug: 3, + verbose: 4, +}; +export type LogLevel = keyof typeof LOG_LEVEL_RANKS; +const LOG_LEVELS = (Object.keys(LOG_LEVEL_RANKS) as LogLevel[]).sort( + (a, b) => LOG_LEVEL_RANKS[a] - LOG_LEVEL_RANKS[b] +); +const LOG_LEVELS_DESC = LOG_LEVELS.slice().reverse(); + +type LogLevelMap = { [k in LogLevel]: boolean }; + +export interface LogWriter { + write(chunk: string): void; +} + +export class CliLog implements Logger { + static parseLogLevel(level: LogLevel) { + if (!LOG_LEVELS.includes(level)) { + throw new Error('invalid log level'); + } + + const rank = LOG_LEVEL_RANKS[level]; + return Object.fromEntries( + LOG_LEVELS.map((l) => [l, LOG_LEVEL_RANKS[l] <= rank]) + ) as LogLevelMap; + } + + static pickLogLevelFromFlags( + flags: getopts.ParsedOptions, + defaultLogLevl: LogLevel = 'info' + ): LogLevel { + for (const level of LOG_LEVELS_DESC) { + if (Object.prototype.hasOwnProperty.call(flags, level) && flags[level] === true) { + return level; + } + } + + return defaultLogLevl; + } + + private readonly map: LogLevelMap; + constructor(public readonly level: LogLevel, private readonly writeTo: LogWriter) { + this.map = CliLog.parseLogLevel(level); + } + + info(msg: string, ...args: any[]) { + if (this.map.info) { + this.writeTo.write(`${blueBright('info')} ${format(msg, ...args)}\n`); + } + } + + warn(msg: string, ...args: any[]) { + if (this.map.quiet) { + this.writeTo.write(`${yellowBright('warning')} ${format(msg, ...args)}\n`); + } + } + + error(msg: string, ...args: any[]) { + if (this.map.quiet) { + this.writeTo.write(`${redBright('error')} ${format(msg, ...args)}\n`); + } + } + + debug(msg: string, ...args: any[]) { + if (this.map.debug) { + this.writeTo.write(`${gray('debug')} ${format(msg, ...args)}\n`); + } + } + + verbose(msg: string, ...args: any[]) { + if (this.map.verbose) { + this.writeTo.write(`${dim('verbose')}: ${format(msg, ...args)}\n`); + } + } + + success(msg: string, ...args: any[]): void { + if (this.map.quiet) { + this.writeTo.write(`✅ ${format(msg, ...args)}\n`); + } + } +} diff --git a/packages/kbn-type-summarizer/src/lib/log/index.ts b/packages/kbn-type-summarizer/src/lib/log/index.ts new file mode 100644 index 00000000000000..68a37528d49767 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/log/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './logger'; +export * from './cli_log'; +export * from './test_log'; diff --git a/packages/kbn-type-summarizer/src/lib/log/logger.ts b/packages/kbn-type-summarizer/src/lib/log/logger.ts new file mode 100644 index 00000000000000..76cb7fe525f6d8 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/log/logger.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +/** + * Logger interface used by @kbn/type-summarizer + */ +export interface Logger { + /** + * Write a message to the log with the level "info" + * @param msg any message + * @param args any serializeable values you would like to be appended to the log message + */ + info(msg: string, ...args: any[]): void; + /** + * Write a message to the log with the level "warn" + * @param msg any message + * @param args any serializeable values you would like to be appended to the log message + */ + warn(msg: string, ...args: any[]): void; + /** + * Write a message to the log with the level "error" + * @param msg any message + * @param args any serializeable values you would like to be appended to the log message + */ + error(msg: string, ...args: any[]): void; + /** + * Write a message to the log with the level "debug" + * @param msg any message + * @param args any serializeable values you would like to be appended to the log message + */ + debug(msg: string, ...args: any[]): void; + /** + * Write a message to the log with the level "verbose" + * @param msg any message + * @param args any serializeable values you would like to be appended to the log message + */ + verbose(msg: string, ...args: any[]): void; + /** + * Write a message to the log, only excluded in silent mode + * @param msg any message + * @param args any serializeable values you would like to be appended to the log message + */ + success(msg: string, ...args: any[]): void; +} diff --git a/packages/kbn-type-summarizer/src/lib/log/test_log.ts b/packages/kbn-type-summarizer/src/lib/log/test_log.ts new file mode 100644 index 00000000000000..5062a8cbae841d --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/log/test_log.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CliLog, LogLevel } from './cli_log'; + +export class TestLog extends CliLog { + messages: string[] = []; + constructor(level: LogLevel = 'verbose') { + super(level, { + write: (chunk) => { + this.messages.push(chunk); + }, + }); + } +} diff --git a/packages/kbn-type-summarizer/src/lib/printer.ts b/packages/kbn-type-summarizer/src/lib/printer.ts new file mode 100644 index 00000000000000..3ce675f7279275 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/printer.ts @@ -0,0 +1,362 @@ +/* + * Copyright 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 Path from 'path'; + +import * as ts from 'typescript'; +import { SourceNode, CodeWithSourceMap } from 'source-map'; + +import { findKind } from './ts_nodes'; +import { SourceMapper } from './source_mapper'; +import { CollectorResult } from './export_collector'; + +type SourceNodes = Array; +const COMMENT_TRIM = /^(\s+)(\/\*|\*|\/\/)/; + +export class Printer { + private readonly tsPrint = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + noEmitHelpers: true, + omitTrailingSemicolon: false, + removeComments: true, + }); + + constructor( + private readonly sourceMapper: SourceMapper, + private readonly results: CollectorResult[], + private readonly outputPath: string, + private readonly mapOutputPath: string, + private readonly sourceRoot: string, + private readonly strict: boolean + ) {} + + async print(): Promise { + const file = new SourceNode( + null, + null, + null, + this.results.flatMap((r) => { + if (r.type === 'reference') { + return `/// \n`; + } + + if (r.type === 'import') { + // TODO: handle default imports, imports with alternate names, etc + return `import { ${r.symbols + .map((s) => s.escapedName) + .join(', ')} } from ${r.importNode.moduleSpecifier.getText()};\n`; + } + + return this.toSourceNodes(r.node, r.exported); + }) + ); + + const outputDir = Path.dirname(this.outputPath); + const mapOutputDir = Path.dirname(this.mapOutputPath); + + const output = file.toStringWithSourceMap({ + file: Path.relative(mapOutputDir, this.outputPath), + sourceRoot: this.sourceRoot, + }); + + const nl = output.code.endsWith('\n') ? '' : '\n'; + const sourceMapPathRel = Path.relative(outputDir, this.mapOutputPath); + output.code += `${nl}//# sourceMappingURL=${sourceMapPathRel}`; + + return output; + } + + private getDeclarationKeyword(node: ts.Declaration) { + if (node.kind === ts.SyntaxKind.FunctionDeclaration) { + return 'function'; + } + + if (node.kind === ts.SyntaxKind.TypeAliasDeclaration) { + return 'type'; + } + + if (node.kind === ts.SyntaxKind.ClassDeclaration) { + return 'class'; + } + + if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { + return 'interface'; + } + + if (ts.isVariableDeclaration(node)) { + return this.getVariableDeclarationType(node); + } + } + + private printModifiers(exported: boolean, node: ts.Declaration) { + const flags = ts.getCombinedModifierFlags(node); + const modifiers: string[] = []; + if (exported) { + modifiers.push('export'); + } + if (flags & ts.ModifierFlags.Default) { + modifiers.push('default'); + } + if (flags & ts.ModifierFlags.Abstract) { + modifiers.push('abstract'); + } + if (flags & ts.ModifierFlags.Private) { + modifiers.push('private'); + } + if (flags & ts.ModifierFlags.Public) { + modifiers.push('public'); + } + if (flags & ts.ModifierFlags.Static) { + modifiers.push('static'); + } + if (flags & ts.ModifierFlags.Readonly) { + modifiers.push('readonly'); + } + if (flags & ts.ModifierFlags.Const) { + modifiers.push('const'); + } + if (flags & ts.ModifierFlags.Async) { + modifiers.push('async'); + } + + const keyword = this.getDeclarationKeyword(node); + if (keyword) { + modifiers.push(keyword); + } + + return `${modifiers.join(' ')} `; + } + + private printNode(node: ts.Node) { + return this.tsPrint.printNode( + ts.EmitHint.Unspecified, + node, + this.sourceMapper.getSourceFile(node) + ); + } + + private ensureNewline(string: string): string; + private ensureNewline(string: SourceNodes): SourceNodes; + private ensureNewline(string: string | SourceNodes): string | SourceNodes { + if (typeof string === 'string') { + return string.endsWith('\n') ? string : `${string}\n`; + } + + const end = string.at(-1); + if (end === undefined) { + return []; + } + + const valid = (typeof end === 'string' ? end : end.toString()).endsWith('\n'); + return valid ? string : [...string, '\n']; + } + + private getMappedSourceNode(node: ts.Node, code?: string) { + return this.sourceMapper.getSourceNode(node, code ?? node.getText()); + } + + private getVariableDeclarationList(node: ts.VariableDeclaration) { + const list = node.parent; + if (!ts.isVariableDeclarationList(list)) { + const kind = findKind(list); + throw new Error( + `expected parent of variable declaration to be a VariableDeclarationList, got [${kind}]` + ); + } + return list; + } + + private getVariableDeclarationType(node: ts.VariableDeclaration) { + const flags = ts.getCombinedNodeFlags(this.getVariableDeclarationList(node)); + if (flags & ts.NodeFlags.Const) { + return 'const'; + } + if (flags & ts.NodeFlags.Let) { + return 'let'; + } + return 'var'; + } + + private getSourceWithLeadingComments(node: ts.Node) { + // variable declarations regularly have leading comments but they're two-parents up, so we have to handle them separately + if (!ts.isVariableDeclaration(node)) { + return node.getFullText(); + } + + const list = this.getVariableDeclarationList(node); + if (list.declarations.length > 1) { + return node.getFullText(); + } + + const statement = list.parent; + if (!ts.isVariableStatement(statement)) { + throw new Error('expected parent of VariableDeclarationList to be a VariableStatement'); + } + + return statement.getFullText(); + } + + private getLeadingComments(node: ts.Node, indentWidth = 0): string[] { + const fullText = this.getSourceWithLeadingComments(node); + const ranges = ts.getLeadingCommentRanges(fullText, 0); + if (!ranges) { + return []; + } + const indent = ' '.repeat(indentWidth); + + return ranges.flatMap((range) => { + const comment = fullText + .slice(range.pos, range.end) + .split('\n') + .map((line) => { + const match = line.match(COMMENT_TRIM); + if (!match) { + return line; + } + + const [, spaces, type] = match; + return line.slice(type === '*' ? spaces.length - 1 : spaces.length); + }) + .map((line) => `${indent}${line}`) + .join('\n'); + + if (comment.startsWith('/// this.printNode(p)).join(', ')}>`; + } + + private toSourceNodes(node: ts.Node, exported = false): SourceNodes { + switch (node.kind) { + case ts.SyntaxKind.LiteralType: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.BigIntLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.StringKeyword: + return [this.printNode(node)]; + } + + if (ts.isFunctionDeclaration(node)) { + // we are just trying to replace the name with a sourceMapped node, so if there + // is no name just return the source + if (!node.name) { + return [node.getFullText()]; + } + + return [ + this.getLeadingComments(node), + this.printModifiers(exported, node), + this.getMappedSourceNode(node.name), + this.printTypeParameters(node), + `(${node.parameters.map((p) => p.getFullText()).join(', ')})`, + node.type ? [': ', this.printNode(node.type), ';'] : ';', + ].flat(); + } + + if (ts.isInterfaceDeclaration(node)) { + const text = node.getText(); + const name = node.name.getText(); + const nameI = text.indexOf(name); + if (nameI === -1) { + throw new Error(`printed version of interface does not include name [${name}]: ${text}`); + } + return [ + ...this.getLeadingComments(node), + text.slice(0, nameI), + this.getMappedSourceNode(node.name, name), + text.slice(nameI + name.length), + '\n', + ]; + } + + if (ts.isVariableDeclaration(node)) { + return [ + ...this.getLeadingComments(node), + this.printModifiers(exported, node), + this.getMappedSourceNode(node.name), + ...(node.type ? [': ', this.printNode(node.type)] : []), + ';\n', + ]; + } + + if (ts.isUnionTypeNode(node)) { + return node.types.flatMap((type, i) => + i > 0 ? [' | ', ...this.toSourceNodes(type)] : this.toSourceNodes(type) + ); + } + + if (ts.isTypeAliasDeclaration(node)) { + return [ + ...this.getLeadingComments(node), + this.printModifiers(exported, node), + this.getMappedSourceNode(node.name), + this.printTypeParameters(node), + ' = ', + this.ensureNewline(this.toSourceNodes(node.type)), + ].flat(); + } + + if (ts.isClassDeclaration(node)) { + return [ + ...this.getLeadingComments(node), + this.printModifiers(exported, node), + node.name ? this.getMappedSourceNode(node.name) : [], + this.printTypeParameters(node), + ' {\n', + node.members.flatMap((m) => { + const memberText = m.getText(); + + if (ts.isConstructorDeclaration(m)) { + return ` ${memberText}\n`; + } + + if (!m.name) { + return ` ${memberText}\n`; + } + + const nameText = m.name.getText(); + const pos = memberText.indexOf(nameText); + if (pos === -1) { + return ` ${memberText}\n`; + } + + const left = memberText.slice(0, pos); + const right = memberText.slice(pos + nameText.length); + const nameNode = this.getMappedSourceNode(m.name, nameText); + + return [...this.getLeadingComments(m, 2), ` `, left, nameNode, right, `\n`]; + }), + '}\n', + ].flat(); + } + + if (!this.strict) { + return [this.ensureNewline(this.printNode(node))]; + } else { + throw new Error(`unable to print export type of kind [${findKind(node)}]`); + } + } +} diff --git a/packages/kbn-type-summarizer/src/lib/run.ts b/packages/kbn-type-summarizer/src/lib/run.ts new file mode 100644 index 00000000000000..4834c4d8aae9bf --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/run.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 getopts from 'getopts'; + +import { CliLog, LogLevel } from './log'; +import { toError } from './helpers/error'; +import { CliError } from './cli_error'; + +export interface RunContext { + argv: string[]; + log: CliLog; +} + +export interface RunOptions { + helpText: string; + defaultLogLevel?: LogLevel; +} + +export async function run(main: (ctx: RunContext) => Promise, options: RunOptions) { + const argv = process.argv.slice(2); + const rawFlags = getopts(argv); + + const log = new CliLog( + CliLog.pickLogLevelFromFlags(rawFlags, options.defaultLogLevel), + process.stdout + ); + + try { + await main({ argv, log }); + } catch (_) { + const error = toError(_); + if (error instanceof CliError) { + process.exitCode = error.exitCode; + log.error(error.message); + if (error.showHelp) { + process.stdout.write(options.helpText); + } + } else { + log.error('UNHANDLED ERROR', error.stack); + process.exitCode = 1; + } + } +} diff --git a/packages/kbn-type-summarizer/src/lib/source_mapper.ts b/packages/kbn-type-summarizer/src/lib/source_mapper.ts new file mode 100644 index 00000000000000..f6075684e04a6d --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/source_mapper.ts @@ -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 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 Path from 'path'; + +import * as ts from 'typescript'; +import { SourceNode, SourceMapConsumer, BasicSourceMapConsumer } from 'source-map'; +import normalizePath from 'normalize-path'; + +import { Logger } from './log'; +import { tryReadFile } from './helpers/fs'; +import { parseJson } from './helpers/json'; +import { isNodeModule } from './is_node_module'; + +export class SourceMapper { + static async forSourceFiles( + log: Logger, + dtsDir: string, + repoRelativePackageDir: string, + sourceFiles: readonly ts.SourceFile[] + ) { + const consumers = new Map(); + + await Promise.all( + sourceFiles.map(async (sourceFile) => { + if (isNodeModule(dtsDir, sourceFile.fileName)) { + return; + } + + const text = sourceFile.getText(); + const match = text.match(/^\/\/#\s*sourceMappingURL=(.*)/im); + if (!match) { + consumers.set(sourceFile, undefined); + return; + } + + const relSourceFile = Path.relative(process.cwd(), sourceFile.fileName); + const sourceMapPath = Path.resolve(Path.dirname(sourceFile.fileName), match[1]); + const relSourceMapPath = Path.relative(process.cwd(), sourceMapPath); + const sourceJson = await tryReadFile(sourceMapPath, 'utf8'); + if (!sourceJson) { + throw new Error( + `unable to find source map for [${relSourceFile}] expected at [${match[1]}]` + ); + } + + const json = parseJson(sourceJson, `source map at [${relSourceMapPath}]`); + consumers.set(sourceFile, await new SourceMapConsumer(json)); + log.debug('loaded sourcemap for', relSourceFile); + }) + ); + + return new SourceMapper(consumers, repoRelativePackageDir); + } + + private readonly sourceFixDir: string; + constructor( + private readonly consumers: Map, + repoRelativePackageDir: string + ) { + this.sourceFixDir = Path.join('/', repoRelativePackageDir); + } + + /** + * We ensure that `sourceRoot` is not defined in the tsconfig files, and we assume that the `source` value + * for each file in the source map will be a relative path out of the bazel-out dir and to the `repoRelativePackageDir` + * or some path outside of the package in rare situations. Our goal is to convert each of these source paths + * to new path that is relative to the `repoRelativePackageDir` path. To do this we resolve the `repoRelativePackageDir` + * as if it was at the root of the filesystem, then do the same for the `source`, so both paths should be + * absolute, but only include the path segments from the root of the repo. We then get the relative path from + * the absolute version of the `repoRelativePackageDir` to the absolute version of the `source`, which should give + * us the path to the source, relative to the `repoRelativePackageDir`. + */ + fixSourcePath(source: string) { + return normalizePath(Path.relative(this.sourceFixDir, Path.join('/', source))); + } + + getSourceNode(generatedNode: ts.Node, code: string) { + const pos = this.findOriginalPosition(generatedNode); + + if (pos) { + return new SourceNode(pos.line, pos.column, pos.source, code, pos.name ?? undefined); + } + + return new SourceNode(null, null, null, code); + } + + sourceFileCache = new WeakMap(); + // abstracted so we can cache this + getSourceFile(node: ts.Node): ts.SourceFile { + if (ts.isSourceFile(node)) { + return node; + } + + const cached = this.sourceFileCache.get(node); + if (cached) { + return cached; + } + + const sourceFile = this.getSourceFile(node.parent); + this.sourceFileCache.set(node, sourceFile); + return sourceFile; + } + + findOriginalPosition(node: ts.Node) { + const dtsSource = this.getSourceFile(node); + + if (!this.consumers.has(dtsSource)) { + throw new Error(`sourceFile for [${dtsSource.fileName}] didn't have sourcemaps loaded`); + } + + const consumer = this.consumers.get(dtsSource); + if (!consumer) { + return; + } + + const posInDts = dtsSource.getLineAndCharacterOfPosition(node.getStart()); + const pos = consumer.originalPositionFor({ + /* ts line column numbers are 0 based, source map column numbers are also 0 based */ + column: posInDts.character, + /* ts line numbers are 0 based, source map line numbers are 1 based */ + line: posInDts.line + 1, + }); + + return { + ...pos, + source: pos.source ? this.fixSourcePath(pos.source) : null, + }; + } + + close() { + for (const consumer of this.consumers.values()) { + consumer?.destroy(); + } + } +} diff --git a/packages/kbn-type-summarizer/src/lib/ts_nodes.ts b/packages/kbn-type-summarizer/src/lib/ts_nodes.ts new file mode 100644 index 00000000000000..b5c03ee8c4c17b --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/ts_nodes.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as ts from 'typescript'; + +export type ValueNode = + | ts.ClassDeclaration + | ts.FunctionDeclaration + | ts.TypeAliasDeclaration + | ts.VariableDeclaration + | ts.InterfaceDeclaration; + +export function isExportedValueNode(node: ts.Node): node is ValueNode { + return ( + node.kind === ts.SyntaxKind.ClassDeclaration || + node.kind === ts.SyntaxKind.FunctionDeclaration || + node.kind === ts.SyntaxKind.TypeAliasDeclaration || + node.kind === ts.SyntaxKind.VariableDeclaration || + node.kind === ts.SyntaxKind.InterfaceDeclaration + ); +} +export function assertExportedValueNode(node: ts.Node): asserts node is ValueNode { + if (!isExportedValueNode(node)) { + const kind = findKind(node); + throw new Error(`not a valid ExportedValueNode [kind=${kind}]`); + } +} +export function toExportedNodeValue(node: ts.Node): ValueNode { + assertExportedValueNode(node); + return node; +} + +export function findKind(node: ts.Node) { + for (const [name, value] of Object.entries(ts.SyntaxKind)) { + if (node.kind === value) { + return name; + } + } + + throw new Error('node.kind is not in the SyntaxKind map'); +} + +export type DecSymbol = ts.Symbol & { + declarations: NonNullable; +}; +export function isDecSymbol(symbol: ts.Symbol): symbol is DecSymbol { + return !!symbol.declarations; +} +export function assertDecSymbol(symbol: ts.Symbol): asserts symbol is DecSymbol { + if (!isDecSymbol(symbol)) { + throw new Error('symbol has no declarations'); + } +} +export function toDecSymbol(symbol: ts.Symbol): DecSymbol { + assertDecSymbol(symbol); + return symbol; +} + +export type ExportFromDeclaration = ts.ExportDeclaration & { + moduleSpecifier: NonNullable; +}; +export function isExportFromDeclaration(node: ts.Node): node is ExportFromDeclaration { + return ts.isExportDeclaration(node) && !!node.moduleSpecifier; +} + +export function isAliasSymbol(symbol: ts.Symbol) { + return symbol.flags & ts.SymbolFlags.Alias; +} diff --git a/packages/kbn-type-summarizer/src/lib/ts_project.ts b/packages/kbn-type-summarizer/src/lib/ts_project.ts new file mode 100644 index 00000000000000..92946e3290449c --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/ts_project.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as ts from 'typescript'; + +export function createTsProject(tsConfig: ts.ParsedCommandLine, inputPaths: string[]) { + return ts.createProgram({ + rootNames: inputPaths, + options: { + ...tsConfig.options, + skipLibCheck: false, + }, + projectReferences: tsConfig.projectReferences, + }); +} diff --git a/packages/kbn-type-summarizer/src/lib/tsconfig_file.ts b/packages/kbn-type-summarizer/src/lib/tsconfig_file.ts new file mode 100644 index 00000000000000..7d327b1f03e0a3 --- /dev/null +++ b/packages/kbn-type-summarizer/src/lib/tsconfig_file.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as ts from 'typescript'; +import Path from 'path'; + +import { CliError } from './cli_error'; + +export function readTsConfigFile(path: string) { + const json = ts.readConfigFile(path, ts.sys.readFile); + + if (json.error) { + throw new CliError(`Unable to load tsconfig file: ${json.error.messageText}`); + } + + return json.config; +} + +export function loadTsConfigFile(path: string) { + return ts.parseJsonConfigFileContent(readTsConfigFile(path) ?? {}, ts.sys, Path.dirname(path)); +} diff --git a/packages/kbn-type-summarizer/src/run_api_extractor.ts b/packages/kbn-type-summarizer/src/run_api_extractor.ts new file mode 100644 index 00000000000000..0e7bae5165a4d8 --- /dev/null +++ b/packages/kbn-type-summarizer/src/run_api_extractor.ts @@ -0,0 +1,86 @@ +/* eslint-disable @kbn/eslint/require-license-header */ + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import Fsp from 'fs/promises'; +import Path from 'path'; + +import { Extractor, ExtractorConfig } from '@microsoft/api-extractor'; + +import { readTsConfigFile } from './lib/tsconfig_file'; +import { CliError } from './lib/cli_error'; + +export async function runApiExtractor( + tsconfigPath: string, + entryPath: string, + dtsBundleOutDir: string +) { + const pkgJson = Path.resolve(Path.dirname(entryPath), 'package.json'); + try { + await Fsp.writeFile( + pkgJson, + JSON.stringify({ + name: 'GENERATED-BY-BAZEL', + description: 'This is a dummy package.json as API Extractor always requires one.', + types: './index.d.ts', + private: true, + license: 'SSPL-1.0 OR Elastic License 2.0', + version: '1.0.0', + }), + { + flag: 'wx', + } + ); + } catch (error) { + if (!error.code || error.code !== 'EEXIST') { + throw error; + } + } + + // API extractor doesn't always support the version of TypeScript used in the repo + // example: at the moment it is not compatable with 3.2 + // to use the internal TypeScript we shall not create a program but rather pass a parsed tsConfig. + const extractorOptions = { + localBuild: false, + }; + + const extractorConfig = ExtractorConfig.prepare({ + configObject: { + compiler: { + overrideTsconfig: readTsConfigFile(tsconfigPath), + }, + projectFolder: Path.dirname(tsconfigPath), + mainEntryPointFilePath: entryPath, + apiReport: { + enabled: false, + // TODO(alan-agius4): remove this folder name when the below issue is solved upstream + // See: https://github.com/microsoft/web-build-tools/issues/1470 + reportFileName: 'invalid', + }, + docModel: { + enabled: false, + }, + dtsRollup: { + enabled: !!dtsBundleOutDir, + untrimmedFilePath: dtsBundleOutDir, + }, + tsdocMetadata: { + enabled: false, + }, + }, + packageJson: undefined, + packageJsonFullPath: pkgJson, + configObjectFullPath: undefined, + }); + const { succeeded } = Extractor.invoke(extractorConfig, extractorOptions); + + if (!succeeded) { + throw new CliError('api-extractor failed'); + } +} diff --git a/packages/kbn-type-summarizer/src/summarize_package.ts b/packages/kbn-type-summarizer/src/summarize_package.ts new file mode 100644 index 00000000000000..d3aac96af17725 --- /dev/null +++ b/packages/kbn-type-summarizer/src/summarize_package.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 Fsp from 'fs/promises'; +import Path from 'path'; + +import normalizePath from 'normalize-path'; + +import { SourceMapper } from './lib/source_mapper'; +import { createTsProject } from './lib/ts_project'; +import { loadTsConfigFile } from './lib/tsconfig_file'; +import { ExportCollector } from './lib/export_collector'; +import { isNodeModule } from './lib/is_node_module'; +import { Printer } from './lib/printer'; +import { Logger } from './lib/log'; + +/** + * Options used to customize the summarizePackage function + */ +export interface SummarizePacakgeOptions { + /** + * Absolute path to the directory containing the .d.ts files produced by `tsc`. Maps to the + * `declarationDir` compiler option. + */ + dtsDir: string; + /** + * Absolute path to the tsconfig.json file for the project we are summarizing + */ + tsconfigPath: string; + /** + * Array of absolute paths to the .d.ts files which will be summarized. Each file in this + * array will cause an output .d.ts summary file to be created containing all the AST nodes + * which are exported or referenced by those exports. + */ + inputPaths: string[]; + /** + * Absolute path to the output directory where the summary .d.ts files should be written + */ + outputDir: string; + /** + * Repo-relative path to the package source, for example `packages/kbn-type-summarizer` for + * this package. This is used to provide the correct `sourceRoot` path in the resulting source + * map files. + */ + repoRelativePackageDir: string; + /** + * Should the printer throw an error if it doesn't know how to print an AST node? Primarily + * used for testing + */ + strictPrinting?: boolean; +} + +/** + * Produce summary .d.ts files for a package + */ +export async function summarizePackage(log: Logger, options: SummarizePacakgeOptions) { + const tsConfig = loadTsConfigFile(options.tsconfigPath); + log.verbose('Created tsconfig', tsConfig); + + if (tsConfig.options.sourceRoot) { + throw new Error(`${options.tsconfigPath} must not define "compilerOptions.sourceRoot"`); + } + + const program = createTsProject(tsConfig, options.inputPaths); + log.verbose('Loaded typescript program'); + + const typeChecker = program.getTypeChecker(); + log.verbose('Typechecker loaded'); + + const sourceFiles = program + .getSourceFiles() + .filter((f) => !isNodeModule(options.dtsDir, f.fileName)) + .sort((a, b) => a.fileName.localeCompare(b.fileName)); + + const sourceMapper = await SourceMapper.forSourceFiles( + log, + options.dtsDir, + options.repoRelativePackageDir, + sourceFiles + ); + + // value that will end up as the `sourceRoot` in the final sourceMaps + const sourceRoot = `../../../${normalizePath(options.repoRelativePackageDir)}`; + + for (const input of options.inputPaths) { + const outputPath = Path.resolve(options.outputDir, Path.basename(input)); + const mapOutputPath = `${outputPath}.map`; + const sourceFile = program.getSourceFile(input); + if (!sourceFile) { + throw new Error(`input file wasn't included in the program`); + } + + const results = new ExportCollector( + log, + typeChecker, + sourceFile, + options.dtsDir, + sourceMapper + ).run(); + + const printer = new Printer( + sourceMapper, + results.getAll(), + outputPath, + mapOutputPath, + sourceRoot, + !!options.strictPrinting + ); + + const summary = await printer.print(); + + await Fsp.mkdir(options.outputDir, { recursive: true }); + await Fsp.writeFile(outputPath, summary.code); + await Fsp.writeFile(mapOutputPath, JSON.stringify(summary.map)); + + sourceMapper.close(); + } +} diff --git a/packages/kbn-type-summarizer/tests/integration_helpers.ts b/packages/kbn-type-summarizer/tests/integration_helpers.ts new file mode 100644 index 00000000000000..68e1f3cc3a3b0f --- /dev/null +++ b/packages/kbn-type-summarizer/tests/integration_helpers.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable no-console */ + +import Path from 'path'; +import Fsp from 'fs/promises'; + +import * as ts from 'typescript'; +import stripAnsi from 'strip-ansi'; + +import { loadTsConfigFile } from '../src/lib/tsconfig_file'; +import { createTsProject } from '../src/lib/ts_project'; +import { TestLog } from '../src/lib/log'; +import { summarizePackage } from '../src/summarize_package'; + +const TMP_DIR = Path.resolve(__dirname, '__tmp__'); + +const DIAGNOSTIC_HOST = { + getCanonicalFileName: (p: string) => p, + getCurrentDirectory: () => process.cwd(), + getNewLine: () => '\n', +}; + +function dedent(string: string) { + const lines = string.split('\n'); + while (lines.length && lines[0].trim() === '') { + lines.shift(); + } + if (lines.length === 0) { + return ''; + } + const indent = lines[0].split('').findIndex((c) => c !== ' '); + return lines.map((l) => l.slice(indent)).join('\n'); +} + +function ensureDts(path: string) { + if (path.endsWith('.d.ts')) { + throw new Error('path should end with .ts, not .d.ts'); + } + return `${path.slice(0, -3)}.d.ts`; +} + +interface Options { + /* Other files which should be available to the test execution */ + otherFiles?: Record; +} + +class MockCli { + /* file contents which will be fed into TypeScript for this test */ + public readonly mockFiles: Record; + + /* directory where mockFiles pretend to be from */ + public readonly sourceDir = Path.resolve(TMP_DIR, 'src'); + /* directory where we will write .d.ts versions of mockFiles */ + public readonly dtsOutputDir = Path.resolve(TMP_DIR, 'dist_dts'); + /* directory where output will be written */ + public readonly outputDir = Path.resolve(TMP_DIR, 'dts'); + /* path where the tsconfig.json file will be written */ + public readonly tsconfigPath = Path.resolve(this.sourceDir, 'tsconfig.json'); + + /* .d.ts file which we will read to discover the types we need to summarize */ + public readonly inputPath = ensureDts(Path.resolve(this.dtsOutputDir, 'index.ts')); + /* the location we will write the summarized .d.ts file */ + public readonly outputPath = Path.resolve(this.outputDir, Path.basename(this.inputPath)); + /* the location we will write the sourcemaps for the summaried .d.ts file */ + public readonly mapOutputPath = `${this.outputPath}.map`; + + constructor(tsContent: string, options?: Options) { + this.mockFiles = { + ...options?.otherFiles, + 'index.ts': tsContent, + }; + } + + private buildDts() { + const program = createTsProject( + loadTsConfigFile(this.tsconfigPath), + Object.keys(this.mockFiles).map((n) => Path.resolve(this.sourceDir, n)) + ); + + this.printDiagnostics(`dts/config`, program.getConfigFileParsingDiagnostics()); + this.printDiagnostics(`dts/global`, program.getGlobalDiagnostics()); + this.printDiagnostics(`dts/options`, program.getOptionsDiagnostics()); + this.printDiagnostics(`dts/semantic`, program.getSemanticDiagnostics()); + this.printDiagnostics(`dts/syntactic`, program.getSyntacticDiagnostics()); + this.printDiagnostics(`dts/declaration`, program.getDeclarationDiagnostics()); + + const result = program.emit(undefined, undefined, undefined, true); + this.printDiagnostics('dts/results', result.diagnostics); + } + + private printDiagnostics(type: string, diagnostics: readonly ts.Diagnostic[]) { + const errors = diagnostics.filter((d) => d.category === ts.DiagnosticCategory.Error); + if (!errors.length) { + return; + } + + const message = ts.formatDiagnosticsWithColorAndContext(errors, DIAGNOSTIC_HOST); + + console.error( + `TS Errors (${type}):\n${message + .split('\n') + .map((l) => ` ${l}`) + .join('\n')}` + ); + } + + async run() { + const log = new TestLog('debug'); + + // wipe out the tmp dir + await Fsp.rm(TMP_DIR, { recursive: true, force: true }); + + // write mock files to the filesystem + await Promise.all( + Object.entries(this.mockFiles).map(async ([rel, content]) => { + const path = Path.resolve(this.sourceDir, rel); + await Fsp.mkdir(Path.dirname(path), { recursive: true }); + await Fsp.writeFile(path, dedent(content)); + }) + ); + + // write tsconfig.json to the filesystem + await Fsp.writeFile( + this.tsconfigPath, + JSON.stringify({ + include: [`**/*.ts`, `**/*.tsx`], + compilerOptions: { + moduleResolution: 'node', + target: 'es2021', + module: 'CommonJS', + strict: true, + esModuleInterop: true, + allowSyntheticDefaultImports: true, + declaration: true, + emitDeclarationOnly: true, + declarationDir: '../dist_dts', + declarationMap: true, + // prevent loading all @types packages + typeRoots: [], + }, + }) + ); + + // convert the source files to .d.ts files + this.buildDts(); + + // summarize the .d.ts files into the output dir + await summarizePackage(log, { + dtsDir: this.dtsOutputDir, + inputPaths: [this.inputPath], + outputDir: this.outputDir, + repoRelativePackageDir: 'src', + tsconfigPath: this.tsconfigPath, + strictPrinting: false, + }); + + // return the results + return { + code: await Fsp.readFile(this.outputPath, 'utf8'), + map: JSON.parse(await Fsp.readFile(this.mapOutputPath, 'utf8')), + logs: stripAnsi(log.messages.join('')), + }; + } +} + +export async function run(tsContent: string, options?: Options) { + const project = new MockCli(tsContent, options); + return await project.run(); +} diff --git a/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts new file mode 100644 index 00000000000000..84c1ee80c5f166 --- /dev/null +++ b/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { run } from '../integration_helpers'; + +it('prints basic class correctly', async () => { + const output = await run(` + /** + * Interface for writin records to a database + */ + interface Db { + write(record: Record): Promise + } + + export class Foo { + /** + * The name of the Foo + */ + public readonly name: string + constructor(name: string) { + this.name = name.toLowerCase() + } + + speak() { + alert('hi, my name is ' + this.name) + } + + async save(db: Db) { + await db.write({ + name: this.name + }) + } + } + `); + + expect(output.code).toMatchInlineSnapshot(` + "/** + * Interface for writin records to a database + */ + interface Db { + write(record: Record): Promise; + } + export class Foo { + /** + * The name of the Foo + */ + readonly name: string; + constructor(name: string); + speak(): void; + save(db: Db): Promise; + } + //# sourceMappingURL=index.d.ts.map" + `); + expect(output.map).toMatchInlineSnapshot(` + Object { + "file": "index.d.ts", + "mappings": ";;;UAGU,E;;;aAIG,G;;;;WAIK,I;;EAKhB,K;EAIM,I", + "names": Array [], + "sourceRoot": "../../../src", + "sources": Array [ + "index.ts", + ], + "version": 3, + } + `); + expect(output.logs).toMatchInlineSnapshot(` + "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + debug Ignoring 1 global declarations for \\"Record\\" + debug Ignoring 5 global declarations for \\"Promise\\" + " + `); +}); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts new file mode 100644 index 00000000000000..6afc04afe8faad --- /dev/null +++ b/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { run } from '../integration_helpers'; + +it('prints the function declaration, including comments', async () => { + const result = await run( + ` + import { Bar } from './bar'; + + /** + * Convert a Bar to a string + */ + export function foo( + /** + * Important comment + */ + name: Bar + ) { + return name.toString(); + } + `, + { + otherFiles: { + 'bar.ts': ` + export class Bar { + constructor( + private value: T + ){} + + toString() { + return this.value.toString() + } + } + `, + }, + } + ); + + expect(result.code).toMatchInlineSnapshot(` + "class Bar { + private value; + constructor(value: T); + toString(): string; + } + /** + * Convert a Bar to a string + */ + export function foo( + /** + * Important comment + */ + name: Bar): string; + //# sourceMappingURL=index.d.ts.map" + `); + expect(result.map).toMatchInlineSnapshot(` + Object { + "file": "index.d.ts", + "mappings": "MAAa,G;;;UAED,K;;EAGV,Q;;;;;gBCAc,G", + "names": Array [], + "sourceRoot": "../../../src", + "sources": Array [ + "bar.ts", + "index.ts", + ], + "version": 3, + } + `); + expect(result.logs).toMatchInlineSnapshot(` + "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/bar.d.ts + debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + " + `); +}); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts new file mode 100644 index 00000000000000..f23b6c3656d508 --- /dev/null +++ b/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { run } from '../integration_helpers'; + +const nodeModules = { + 'node_modules/foo/index.ts': ` + export class Foo { + render() { + return 'hello' + } + } + `, + 'node_modules/bar/index.ts': ` + export default class Bar { + render() { + return 'hello' + } + } + `, +}; + +it('output type links to named import from node modules', async () => { + const output = await run( + ` + import { Foo } from 'foo' + export type ValidName = string | Foo + `, + { otherFiles: nodeModules } + ); + + expect(output.code).toMatchInlineSnapshot(` + "import { Foo } from 'foo'; + export type ValidName = string | Foo + //# sourceMappingURL=index.d.ts.map" + `); + expect(output.map).toMatchInlineSnapshot(` + Object { + "file": "index.d.ts", + "mappings": ";YACY,S", + "names": Array [], + "sourceRoot": "../../../src", + "sources": Array [ + "index.ts", + ], + "version": 3, + } + `); + expect(output.logs).toMatchInlineSnapshot(` + "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + " + `); +}); + +it('output type links to default import from node modules', async () => { + const output = await run( + ` + import Bar from 'bar' + export type ValidName = string | Bar + `, + { otherFiles: nodeModules } + ); + + expect(output.code).toMatchInlineSnapshot(` + "import { Bar } from 'bar'; + export type ValidName = string | Bar + //# sourceMappingURL=index.d.ts.map" + `); + expect(output.map).toMatchInlineSnapshot(` + Object { + "file": "index.d.ts", + "mappings": ";YACY,S", + "names": Array [], + "sourceRoot": "../../../src", + "sources": Array [ + "index.ts", + ], + "version": 3, + } + `); + expect(output.logs).toMatchInlineSnapshot(` + "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + " + `); +}); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts new file mode 100644 index 00000000000000..da53e91302eef0 --- /dev/null +++ b/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { run } from '../integration_helpers'; + +it('prints the whole interface, including comments', async () => { + const result = await run(` + /** + * This is an interface + */ + export interface Foo { + /** + * method + */ + name(): string + + /** + * hello + */ + close(): Promise + } + `); + + expect(result.code).toMatchInlineSnapshot(` + "/** + * This is an interface + */ + export interface Foo { + /** + * method + */ + name(): string; + /** + * hello + */ + close(): Promise; + } + //# sourceMappingURL=index.d.ts.map" + `); + expect(result.map).toMatchInlineSnapshot(` + Object { + "file": "index.d.ts", + "mappings": ";;;iBAGiB,G", + "names": Array [], + "sourceRoot": "../../../src", + "sources": Array [ + "index.ts", + ], + "version": 3, + } + `); + expect(result.logs).toMatchInlineSnapshot(` + "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + debug Ignoring 5 global declarations for \\"Promise\\" + " + `); +}); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts new file mode 100644 index 00000000000000..1733b43694000d --- /dev/null +++ b/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { run } from '../integration_helpers'; + +it('collects references from source files which contribute to result', async () => { + const result = await run( + ` + /// + export type PromiseOfString = Promise<'string'> + export * from './files' + `, + { + otherFiles: { + 'files/index.ts': ` + /// + export type MySymbol = Symbol & { __tag: 'MySymbol' } + export * from './foo' + `, + 'files/foo.ts': ` + /// + interface Props {} + export type MyComponent = React.Component + `, + }, + } + ); + + expect(result.code).toMatchInlineSnapshot(` + "/// + /// + /// + export type PromiseOfString = Promise<'string'> + export type MySymbol = Symbol & { + __tag: 'MySymbol'; + } + interface Props { + } + export type MyComponent = React.Component + //# sourceMappingURL=index.d.ts.map" + `); + expect(result.map).toMatchInlineSnapshot(` + Object { + "file": "index.d.ts", + "mappings": ";;;YACY,e;YCAA,Q;;;UCAF,K;;YACE,W", + "names": Array [], + "sourceRoot": "../../../src", + "sources": Array [ + "index.ts", + "files/index.ts", + "files/foo.ts", + ], + "version": 3, + } + `); + expect(result.logs).toMatchInlineSnapshot(` + "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/foo.d.ts + debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/index.d.ts + debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + debug Ignoring 5 global declarations for \\"Promise\\" + debug Ignoring 4 global declarations for \\"Symbol\\" + debug Ignoring 2 global declarations for \\"Component\\" + debug Ignoring 1 global declarations for \\"React\\" + " + `); +}); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts new file mode 100644 index 00000000000000..79c2ea69b94777 --- /dev/null +++ b/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { run } from '../integration_helpers'; + +it('prints basic type alias', async () => { + const output = await run(` + export type Name = 'foo' | string + + function hello(name: Name) { + console.log('hello', name) + } + + hello('john') + `); + + expect(output.code).toMatchInlineSnapshot(` + "export type Name = 'foo' | string + //# sourceMappingURL=index.d.ts.map" + `); + expect(output.map).toMatchInlineSnapshot(` + Object { + "file": "index.d.ts", + "mappings": "YAAY,I", + "names": Array [], + "sourceRoot": "../../../src", + "sources": Array [ + "index.ts", + ], + "version": 3, + } + `); + expect(output.logs).toMatchInlineSnapshot(` + "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + " + `); +}); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts new file mode 100644 index 00000000000000..daa6abcc34c594 --- /dev/null +++ b/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { run } from '../integration_helpers'; + +it('prints basic variable exports with sourcemaps', async () => { + const output = await run(` + /** + * What is a type + */ + type Type = 'bar' | 'baz' + + /** some comment */ + export const bar: Type = 'bar' + + export var + /** + * checkout bar + */ + baz: Type = 'baz', + /** + * this is foo + */ + foo: Type = 'bar' + + export let types = [bar, baz, foo] + `); + + expect(output.code).toMatchInlineSnapshot(` + "/** + * What is a type + */ + type Type = 'bar' | 'baz' + /** some comment */ + export const bar: Type; + /** + * checkout bar + */ + export var baz: Type; + /** + * this is foo + */ + export var foo: Type; + export let types: (\\"bar\\" | \\"baz\\")[]; + //# sourceMappingURL=index.d.ts.map" + `); + expect(output.map).toMatchInlineSnapshot(` + Object { + "file": "index.d.ts", + "mappings": ";;;KAGK,I;;aAGQ,G;;;;WAMX,G;;;;WAIA,G;WAES,K", + "names": Array [], + "sourceRoot": "../../../src", + "sources": Array [ + "index.ts", + ], + "version": 3, + } + `); + expect(output.logs).toMatchInlineSnapshot(` + "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + " + `); +}); diff --git a/packages/kbn-type-summarizer/tsconfig.json b/packages/kbn-type-summarizer/tsconfig.json new file mode 100644 index 00000000000000..f3c3802071ac46 --- /dev/null +++ b/packages/kbn-type-summarizer/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "outDir": "target_types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*", + "tests/**/*" + ] +} diff --git a/scripts/build_type_summarizer_output.js b/scripts/build_type_summarizer_output.js new file mode 100644 index 00000000000000..619c8db5d2d059 --- /dev/null +++ b/scripts/build_type_summarizer_output.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +require('../src/setup_node_env/ensure_node_preserve_symlinks'); +require('source-map-support/register'); +require('@kbn/type-summarizer/target_node/bazel_cli'); diff --git a/src/core/public/execution_context/execution_context_service.test.ts b/src/core/public/execution_context/execution_context_service.test.ts new file mode 100644 index 00000000000000..70e57b8993bb1a --- /dev/null +++ b/src/core/public/execution_context/execution_context_service.test.ts @@ -0,0 +1,205 @@ +/* + * Copyright 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 { BehaviorSubject } from 'rxjs'; +import { ExecutionContextService, ExecutionContextSetup } from './execution_context_service'; + +describe('ExecutionContextService', () => { + let execContext: ExecutionContextSetup; + let curApp$: BehaviorSubject; + let execService: ExecutionContextService; + + beforeEach(() => { + execService = new ExecutionContextService(); + execContext = execService.setup(); + curApp$ = new BehaviorSubject('app1'); + execContext = execService.start({ + curApp$, + }); + }); + + it('app name updates automatically and clears everything else', () => { + execContext.set({ + type: 'ghf', + description: 'first set', + }); + + expect(execContext.get()).toStrictEqual({ + name: 'app1', + description: 'first set', + type: 'ghf', + url: '/', + }); + + curApp$.next('app2'); + + expect(execContext.get()).toStrictEqual({ + name: 'app2', + url: '/', + }); + }); + + it('sets context and adds current url and appid when getting it', () => { + execContext.set({ + type: 'ghf', + description: 'first set', + }); + + expect(execContext.get()).toStrictEqual({ + name: 'app1', + description: 'first set', + type: 'ghf', + url: '/', + }); + }); + + it('merges context between calls and gets it', () => { + execContext.set({ + type: 'ghf', + description: 'first set', + }); + + execContext.set({ + type: 'ghf', + description: 'second set', + }); + + expect(execContext.get()).toStrictEqual({ + name: 'app1', + type: 'ghf', + description: 'second set', + url: '/', + }); + }); + + it('context observable fires the context each time it changes', () => { + const sub = jest.fn(); + + execContext.set({ + type: 'ghf', + description: 'first set', + }); + + execContext.context$.subscribe(sub); + + expect(sub).toHaveBeenCalledWith({ + name: 'app1', + type: 'ghf', + description: 'first set', + url: '/', + }); + + execContext.set({ + type: 'str', + description: 'first set', + }); + + expect(sub).toHaveBeenCalledWith({ + name: 'app1', + type: 'str', + description: 'first set', + url: '/', + }); + + expect(sub).toHaveBeenCalledTimes(2); + }); + + it('context observable doesnt fires if the context did not change', () => { + const sub = jest.fn(); + + execContext.set({ + type: 'ghf', + description: 'first set', + }); + + execContext.context$.subscribe(sub); + + execContext.set({ + type: 'ghf', + }); + + expect(sub).toHaveBeenCalledWith({ + name: 'app1', + type: 'ghf', + description: 'first set', + url: '/', + }); + + expect(sub).toHaveBeenCalledTimes(1); + }); + + it('clear resets context and triggers context observable', () => { + const sub = jest.fn(); + + execContext.set({ + type: 'ghf', + description: 'first set', + }); + execContext.context$.subscribe(sub); + + execContext.clear(); + expect(sub).toHaveBeenCalledWith({ + name: 'app1', + url: '/', + }); + + // Clear triggers the observable + expect(sub).toHaveBeenCalledTimes(2); + }); + + it('getAsLabels return relevant values', () => { + execContext.set({ + type: 'ghf', + description: 'first set', + page: 'mypage', + child: { + description: 'inner', + }, + id: '123', + }); + + expect(execContext.getAsLabels()).toStrictEqual({ + name: 'app1', + page: 'mypage', + id: '123', + }); + }); + + it('getAsLabels removes undefined values', () => { + execContext.set({ + type: 'ghf', + description: 'first set', + page: 'mypage', + id: undefined, + }); + + expect(execContext.get()).toStrictEqual({ + name: 'app1', + type: 'ghf', + page: 'mypage', + url: '/', + description: 'first set', + id: undefined, + }); + + expect(execContext.getAsLabels()).toStrictEqual({ + name: 'app1', + page: 'mypage', + }); + }); + + it('stop clears subscriptions', () => { + const sub = jest.fn(); + execContext.context$.subscribe(sub); + sub.mockReset(); + + execService.stop(); + curApp$.next('abc'); + + expect(sub).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/public/execution_context/execution_context_service.ts b/src/core/public/execution_context/execution_context_service.ts index bf13a7351f9b5f..a14d876c9643c8 100644 --- a/src/core/public/execution_context/execution_context_service.ts +++ b/src/core/public/execution_context/execution_context_service.ts @@ -72,13 +72,10 @@ export class ExecutionContextService this.contract = { context$: this.context$.asObservable(), clear: () => { - this.context$.next({}); + this.context$.next(this.getDefaultContext()); }, set: (c: KibanaExecutionContext) => { - const newVal = { - ...this.context$.value, - ...c, - }; + const newVal = this.mergeContext(c); if (!isEqual(newVal, this.context$.value)) { this.context$.next(newVal); } @@ -123,10 +120,16 @@ export class ExecutionContextService return omitBy(context, isUndefined); } - private mergeContext(context: KibanaExecutionContext = {}): KibanaExecutionContext { + private getDefaultContext() { return { name: this.appId, url: window.location.pathname, + }; + } + + private mergeContext(context: KibanaExecutionContext = {}): KibanaExecutionContext { + return { + ...this.getDefaultContext(), ...this.context$.value, ...context, }; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index e3f2822b5a7c8d..b30c009bf25384 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1128,7 +1128,7 @@ export class SavedObjectsClient { }>) => Promise<{ resolved_objects: ResolvedSimpleSavedObject[]; }>; - bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; + bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; // Warning: (ae-forgotten-export) The symbol "SavedObjectsDeleteOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SavedObjectsClientContract" needs to be exported by the entry point index.d.ts diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index c19233809a94be..8509ace0476910 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -596,7 +596,7 @@ export class SavedObjectsClient { return renameKeys< PromiseType>, SavedObjectsBatchResponse - >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse; + >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse; }); } diff --git a/src/core/server/ui_settings/settings/date_formats.ts b/src/core/server/ui_settings/settings/date_formats.ts index c626c4a83cc4cc..039ead326a2361 100644 --- a/src/core/server/ui_settings/settings/date_formats.ts +++ b/src/core/server/ui_settings/settings/date_formats.ts @@ -31,7 +31,7 @@ export const getDateFormatSettings = (): Record => { }), value: 'MMM D, YYYY @ HH:mm:ss.SSS', description: i18n.translate('core.ui_settings.params.dateFormatText', { - defaultMessage: 'When displaying a pretty formatted date, use this {formatLink}', + defaultMessage: 'The {formatLink} for pretty formatted dates.', description: 'Part of composite text: core.ui_settings.params.dateFormatText + ' + 'core.ui_settings.params.dateFormat.optionsLinkText', @@ -48,15 +48,11 @@ export const getDateFormatSettings = (): Record => { }, 'dateFormat:tz': { name: i18n.translate('core.ui_settings.params.dateFormat.timezoneTitle', { - defaultMessage: 'Timezone for date formatting', + defaultMessage: 'Time zone', }), value: 'Browser', description: i18n.translate('core.ui_settings.params.dateFormat.timezoneText', { - defaultMessage: - 'Which timezone should be used. {defaultOption} will use the timezone detected by your browser.', - values: { - defaultOption: '"Browser"', - }, + defaultMessage: 'The default time zone.', }), type: 'select', options: timezones, @@ -115,7 +111,7 @@ export const getDateFormatSettings = (): Record => { }), value: defaultWeekday, description: i18n.translate('core.ui_settings.params.dateFormat.dayOfWeekText', { - defaultMessage: 'What day should weeks start on?', + defaultMessage: 'The day that starts the week.', }), type: 'select', options: weekdays, @@ -141,7 +137,7 @@ export const getDateFormatSettings = (): Record => { }), value: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS', description: i18n.translate('core.ui_settings.params.dateNanosFormatText', { - defaultMessage: 'Used for the {dateNanosLink} datatype of Elasticsearch', + defaultMessage: 'The format for {dateNanosLink} data.', values: { dateNanosLink: '' + 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/dev/bazel/index.bzl b/src/dev/bazel/index.bzl index fcd4212bd5329b..cca81dfcbcd5ae 100644 --- a/src/dev/bazel/index.bzl +++ b/src/dev/bazel/index.bzl @@ -12,7 +12,7 @@ Please do not import from any other files when looking to use a custom rule load("//src/dev/bazel:jsts_transpiler.bzl", _jsts_transpiler = "jsts_transpiler") load("//src/dev/bazel:pkg_npm.bzl", _pkg_npm = "pkg_npm") -load("//src/dev/bazel/pkg_npm_types:index.bzl", _pkg_npm_types = "pkg_npm_types") +load("//src/dev/bazel:pkg_npm_types.bzl", _pkg_npm_types = "pkg_npm_types") load("//src/dev/bazel:ts_project.bzl", _ts_project = "ts_project") jsts_transpiler = _jsts_transpiler diff --git a/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl b/src/dev/bazel/pkg_npm_types.bzl similarity index 83% rename from src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl rename to src/dev/bazel/pkg_npm_types.bzl index ed48228bc95871..e5caba51490538 100644 --- a/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl +++ b/src/dev/bazel/pkg_npm_types.bzl @@ -72,32 +72,22 @@ def _pkg_npm_types_impl(ctx): inputs = ctx.files.srcs[:] inputs.extend(tsconfig_inputs) inputs.extend(deps_inputs) - inputs.append(ctx.file._generated_package_json_template) # output dir declaration package_path = ctx.label.package package_dir = ctx.actions.declare_directory(ctx.label.name) outputs = [package_dir] - # gathering template args - template_args = [ - "NAME", _get_type_package_name(ctx.attr.package_name) - ] - # layout api extractor arguments extractor_args = ctx.actions.args() - ## general args layout - ### [0] = base output dir - ### [1] = generated package json template input file path - ### [2] = stringified template args - ### [3] = tsconfig input file path - ### [4] = entry point from provided types to summarise - extractor_args.add(package_dir.path) - extractor_args.add(ctx.file._generated_package_json_template.path) - extractor_args.add_joined(template_args, join_with = ",", omit_if_empty = False) - extractor_args.add(tsconfig_inputs[0]) - extractor_args.add(_calculate_entrypoint_path(ctx)) + extractor_args.add(struct( + packageName = ctx.attr.package_name, + outputDir = package_dir.path, + buildFilePath = ctx.build_file_path, + tsconfigPath = tsconfig_inputs[0].path, + inputPath = _calculate_entrypoint_path(ctx), + ).to_json()) run_node( ctx, @@ -141,7 +131,9 @@ pkg_npm_types = rule( doc = """Entrypoint name of the types files group to summarise""", default = "index.d.ts", ), - "package_name": attr.string(), + "package_name": attr.string( + mandatory = True + ), "srcs": attr.label_list( doc = """Files inside this directory which are inputs for the types to summarise.""", allow_files = True, @@ -151,11 +143,7 @@ pkg_npm_types = rule( doc = "Target that executes the npm types package assembler binary", executable = True, cfg = "host", - default = Label("//src/dev/bazel/pkg_npm_types:_packager"), - ), - "_generated_package_json_template": attr.label( - allow_single_file = True, - default = "package_json.mustache", + default = Label("//packages/kbn-type-summarizer:bazel-cli"), ), }, ) diff --git a/src/dev/bazel/pkg_npm_types/BUILD.bazel b/src/dev/bazel/pkg_npm_types/BUILD.bazel deleted file mode 100644 index f30d0f8cb8324a..00000000000000 --- a/src/dev/bazel/pkg_npm_types/BUILD.bazel +++ /dev/null @@ -1,28 +0,0 @@ -package(default_visibility = ["//visibility:public"]) - -load("@build_bazel_rules_nodejs//internal/node:node.bzl", "nodejs_binary") - -filegroup( - name = "packager_all_files", - srcs = glob([ - "packager/*", - ]), -) - -exports_files( - [ - "package_json.mustache", - ], - visibility = ["//visibility:public"] -) - -nodejs_binary( - name = "_packager", - data = [ - "@npm//@bazel/typescript", - "@npm//@microsoft/api-extractor", - "@npm//mustache", - ":packager_all_files" - ], - entry_point = ":packager/index.js", -) diff --git a/src/dev/bazel/pkg_npm_types/index.bzl b/src/dev/bazel/pkg_npm_types/index.bzl deleted file mode 100644 index 578ecdd885d158..00000000000000 --- a/src/dev/bazel/pkg_npm_types/index.bzl +++ /dev/null @@ -1,15 +0,0 @@ -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License -# 2.0 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. -# - -"""Public API interface for pkg_npm_types rule. -Please do not import from any other files when looking to this rule -""" - -load(":pkg_npm_types.bzl", _pkg_npm_types = "pkg_npm_types") - -pkg_npm_types = _pkg_npm_types diff --git a/src/dev/bazel/pkg_npm_types/package_json.mustache b/src/dev/bazel/pkg_npm_types/package_json.mustache deleted file mode 100644 index 2229345252e3f2..00000000000000 --- a/src/dev/bazel/pkg_npm_types/package_json.mustache +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "{{{NAME}}}", - "description": "Generated by Bazel", - "types": "./index.d.ts", - "private": true, - "license": "MIT", - "version": "1.1.0" -} diff --git a/src/dev/bazel/pkg_npm_types/packager/create_api_extraction.js b/src/dev/bazel/pkg_npm_types/packager/create_api_extraction.js deleted file mode 100644 index d5f7e0c33ff1cf..00000000000000 --- a/src/dev/bazel/pkg_npm_types/packager/create_api_extraction.js +++ /dev/null @@ -1,90 +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. - */ - -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -const { format, parseTsconfig } = require('@bazel/typescript'); -const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor'); -const fs = require('fs'); -const path = require('path'); - -function createApiExtraction( - tsConfig, - entryPoint, - dtsBundleOut, - apiReviewFolder, - acceptApiUpdates = false -) { - const [parsedConfig, errors] = parseTsconfig(tsConfig); - if (errors && errors.length) { - console.error(format('', errors)); - return 1; - } - const pkgJson = path.resolve(path.dirname(entryPoint), 'package.json'); - if (!fs.existsSync(pkgJson)) { - fs.writeFileSync( - pkgJson, - JSON.stringify({ - name: 'GENERATED-BY-BAZEL', - description: 'This is a dummy package.json as API Extractor always requires one.', - types: './index.d.ts', - private: true, - license: 'SSPL-1.0 OR Elastic License 2.0', - version: '1.0.0', - }) - ); - } - // API extractor doesn't always support the version of TypeScript used in the repo - // example: at the moment it is not compatable with 3.2 - // to use the internal TypeScript we shall not create a program but rather pass a parsed tsConfig. - const parsedTsConfig = parsedConfig.config; - const extractorOptions = { - localBuild: acceptApiUpdates, - }; - const configObject = { - compiler: { - overrideTsconfig: parsedTsConfig, - }, - projectFolder: path.resolve(path.dirname(tsConfig)), - mainEntryPointFilePath: path.resolve(entryPoint), - apiReport: { - enabled: !!apiReviewFolder, - // TODO(alan-agius4): remove this folder name when the below issue is solved upstream - // See: https://github.com/microsoft/web-build-tools/issues/1470 - reportFileName: (apiReviewFolder && path.resolve(apiReviewFolder)) || 'invalid', - }, - docModel: { - enabled: false, - }, - dtsRollup: { - enabled: !!dtsBundleOut, - untrimmedFilePath: dtsBundleOut && path.resolve(dtsBundleOut), - }, - tsdocMetadata: { - enabled: false, - }, - }; - const options = { - configObject, - packageJson: undefined, - packageJsonFullPath: pkgJson, - configObjectFullPath: undefined, - }; - const extractorConfig = ExtractorConfig.prepare(options); - const { succeeded } = Extractor.invoke(extractorConfig, extractorOptions); - // API extractor errors are emitted by it's logger. - return succeeded ? 0 : 1; -} - -module.exports.createApiExtraction = createApiExtraction; diff --git a/src/dev/bazel/pkg_npm_types/packager/generate_package_json.js b/src/dev/bazel/pkg_npm_types/packager/generate_package_json.js deleted file mode 100644 index d4a478a262e5bc..00000000000000 --- a/src/dev/bazel/pkg_npm_types/packager/generate_package_json.js +++ /dev/null @@ -1,43 +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. - */ - -const fs = require('fs'); -const Mustache = require('mustache'); -const path = require('path'); - -function generatePackageJson(outputBasePath, packageJsonTemplatePath, rawPackageJsonTemplateArgs) { - const packageJsonTemplateArgsInTuples = rawPackageJsonTemplateArgs.reduce( - (a, v) => { - const lastTupleIdx = a.length - 1; - const lastTupleSize = a[lastTupleIdx].length; - - if (lastTupleSize < 2) { - a[lastTupleIdx].push(v); - - return a; - } - - return a.push([v]); - }, - [[]] - ); - const packageJsonTemplateArgs = Object.fromEntries(new Map(packageJsonTemplateArgsInTuples)); - - try { - const template = fs.readFileSync(packageJsonTemplatePath); - const renderedTemplate = Mustache.render(template.toString(), packageJsonTemplateArgs); - fs.writeFileSync(path.resolve(outputBasePath, 'package.json'), renderedTemplate); - } catch (e) { - console.error(e); - return 1; - } - - return 0; -} - -module.exports.generatePackageJson = generatePackageJson; diff --git a/src/dev/bazel/pkg_npm_types/packager/index.js b/src/dev/bazel/pkg_npm_types/packager/index.js deleted file mode 100644 index cda299a99d76fc..00000000000000 --- a/src/dev/bazel/pkg_npm_types/packager/index.js +++ /dev/null @@ -1,46 +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. - */ - -const { createApiExtraction } = require('./create_api_extraction'); -const { generatePackageJson } = require('./generate_package_json'); -const path = require('path'); - -const DEBUG = false; - -if (require.main === module) { - if (DEBUG) { - console.error(` -pkg_npm_types packager: running with - cwd: ${process.cwd()} - argv: - ${process.argv.join('\n ')} - `); - } - - // layout args - const [ - outputBasePath, - packageJsonTemplatePath, - stringifiedPackageJsonTemplateArgs, - tsConfig, - entryPoint, - ] = process.argv.slice(2); - const dtsBundleOutput = path.resolve(outputBasePath, 'index.d.ts'); - - // generate pkg json output - const generatePackageJsonRValue = generatePackageJson( - outputBasePath, - packageJsonTemplatePath, - stringifiedPackageJsonTemplateArgs.split(',') - ); - // create api extraction output - const createApiExtractionRValue = createApiExtraction(tsConfig, entryPoint, dtsBundleOutput); - - // setup correct exit code - process.exitCode = generatePackageJsonRValue || createApiExtractionRValue; -} 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/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/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index be7fa5b50a0749..4bdb5ba7284eeb 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -41,40 +41,50 @@ interface Package { const packages: Package[] = [ { name: 're2', - version: '1.16.0', + version: '1.17.4', destinationPath: 'node_modules/re2/build/Release/re2.node', extractMethod: 'gunzip', archives: { 'darwin-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/darwin-x64-93.gz', - sha256: 'a267c6202d86d08170eb4a833acf81d83660ce33e8981fcd5b7f6e0310961d56', + url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/darwin-x64-93.gz', + sha256: '9558c5cb39622e9b3653203e772b129d6c634e7dbd7af1b244352fc1d704601f', }, 'linux-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/linux-x64-93.gz', - sha256: 'e0ca5d6527fe7ec0fe98b6960c47b66a5bb2823c3bebb3bf4ed4d58eed3d23c5', + url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/linux-x64-93.gz', + sha256: '4d06747b266c75b6f7ced93977692c0586ce6a52924cabb569bd966378941aa1', }, - // ARM build is currently done manually as Github Actions used in upstream project + // ARM builds are currently done manually as Github Actions used in upstream project // do not natively support an ARM target. - // From a AWS Graviton instance: - // * checkout the node-re2 project, - // * install Node using the same minor used by Kibana - // * git submodule update --init --recursive to download re2 - // * npm install, which will also create a build - // * gzip -c build/Release/re2.node > linux-arm64-83.gz - // * upload to kibana-ci-proxy-cache bucket + // From a AWS Graviton instance running Ubuntu: + // * install build-essential package + // * install nvm and the node version used by the Kibana repository + // * `npm install re2@1.17.4` + // * re2 will build itself on install + // * `cp node_modules/re2/build/Release/re2.node > linux-arm64-$(node -e "console.log(process.versions.modules)") + // * `gzip linux-arm64-*` + // * capture the sha256 with: `shasum -a 256 linux-arm64-*` + // * upload the `linux-arm64-*.gz` artifact to the `yarn-prebuilt-assets` bucket in GCS using the correct version number 'linux-arm64': { - url: 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.16.0/linux-arm64-93.gz', - sha256: '7a786e0b75985e5aafdefa9af55cad8e85e69a3326f16d8c63d21d6b5b3bff1b', + url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/linux-arm64-93.gz', + sha256: '25409584f76f3d6ed85463d84adf094eb6e256ed1cb0b754b95bcbda6691fc26', }, + + // A similar process is necessary for building on ARM macs: + // * bootstrap and re2 will build itself on install + // * `cp node_modules/re2/build/Release/re2.node > darwin-arm64-$(node -e "console.log(process.versions.modules)") + // * `gzip darwin-arm64-*` + // * capture the sha256 with: `shasum -a 256 darwin-arm64-*` + // * upload the `darwin-arm64-*.gz` artifact to the `yarn-prebuilt-assets` bucket in GCS using the correct version number 'darwin-arm64': { - url: 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.16.0/darwin-arm64-93.gz', - sha256: '28b540cdddf13578f1bd28a03e29ffdc26a7f00ec859c369987b8d51ec6357c8', + url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/darwin-arm64-93.gz', + sha256: 'd4b708749ddef1c87019f6b80e051ed0c29ccd1de34f233c47d8dcaddf803872', }, + 'win32-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/win32-x64-93.gz', - sha256: '37245ceb59a086b5e7e9de8746a3cdf148c383be9ae2580f92baea90d0d39947', + url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/win32-x64-93.gz', + sha256: '0320d0c0385432944c6fb3c8c8fcd78d440ce5626f7618f9ec71d88e44820674', }, }, }, 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/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..6b47d9b805af79 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( @@ -82,4 +82,5 @@ export const PROJECTS = [ ...findProjects('test/plugin_functional/plugins/*/tsconfig.json'), ...findProjects('test/interpreter_functional/plugins/*/tsconfig.json'), ...findProjects('test/server_integration/__fixtures__/plugins/*/tsconfig.json'), + ...findProjects('packages/kbn-type-summarizer/tests/tsconfig.json'), ]; diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts index ae9da90ffb5727..80cfaff57c35c1 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts +++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts @@ -11,6 +11,7 @@ import { findAccessorOrFail } from '../../../../visualizations/common/utils'; import type { ExpressionValueVisDimension } from '../../../../visualizations/common'; import { prepareLogTable } from '../../../../visualizations/common/utils'; import type { DatatableColumn } from '../../../../expressions'; +import { validateOptions } from '../../../../charts/common'; import { GaugeExpressionFunctionDefinition } from '../types'; import { EXPRESSION_GAUGE_NAME, @@ -79,16 +80,6 @@ const validateAccessor = ( } }; -const validateOptions = ( - value: string, - availableOptions: Record, - getErrorMessage: () => string -) => { - if (!Object.values(availableOptions).includes(value)) { - throw new Error(getErrorMessage()); - } -}; - export const gaugeFunction = (): GaugeExpressionFunctionDefinition => ({ name: EXPRESSION_GAUGE_NAME, type: 'render', diff --git a/src/plugins/chart_expressions/expression_gauge/common/index.ts b/src/plugins/chart_expressions/expression_gauge/common/index.ts index afd8f6105d8f6a..395aa3ed608612 100755 --- a/src/plugins/chart_expressions/expression_gauge/common/index.ts +++ b/src/plugins/chart_expressions/expression_gauge/common/index.ts @@ -10,6 +10,7 @@ export const PLUGIN_ID = 'expressionGauge'; export const PLUGIN_NAME = 'expressionGauge'; export type { + GaugeExpressionFunctionDefinition, GaugeExpressionProps, FormatFactory, GaugeRenderProps, diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx index 99342edbdbc648..22601ae409f62e 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx +++ b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx @@ -10,6 +10,7 @@ import { Chart, Goal, Settings } from '@elastic/charts'; import { FormattedMessage } from '@kbn/i18n-react'; import type { CustomPaletteState } from '../../../../charts/public'; import { EmptyPlaceholder } from '../../../../charts/public'; +import { isVisDimension } from '../../../../visualizations/common/utils'; import { GaugeRenderProps, GaugeLabelMajorMode, @@ -234,17 +235,22 @@ export const GaugeComponent: FC = memo( /> ); } + const customMetricFormatParams = isVisDimension(args.metric) ? args.metric.format : undefined; + const tableMetricFormatParams = metricColumn?.meta?.params?.params + ? metricColumn?.meta?.params + : undefined; + + const defaultMetricFormatParams = { + id: 'number', + params: { + pattern: max - min > 5 ? `0,0` : `0,0.0`, + }, + }; const tickFormatter = formatFactory( - metricColumn?.meta?.params?.params - ? metricColumn?.meta?.params - : { - id: 'number', - params: { - pattern: max - min > 5 ? `0,0` : `0,0.0`, - }, - } + customMetricFormatParams ?? tableMetricFormatParams ?? defaultMetricFormatParams ); + const colors = palette?.params?.colors ? normalizeColors(palette.params, min, max) : undefined; const bands: number[] = (palette?.params as CustomPaletteState) ? normalizeBands(args.palette?.params as CustomPaletteState, { min, max }) diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_legend.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_legend.ts index efbc251f6360b3..28a37c522ac2db 100644 --- a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_legend.ts +++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_legend.ts @@ -8,9 +8,18 @@ import { Position } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import type { ExpressionFunctionDefinition } from '../../../../expressions/common'; +import { validateOptions } from '../../../../charts/common'; import { EXPRESSION_HEATMAP_LEGEND_NAME } from '../constants'; import { HeatmapLegendConfig, HeatmapLegendConfigResult } from '../types'; +export const errors = { + invalidPositionError: () => + i18n.translate('expressionHeatmap.functions.heatmap.errors.invalidPositionError', { + defaultMessage: `Invalid position is specified. Supported positions: {positions}`, + values: { positions: Object.values(Position).join(', ') }, + }), +}; + export const heatmapLegendConfig: ExpressionFunctionDefinition< typeof EXPRESSION_HEATMAP_LEGEND_NAME, null, @@ -31,6 +40,7 @@ export const heatmapLegendConfig: ExpressionFunctionDefinition< }, position: { types: ['string'], + default: Position.Right, options: [Position.Top, Position.Right, Position.Bottom, Position.Left], help: i18n.translate('expressionHeatmap.function.args.legend.position.help', { defaultMessage: 'Specifies the legend position.', @@ -51,6 +61,7 @@ export const heatmapLegendConfig: ExpressionFunctionDefinition< }, }, fn(input, args) { + validateOptions(args.position, Position, errors.invalidPositionError); return { type: EXPRESSION_HEATMAP_LEGEND_NAME, ...args, diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts index 596b59b418b422..690ea75e9cb2fc 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts @@ -10,20 +10,10 @@ import { i18n } from '@kbn/i18n'; import { visType } from '../types'; import { prepareLogTable, Dimension } from '../../../../visualizations/common/utils'; -import { ColorMode } from '../../../../charts/common'; +import { ColorMode, validateOptions } from '../../../../charts/common'; import { MetricVisExpressionFunctionDefinition } from '../types'; import { EXPRESSION_METRIC_NAME, LabelPosition } from '../constants'; -const validateOptions = ( - value: string, - availableOptions: Record, - getErrorMessage: () => string -) => { - if (!Object.values(availableOptions).includes(value)) { - throw new Error(getErrorMessage()); - } -}; - const errors = { invalidColorModeError: () => i18n.translate('expressionMetricVis.function.errors.invalidColorModeError', { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts index aa433b8eaee2d8..deedb10940253a 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts @@ -7,6 +7,8 @@ */ import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; +import { LegendDisplay } from '../types/expression_renderers'; export const strings = { getPieVisFunctionName: () => @@ -127,4 +129,14 @@ export const errors = { defaultMessage: 'A split row and column are specified. Expression is supporting only one of them at once.', }), + invalidLegendDisplayError: () => + i18n.translate('expressionPartitionVis.reusable.function.errors.invalidLegendDisplayError', { + defaultMessage: `Invalid legend display mode is specified. Supported ticks legend display modes: {legendDisplayModes}`, + values: { legendDisplayModes: Object.values(LegendDisplay).join(', ') }, + }), + invalidLegendPositionError: () => + i18n.translate('expressionPartitionVis.reusable.function.errors.invalidLegendPositionError', { + defaultMessage: `Invalid legend position is specified. Supported ticks legend positions: {positions}`, + values: { positions: Object.values(Position).join(', ') }, + }), }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts index 2f4c681ef336ca..7db9445ea3ee85 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ +import { Position } from '@elastic/charts'; import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers'; import { prepareLogTable } from '../../../../visualizations/common/utils'; +import { validateOptions } from '../../../../charts/common'; import { ChartTypes, MosaicVisExpressionFunctionDefinition } from '../types'; import { PARTITION_LABELS_FUNCTION, @@ -56,7 +58,9 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ }, legendPosition: { types: ['string'], + default: Position.Right, help: strings.getLegendPositionArgHelp(), + options: [Position.Top, Position.Right, Position.Bottom, Position.Left], }, nestedLegend: { types: ['boolean'], @@ -98,6 +102,9 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); } + validateOptions(args.legendDisplay, LegendDisplay, errors.invalidLegendDisplayError); + validateOptions(args.legendPosition, Position, errors.invalidLegendPositionError); + const visConfig: PartitionVisParams = { ...args, ariaLabel: diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts index d743637f44b866..5982f9a762d8c9 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts @@ -9,6 +9,7 @@ import { Position } from '@elastic/charts'; import { EmptySizeRatios, LegendDisplay, PartitionVisParams } from '../types/expression_renderers'; import { prepareLogTable } from '../../../../visualizations/common/utils'; +import { validateOptions } from '../../../../charts/common'; import { ChartTypes, PieVisExpressionFunctionDefinition } from '../types'; import { PARTITION_LABELS_FUNCTION, @@ -57,6 +58,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ }, legendPosition: { types: ['string'], + default: Position.Right, help: strings.getLegendPositionArgHelp(), options: [Position.Top, Position.Right, Position.Bottom, Position.Left], }, @@ -120,6 +122,9 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); } + validateOptions(args.legendDisplay, LegendDisplay, errors.invalidLegendDisplayError); + validateOptions(args.legendPosition, Position, errors.invalidLegendPositionError); + const visConfig: PartitionVisParams = { ...args, ariaLabel: diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts index d2016b3ae0c813..634fa079671cb2 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ +import { Position } from '@elastic/charts'; import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers'; import { prepareLogTable } from '../../../../visualizations/common/utils'; +import { validateOptions } from '../../../../charts/common'; import { ChartTypes, TreemapVisExpressionFunctionDefinition } from '../types'; import { PARTITION_LABELS_FUNCTION, @@ -56,7 +58,9 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => }, legendPosition: { types: ['string'], + default: Position.Right, help: strings.getLegendPositionArgHelp(), + options: [Position.Top, Position.Right, Position.Bottom, Position.Left], }, nestedLegend: { types: ['boolean'], @@ -98,6 +102,9 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); } + validateOptions(args.legendDisplay, LegendDisplay, errors.invalidLegendDisplayError); + validateOptions(args.legendPosition, Position, errors.invalidLegendPositionError); + const visConfig: PartitionVisParams = { ...args, ariaLabel: diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts index 242d8a2c9bace0..f88c2821cdb658 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ +import { Position } from '@elastic/charts'; import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers'; import { prepareLogTable } from '../../../../visualizations/common/utils'; +import { validateOptions } from '../../../../charts/common'; import { ChartTypes, WaffleVisExpressionFunctionDefinition } from '../types'; import { PARTITION_LABELS_FUNCTION, @@ -55,7 +57,9 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ }, legendPosition: { types: ['string'], + default: Position.Right, help: strings.getLegendPositionArgHelp(), + options: [Position.Top, Position.Right, Position.Bottom, Position.Left], }, truncateLegend: { types: ['boolean'], @@ -92,6 +96,9 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); } + validateOptions(args.legendDisplay, LegendDisplay, errors.invalidLegendDisplayError); + validateOptions(args.legendPosition, Position, errors.invalidLegendPositionError); + const buckets = args.bucket ? [args.bucket] : []; const visConfig: PartitionVisParams = { ...args, diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/constants.ts b/src/plugins/chart_expressions/expression_tagcloud/common/constants.ts index 3d834448a94efc..8c4041c6a3a7eb 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/constants.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/constants.ts @@ -10,3 +10,15 @@ export const PLUGIN_ID = 'expressionTagcloud'; export const PLUGIN_NAME = 'expressionTagcloud'; export const EXPRESSION_NAME = 'tagcloud'; + +export const ScaleOptions = { + LINEAR: 'linear', + LOG: 'log', + SQUARE_ROOT: 'square root', +} as const; + +export const Orientation = { + SINGLE: 'single', + RIGHT_ANGLED: 'right angled', + MULTIPLE: 'multiple', +} as const; diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts index 86a371afd6912d..dc77848d37ceba 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts @@ -11,6 +11,7 @@ import { tagcloudFunction } from './tagcloud_function'; import { functionWrapper } from '../../../../expressions/common/expression_functions/specs/tests/utils'; import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { Datatable } from '../../../../expressions/common/expression_types/specs'; +import { ScaleOptions, Orientation } from '../constants'; type Arguments = Parameters['fn']>[1]; @@ -30,8 +31,8 @@ describe('interpreter/functions#tagcloud', () => { ], } as unknown as Datatable; const visConfig = { - scale: 'linear', - orientation: 'single', + scale: ScaleOptions.LINEAR, + orientation: Orientation.SINGLE, minFontSize: 18, maxFontSize: 72, showLabel: true, diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts index 85f98b35cede93..a11c0b6d867ec1 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import { prepareLogTable, Dimension } from '../../../../visualizations/common/utils'; +import { validateOptions } from '../../../../charts/common'; import { TagCloudRendererParams } from '../types'; import { ExpressionTagcloudFunction } from '../types'; -import { EXPRESSION_NAME } from '../constants'; +import { EXPRESSION_NAME, ScaleOptions, Orientation } from '../constants'; const strings = { help: i18n.translate('expressionTagcloud.functions.tagcloudHelpText', { @@ -74,6 +75,16 @@ export const errors = { }, }) ), + invalidScaleOptionError: () => + i18n.translate('expressionTagcloud.functions.tagcloud.invalidScaleOptionError', { + defaultMessage: `Invalid scale option is specified. Supported scale options: {scaleOptions}`, + values: { scaleOptions: Object.values(ScaleOptions).join(', ') }, + }), + invalidOrientationError: () => + i18n.translate('expressionTagcloud.functions.tagcloud.invalidOrientationError', { + defaultMessage: `Invalid orientation of words is specified. Supported scale orientation: {orientation}`, + values: { orientation: Object.values(Orientation).join(', ') }, + }), }; export const tagcloudFunction: ExpressionTagcloudFunction = () => { @@ -87,14 +98,14 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { args: { scale: { types: ['string'], - default: 'linear', - options: ['linear', 'log', 'square root'], + default: ScaleOptions.LINEAR, + options: [ScaleOptions.LINEAR, ScaleOptions.LOG, ScaleOptions.SQUARE_ROOT], help: argHelp.scale, }, orientation: { types: ['string'], - default: 'single', - options: ['single', 'right angled', 'multiple'], + default: Orientation.SINGLE, + options: [Orientation.SINGLE, Orientation.RIGHT_ANGLED, Orientation.MULTIPLE], help: argHelp.orientation, }, minFontSize: { @@ -133,6 +144,9 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { }, }, fn(input, args, handlers) { + validateOptions(args.scale, ScaleOptions, errors.invalidScaleOptionError); + validateOptions(args.orientation, Orientation, errors.invalidOrientationError); + const visParams: TagCloudRendererParams = { scale: args.scale, orientation: args.orientation, diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts index 44fc6f3048790f..62f04551680b71 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts @@ -5,6 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import { $Values } from '@kbn/utility-types'; import { PaletteOutput } from '../../../../charts/common'; import { Datatable, @@ -12,11 +14,11 @@ import { ExpressionValueRender, } from '../../../../expressions'; import { ExpressionValueVisDimension } from '../../../../visualizations/common'; -import { EXPRESSION_NAME } from '../constants'; +import { EXPRESSION_NAME, ScaleOptions, Orientation } from '../constants'; interface TagCloudCommonParams { - scale: 'linear' | 'log' | 'square root'; - orientation: 'single' | 'right angled' | 'multiple'; + scale: $Values; + orientation: $Values; minFontSize: number; maxFontSize: number; showLabel: boolean; diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx index eca35918d72898..9866ec644ae263 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx @@ -11,6 +11,7 @@ import { storiesOf } from '@storybook/react'; import { tagcloudRenderer } from '../expression_renderers'; import { Render } from '../../../../presentation_util/public/__stories__'; import { TagcloudRendererConfig } from '../../common/types'; +import { ScaleOptions, Orientation } from '../../common/constants'; import { palettes } from '../__mocks__/palettes'; import { theme } from '../__mocks__/theme'; @@ -39,8 +40,8 @@ const config: TagcloudRendererConfig = { ], }, visParams: { - scale: 'linear', - orientation: 'single', + scale: ScaleOptions.LINEAR, + orientation: Orientation.SINGLE, minFontSize: 18, maxFontSize: 72, showLabel: true, @@ -78,7 +79,7 @@ storiesOf('renderers/tag_cloud_vis', module) return ( tagcloudRenderer({ palettes, theme })} - config={{ ...config, visParams: { ...config.visParams, scale: 'log' } }} + config={{ ...config, visParams: { ...config.visParams, scale: ScaleOptions.LOG } }} {...containerSize} /> ); @@ -87,7 +88,7 @@ storiesOf('renderers/tag_cloud_vis', module) return ( tagcloudRenderer({ palettes, theme })} - config={{ ...config, visParams: { ...config.visParams, scale: 'square root' } }} + config={{ ...config, visParams: { ...config.visParams, scale: ScaleOptions.SQUARE_ROOT } }} {...containerSize} /> ); @@ -96,7 +97,10 @@ storiesOf('renderers/tag_cloud_vis', module) return ( tagcloudRenderer({ palettes, theme })} - config={{ ...config, visParams: { ...config.visParams, orientation: 'right angled' } }} + config={{ + ...config, + visParams: { ...config.visParams, orientation: Orientation.RIGHT_ANGLED }, + }} {...containerSize} /> ); @@ -105,7 +109,10 @@ storiesOf('renderers/tag_cloud_vis', module) return ( tagcloudRenderer({ palettes, theme })} - config={{ ...config, visParams: { ...config.visParams, orientation: 'multiple' } }} + config={{ + ...config, + visParams: { ...config.visParams, orientation: Orientation.MULTIPLE }, + }} {...containerSize} /> ); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx index f65630e422cceb..a85455b9240108 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx @@ -13,6 +13,7 @@ import { mount } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import TagCloudChart, { TagCloudChartProps } from './tagcloud_component'; import { TagCloudRendererParams } from '../../common/types'; +import { ScaleOptions, Orientation } from '../../common/constants'; jest.mock('../format_service', () => ({ getFormatService: jest.fn(() => { @@ -51,8 +52,8 @@ const visData: Datatable = { const visParams: TagCloudRendererParams = { bucket: { type: 'vis_dimension', accessor: 0, format: { params: {} } }, metric: { type: 'vis_dimension', accessor: 1, format: { params: {} } }, - scale: 'linear', - orientation: 'single', + scale: ScaleOptions.LINEAR, + orientation: Orientation.SINGLE, palette: { type: 'palette', name: 'default', @@ -166,7 +167,10 @@ describe('TagCloudChart', function () { }); it('sets the angles correctly', async () => { - const newVisParams: TagCloudRendererParams = { ...visParams, orientation: 'right angled' }; + const newVisParams: TagCloudRendererParams = { + ...visParams, + orientation: Orientation.RIGHT_ANGLED, + }; const newProps = { ...wrapperPropsWithIndexes, visParams: newVisParams }; const component = mount(); expect(component.find(Wordcloud).prop('endAngle')).toBe(90); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx index 560507f84831a0..150da4c952c331 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx @@ -20,6 +20,7 @@ import { import { getFormatService } from '../format_service'; import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { TagcloudRendererConfig } from '../../common/types'; +import { ScaleOptions, Orientation } from '../../common/constants'; import './tag_cloud.scss'; @@ -60,15 +61,15 @@ const getColor = ( }; const ORIENTATIONS = { - single: { + [Orientation.SINGLE]: { endAngle: 0, angleCount: 360, }, - 'right angled': { + [Orientation.RIGHT_ANGLED]: { endAngle: 90, angleCount: 2, }, - multiple: { + [Orientation.MULTIPLE]: { endAngle: -90, angleCount: 12, }, @@ -210,7 +211,7 @@ export const TagCloudChart = ({ maxFontSize={visParams.maxFontSize} spiral="archimedean" data={tagCloudData} - weightFn={scale === 'square root' ? 'squareRoot' : scale} + weightFn={scale === ScaleOptions.SQUARE_ROOT ? 'squareRoot' : scale} outOfRoomCallback={() => { setWarning(true); }} diff --git a/src/plugins/charts/common/index.ts b/src/plugins/charts/common/index.ts index 2b8f252f892a59..35f12884d29cd4 100644 --- a/src/plugins/charts/common/index.ts +++ b/src/plugins/charts/common/index.ts @@ -35,3 +35,5 @@ export { } from './static'; export type { ColorSchemaParams, Labels, Style, PaletteContinuity } from './types'; + +export { validateOptions } from './utils'; diff --git a/src/plugins/charts/common/utils.ts b/src/plugins/charts/common/utils.ts new file mode 100644 index 00000000000000..393110e26994b2 --- /dev/null +++ b/src/plugins/charts/common/utils.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const validateOptions = ( + value: string, + availableOptions: Record | Array, + getErrorMessage: () => string +) => { + const options = Array.isArray(availableOptions) + ? availableOptions + : Object.values(availableOptions); + if (!options.includes(value)) { + throw new Error(getErrorMessage()); + } +}; 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/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx index 44b1aec226fd66..5fece7ff959ced 100644 --- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -153,7 +153,8 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { }; const getEmbeddableFactoryMenuItem = ( - factory: EmbeddableFactoryDefinition + factory: EmbeddableFactoryDefinition, + closePopover: () => void ): EuiContextMenuPanelItemDescriptor => { const icon = factory?.getIconType ? factory.getIconType() : 'empty'; @@ -164,6 +165,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { icon, toolTipContent, onClick: async () => { + closePopover(); if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, factory.type); } @@ -192,42 +194,47 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { defaultMessage: 'Aggregation based', }); - const editorMenuPanels = [ - { - id: 0, - items: [ - ...visTypeAliases.map(getVisTypeAliasMenuItem), - ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({ - name: appName, - icon, - panel: panelId, - 'data-test-subj': `dashboardEditorMenu-${id}Group`, - })), - ...ungroupedFactories.map(getEmbeddableFactoryMenuItem), - ...promotedVisTypes.map(getVisTypeMenuItem), - { - name: aggsPanelTitle, - icon: 'visualizeApp', - panel: aggBasedPanelID, - 'data-test-subj': `dashboardEditorAggBasedMenuItem`, - }, - ...toolVisTypes.map(getVisTypeMenuItem), - ], - }, - { - id: aggBasedPanelID, - title: aggsPanelTitle, - items: aggsBasedVisTypes.map(getVisTypeMenuItem), - }, - ...Object.values(factoryGroupMap).map( - ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({ - id: panelId, - title: appName, - items: groupFactories.map(getEmbeddableFactoryMenuItem), - }) - ), - ]; - + const getEditorMenuPanels = (closePopover: () => void) => { + return [ + { + id: 0, + items: [ + ...visTypeAliases.map(getVisTypeAliasMenuItem), + ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({ + name: appName, + icon, + panel: panelId, + 'data-test-subj': `dashboardEditorMenu-${id}Group`, + })), + ...ungroupedFactories.map((factory) => { + return getEmbeddableFactoryMenuItem(factory, closePopover); + }), + ...promotedVisTypes.map(getVisTypeMenuItem), + { + name: aggsPanelTitle, + icon: 'visualizeApp', + panel: aggBasedPanelID, + 'data-test-subj': `dashboardEditorAggBasedMenuItem`, + }, + ...toolVisTypes.map(getVisTypeMenuItem), + ], + }, + { + id: aggBasedPanelID, + title: aggsPanelTitle, + items: aggsBasedVisTypes.map(getVisTypeMenuItem), + }, + ...Object.values(factoryGroupMap).map( + ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({ + id: panelId, + title: appName, + items: groupFactories.map((factory) => { + return getEmbeddableFactoryMenuItem(factory, closePopover); + }), + }) + ), + ]; + }; return ( { panelPaddingSize="none" data-test-subj="dashboardEditorMenuButton" > - {() => ( + {({ closePopover }: { closePopover: () => void }) => ( +
+
+
+
+
+
+
+

+ test +

+
+
+
+
+
+ test +
+
+
+
+
+
+
+
+
+
+
`; exports[`KibanaPageTemplate render custom empty prompt only 1`] = ` - } /> - + `; exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = ` - } /> - + `; exports[`KibanaPageTemplate render default empty prompt 1`] = ` - - - test -

+ ], + "title": "test", } - iconColor="" - iconType="test" - /> -
+ } +/> `; exports[`KibanaPageTemplate render noDataContent 1`] = ` - + { + const { + className, + noDataConfig, + ...rest + } = props; + + if (!noDataConfig) { + return null; } - pageSideBarProps={ + + const template = _util.NO_DATA_PAGE_TEMPLATE_PROPS.template; + const classes = (0, _util.getClasses)(template, className); + return /*#__PURE__*/_react.default.createElement(_eui.EuiPageTemplate, (0, _extends2.default)({ + "data-test-subj": props['data-test-subj'], + template: template, + className: classes + }, rest, _util.NO_DATA_PAGE_TEMPLATE_PROPS), /*#__PURE__*/_react.default.createElement(_no_data_page.NoDataPage, noDataConfig)); +} + noDataConfig={ Object { - "className": "kbnPageTemplate__pageSideBar", - "paddingSize": "none", - } - } - restrictWidth={950} - template="centeredBody" -> - - -`; - -exports[`KibanaPageTemplate render solutionNav 1`] = ` - - } - pageSideBarProps={ + solutionNav={ Object { - "className": "kbnPageTemplate__pageSideBar", - "paddingSize": "none", + "icon": "solution", + "items": Array [ + Object { + "id": "1", + "items": Array [ + Object { + "id": "1.1", + "items": undefined, + "name": "Ingest Node Pipelines", + "tabIndex": undefined, + }, + Object { + "id": "1.2", + "items": undefined, + "name": "Logstash Pipelines", + "tabIndex": undefined, + }, + Object { + "id": "1.3", + "items": undefined, + "name": "Beats Central Management", + "tabIndex": undefined, + }, + ], + "name": "Ingest", + "tabIndex": undefined, + }, + Object { + "id": "2", + "items": Array [ + Object { + "id": "2.1", + "items": undefined, + "name": "Index Management", + "tabIndex": undefined, + }, + Object { + "id": "2.2", + "items": undefined, + "name": "Index Lifecycle Policies", + "tabIndex": undefined, + }, + Object { + "id": "2.3", + "items": undefined, + "name": "Snapshot and Restore", + "tabIndex": undefined, + }, + ], + "name": "Data", + "tabIndex": undefined, + }, + ], + "name": "Solution", } } - restrictWidth={true} /> `; + +exports[`KibanaPageTemplate render solutionNav 1`] = ` +
+
+
+
+
+
+
+
+
+

+ test +

+
+
+
+
+
+ test +
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/plugins/kibana_react/public/page_template/index.ts b/src/plugins/kibana_react/public/page_template/index.ts index 41eeaab01ef39c..fda644a2847974 100644 --- a/src/plugins/kibana_react/public/page_template/index.ts +++ b/src/plugins/kibana_react/public/page_template/index.ts @@ -8,5 +8,7 @@ export type { KibanaPageTemplateProps } from './page_template'; export { KibanaPageTemplate } from './page_template'; -export { KibanaPageTemplateSolutionNavAvatar } from './solution_nav'; +export { KibanaPageTemplateSolutionNavAvatar, KibanaPageTemplateSolutionNav } from './solution_nav'; export * from './no_data_page'; +export { withSolutionNav } from './with_solution_nav'; +export { NO_DATA_PAGE_MAX_WIDTH, NO_DATA_PAGE_TEMPLATE_PROPS } from './util'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap index 0554e64c5ecb6f..18df4fa2444966 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap @@ -106,88 +106,22 @@ exports[`NoDataPage render 1`] = ` } } > - - - -

- -

- -

- - - , - "solution": "Elastic", - } - } - /> -

-
-
- - - , + , + , + ] } - > - - - - - - - - - - + docsLink="test" + pageTitle="Welcome to Elastic Elastic!" + solution="Elastic" + /> diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.scss b/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/action_cards.scss similarity index 100% rename from src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.scss rename to src/plugins/kibana_react/public/page_template/no_data_page/action_cards/action_cards.scss diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/action_cards.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/action_cards.test.tsx new file mode 100644 index 00000000000000..6223613815f58c --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/action_cards.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { shallowWithIntl } from '@kbn/test-jest-helpers'; +import { NoDataCard } from '../no_data_card'; +import { ActionCards } from './action_cards'; + +describe('ActionCards', () => { + const onClick = jest.fn(); + const action = { + recommended: false, + button: 'Button text', + onClick, + }; + const card = ; + const actionCard1 =
{card}
; + const actionCard2 =
{card}
; + + test('renders correctly', () => { + const component = shallowWithIntl(); + const actionCards = component.find('div'); + expect(actionCards.length).toBe(2); + expect(actionCards.at(0).key()).toBe('first'); + expect(actionCards.at(1).key()).toBe('second'); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/action_cards.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/action_cards.tsx new file mode 100644 index 00000000000000..3af0a61876729a --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/action_cards.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 './action_cards.scss'; + +import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; +import React, { ReactElement } from 'react'; +import { ElasticAgentCard, NoDataCard } from '../no_data_card'; + +interface ActionCardsProps { + actionCards: Array | ReactElement>; +} +export const ActionCards = ({ actionCards }: ActionCardsProps) => { + const cards = actionCards.map((card) => ( + + {card} + + )); + return ( + + {cards} + + ); +}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/index.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/index.tsx new file mode 100644 index 00000000000000..0ba8ef86ba5cbd --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/action_cards/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { ActionCards } from './action_cards'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/index.ts index 55661ad6f14f75..b5a11722dd3975 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/index.ts +++ b/src/plugins/kibana_react/public/page_template/no_data_page/index.ts @@ -8,3 +8,4 @@ export * from './no_data_page'; export * from './no_data_card'; +export * from './no_data_config_page'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_config_page/index.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_config_page/index.tsx new file mode 100644 index 00000000000000..0bdde400213984 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_config_page/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { NoDataConfigPage, NoDataConfigPageWithSolutionNavBar } from './no_data_config_page'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_config_page/no_data_config_page.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_config_page/no_data_config_page.tsx new file mode 100644 index 00000000000000..07ffc96181476d --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_config_page/no_data_config_page.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EuiPageTemplate } from '@elastic/eui'; +import React from 'react'; +import { NoDataPage } from '../no_data_page'; +import { withSolutionNav } from '../../with_solution_nav'; +import { KibanaPageTemplateProps } from '../../page_template'; +import { getClasses, NO_DATA_PAGE_TEMPLATE_PROPS } from '../../util'; + +export const NoDataConfigPage = (props: KibanaPageTemplateProps) => { + const { className, noDataConfig, ...rest } = props; + + if (!noDataConfig) { + return null; + } + + const template = NO_DATA_PAGE_TEMPLATE_PROPS.template; + const classes = getClasses(template, className); + + return ( + + + + ); +}; + +export const NoDataConfigPageWithSolutionNavBar = withSolutionNav(NoDataConfigPage); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx index 0c8754f852b042..077f991477e8d7 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx @@ -6,36 +6,14 @@ * Side Public License, v 1. */ -import './no_data_page.scss'; - import React, { ReactNode, useMemo, FunctionComponent, MouseEventHandler } from 'react'; -import { - EuiFlexItem, - EuiCardProps, - EuiFlexGrid, - EuiSpacer, - EuiText, - EuiTextColor, - EuiLink, - CommonProps, -} from '@elastic/eui'; +import { EuiCardProps, EuiSpacer, EuiText, EuiLink, CommonProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import classNames from 'classnames'; -import { KibanaPageTemplateProps } from '../page_template'; import { ElasticAgentCard, NoDataCard } from './no_data_card'; -import { KibanaPageTemplateSolutionNavAvatar } from '../solution_nav'; - -export const NO_DATA_PAGE_MAX_WIDTH = 950; -export const NO_DATA_PAGE_TEMPLATE_PROPS: KibanaPageTemplateProps = { - restrictWidth: NO_DATA_PAGE_MAX_WIDTH, - template: 'centeredBody', - pageContentProps: { - hasShadow: false, - color: 'transparent', - }, -}; +import { NoDataPageBody } from './no_data_page_body/no_data_page_body'; export const NO_DATA_RECOMMENDED = i18n.translate( 'kibana-react.noDataPage.noDataPage.recommended', @@ -112,70 +90,35 @@ export const NoDataPage: FunctionComponent = ({ // Convert the iterated [[key, value]] array format back into an object const sortedData = Object.fromEntries(sortedEntries); const actionsKeys = Object.keys(sortedData); - const renderActions = useMemo(() => { + + const actionCards = useMemo(() => { return Object.values(sortedData).map((action, i) => { - if (actionsKeys[i] === 'elasticAgent' || actionsKeys[i] === 'beats') { - return ( - - - - ); - } else { - return ( - - - - ); - } + const isAgent = actionsKeys[i] === 'elasticAgent' || actionsKeys[i] === 'beats'; + const key = isAgent ? 'empty-page-agent-action' : `empty-page-${actionsKeys[i]}-action`; + return isAgent ? ( + + ) : ( + + ); }); }, [actions, sortedData, actionsKeys]); + const title = + pageTitle || + i18n.translate('kibana-react.noDataPage.welcomeTitle', { + defaultMessage: 'Welcome to Elastic {solution}!', + values: { solution }, + }); + return (
- - - -

- {pageTitle || ( - - )} -

- -

- - - - ), - }} - /> -

-
-
- - - - {renderActions} - + {actionsKeys.length > 1 ? ( <> diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/__snapshots__/no_data_page_body.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/__snapshots__/no_data_page_body.test.tsx.snap new file mode 100644 index 00000000000000..034e716cb6dce0 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/__snapshots__/no_data_page_body.test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NoDataPageBody render 1`] = ` + + + + +

+ +

+ + + , + "solution": "Elastic", + } + } + /> +

+
+ + + + +

, + ] + } + /> + +`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/index.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/index.tsx new file mode 100644 index 00000000000000..a5312d696139dc --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { NoDataPageBody } from './no_data_page_body'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/no_data_page_body.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/no_data_page_body.test.tsx new file mode 100644 index 00000000000000..f3419a47f63b8d --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/no_data_page_body.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { NoDataPageBody } from './no_data_page_body'; +import React, { ReactElement } from 'react'; +import { shallowWithIntl } from '@kbn/test-jest-helpers'; +import { NoDataCard } from '../no_data_card'; + +describe('NoDataPageBody', () => { + const action = { + recommended: false, + button: 'Button text', + onClick: jest.fn(), + }; + const el = ; + const actionCards: ReactElement[] = []; + actionCards.push(
{el}
); + test('render', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/no_data_page_body.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/no_data_page_body.tsx new file mode 100644 index 00000000000000..67e123de68885b --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page_body/no_data_page_body.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 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 { EuiLink, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; +import React, { ReactElement } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { NoDataPageProps } from '../no_data_page'; +import { KibanaPageTemplateSolutionNavAvatar } from '../../solution_nav'; +import { ActionCards } from '../action_cards'; +import { ElasticAgentCard, NoDataCard } from '../no_data_card'; + +type NoDataPageBodyProps = { + actionCards: Array | ReactElement>; +} & Omit; + +export const NoDataPageBody = (props: NoDataPageBodyProps) => { + const { pageTitle, docsLink, solution, actionCards, logo } = props; + + return ( + <> + + + +

{pageTitle}

+ +

+ + + + ), + }} + /> +

+
+
+ + + + ); +}; diff --git a/src/plugins/kibana_react/public/page_template/page_template.test.tsx b/src/plugins/kibana_react/public/page_template/page_template.test.tsx index 6c6c4bb33e6bb7..aff6082902a34e 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.test.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, render } from 'enzyme'; import { KibanaPageTemplate, KibanaPageTemplateProps } from './page_template'; import { EuiEmptyPrompt } from '@elastic/eui'; import { KibanaPageTemplateSolutionNavProps } from './solution_nav'; @@ -104,7 +104,7 @@ describe('KibanaPageTemplate', () => { }); test('render basic template', () => { - const component = shallow( + const component = render( { }); test('render solutionNav', () => { - const component = shallow( + const component = render( { /> ); expect(component).toMatchSnapshot(); + expect(component.find('div.kbnPageTemplate__pageSideBar').length).toBe(1); }); test('render noDataContent', () => { @@ -167,8 +168,6 @@ describe('KibanaPageTemplate', () => { pageSideBarProps={{ className: 'customClass' }} /> ); - expect(component.prop('pageSideBarProps').className).toEqual( - 'kbnPageTemplate__pageSideBar customClass' - ); + expect(component.html().includes('kbnPageTemplate__pageSideBar customClass')).toBe(true); }); }); diff --git a/src/plugins/kibana_react/public/page_template/page_template.tsx b/src/plugins/kibana_react/public/page_template/page_template.tsx index cf2b27c3b00dab..77469b240a19da 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.tsx @@ -6,25 +6,18 @@ * Side Public License, v 1. */ -/* eslint-disable @typescript-eslint/naming-convention */ import './page_template.scss'; -import React, { FunctionComponent, useState } from 'react'; -import classNames from 'classnames'; +import React, { FunctionComponent } from 'react'; +import { EuiPageTemplateProps } from '@elastic/eui'; +import { KibanaPageTemplateSolutionNavProps } from './solution_nav'; import { - EuiEmptyPrompt, - EuiPageTemplate, - EuiPageTemplateProps, - useIsWithinBreakpoints, -} from '@elastic/eui'; - -import { - KibanaPageTemplateSolutionNav, - KibanaPageTemplateSolutionNavProps, -} from './solution_nav/solution_nav'; - -import { NoDataPage, NoDataPageProps, NO_DATA_PAGE_TEMPLATE_PROPS } from './no_data_page'; + NoDataPageProps, + NoDataConfigPage, + NoDataConfigPageWithSolutionNavBar, +} from './no_data_page'; +import { KibanaPageTemplateInner, KibanaPageTemplateWithSolutionNav } from './page_template_inner'; /** * A thin wrapper around EuiPageTemplate with a few Kibana specific additions @@ -51,119 +44,53 @@ export type KibanaPageTemplateProps = EuiPageTemplateProps & { export const KibanaPageTemplate: FunctionComponent = ({ template, className, - pageHeader, children, - isEmptyState, - restrictWidth = true, - pageSideBar, - pageSideBarProps, solutionNav, noDataConfig, ...rest }) => { /** - * Only default to open in large+ breakpoints - */ - const isMediumBreakpoint = useIsWithinBreakpoints(['m']); - const isLargerBreakpoint = useIsWithinBreakpoints(['l', 'xl']); - - /** - * Create the solution nav component + * If passing the custom template of `noDataConfig` */ - const [isSideNavOpenOnDesktop, setisSideNavOpenOnDesktop] = useState( - JSON.parse(String(localStorage.getItem('solutionNavIsCollapsed'))) ? false : true - ); - const toggleOpenOnDesktop = () => { - setisSideNavOpenOnDesktop(!isSideNavOpenOnDesktop); - // Have to store it as the opposite of the default we want - localStorage.setItem('solutionNavIsCollapsed', JSON.stringify(isSideNavOpenOnDesktop)); - }; - let sideBarClasses = 'kbnPageTemplate__pageSideBar'; - if (solutionNav) { - // Only apply shrinking classes if collapsibility is available through `solutionNav` - sideBarClasses = classNames(sideBarClasses, { - 'kbnPageTemplate__pageSideBar--shrink': - isMediumBreakpoint || (isLargerBreakpoint && !isSideNavOpenOnDesktop), - }); - - pageSideBar = ( - ); } - /** - * An easy way to create the right content for empty pages - */ - const emptyStateDefaultTemplate = pageSideBar ? 'centeredContent' : 'centeredBody'; - if (isEmptyState && pageHeader && !children) { - template = template ?? emptyStateDefaultTemplate; - const { iconType, pageTitle, description, rightSideItems } = pageHeader; - pageHeader = undefined; - children = ( - {pageTitle} : undefined} - body={description ?

{description}

: undefined} - actions={rightSideItems} + if (noDataConfig) { + return ( + ); - } else if (isEmptyState && pageHeader && children) { - template = template ?? 'centeredContent'; - } else if (isEmptyState && !pageHeader) { - template = template ?? emptyStateDefaultTemplate; } - // Set the template before the classes - template = noDataConfig ? NO_DATA_PAGE_TEMPLATE_PROPS.template : template; - - const classes = classNames( - 'kbnPageTemplate', - { [`kbnPageTemplate--${template}`]: template }, - className - ); - - /** - * If passing the custom template of `noDataConfig` - */ - if (noDataConfig) { + if (solutionNav) { return ( - - - + className={className} + solutionNav={solutionNav} + children={children} + {...rest} + /> ); } return ( - - {children} - + /> ); }; diff --git a/src/plugins/kibana_react/public/page_template/page_template_inner.tsx b/src/plugins/kibana_react/public/page_template/page_template_inner.tsx new file mode 100644 index 00000000000000..3060a77c781c4d --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/page_template_inner.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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, { FunctionComponent } from 'react'; + +import { EuiEmptyPrompt, EuiPageTemplate } from '@elastic/eui'; +import { withSolutionNav } from './with_solution_nav'; +import { KibanaPageTemplateProps } from './page_template'; +import { getClasses } from './util'; + +type Props = KibanaPageTemplateProps; + +/** + * A thin wrapper around EuiPageTemplate with a few Kibana specific additions + */ +export const KibanaPageTemplateInner: FunctionComponent = ({ + template, + className, + pageHeader, + children, + isEmptyState, + ...rest +}) => { + /** + * An easy way to create the right content for empty pages + */ + const emptyStateDefaultTemplate = 'centeredBody'; + if (isEmptyState && pageHeader && !children) { + template = template ?? emptyStateDefaultTemplate; + const { iconType, pageTitle, description, rightSideItems } = pageHeader; + pageHeader = undefined; + children = ( + {pageTitle} : undefined} + body={description ?

{description}

: undefined} + actions={rightSideItems} + /> + ); + } else if (isEmptyState && pageHeader && children) { + template = template ?? 'centeredContent'; + } else if (isEmptyState && !pageHeader) { + template = template ?? emptyStateDefaultTemplate; + } + + const classes = getClasses(template, className); + + return ( + + {children} + + ); +}; + +export const KibanaPageTemplateWithSolutionNav = withSolutionNav(KibanaPageTemplateInner); diff --git a/src/plugins/kibana_react/public/page_template/util/constants.ts b/src/plugins/kibana_react/public/page_template/util/constants.ts new file mode 100644 index 00000000000000..159a6d0d8d4c15 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/util/constants.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPageTemplateProps } from '../page_template'; + +export const NO_DATA_PAGE_MAX_WIDTH = 950; + +export const NO_DATA_PAGE_TEMPLATE_PROPS: KibanaPageTemplateProps = { + restrictWidth: NO_DATA_PAGE_MAX_WIDTH, + template: 'centeredBody', + pageContentProps: { + hasShadow: false, + color: 'transparent', + }, +}; diff --git a/src/plugins/kibana_react/public/page_template/util/index.ts b/src/plugins/kibana_react/public/page_template/util/index.ts new file mode 100644 index 00000000000000..adfefdf8345664 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/util/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getClasses } from './presentation'; +export * from './constants'; diff --git a/src/plugins/kibana_react/public/page_template/util/presentation.ts b/src/plugins/kibana_react/public/page_template/util/presentation.ts new file mode 100644 index 00000000000000..ab7144ee37b579 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/util/presentation.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import classNames from 'classnames'; + +export const getClasses = (template: string | undefined, className: string | undefined) => { + return classNames('kbnPageTemplate', { [`kbnPageTemplate--${template}`]: template }, className); +}; diff --git a/src/plugins/kibana_react/public/page_template/with_solution_nav.tsx b/src/plugins/kibana_react/public/page_template/with_solution_nav.tsx new file mode 100644 index 00000000000000..5ec49b7c7cf29c --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/with_solution_nav.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 React, { ComponentType, useState } from 'react'; +import classNames from 'classnames'; +import { useIsWithinBreakpoints } from '@elastic/eui'; +import { EuiPageSideBarProps } from '@elastic/eui/src/components/page/page_side_bar'; +import { + KibanaPageTemplateSolutionNav, + KibanaPageTemplateSolutionNavProps, +} from '../page_template/solution_nav'; +import { KibanaPageTemplateProps } from '../page_template'; + +type SolutionNavProps = KibanaPageTemplateProps & { + solutionNav: KibanaPageTemplateSolutionNavProps; +}; + +const SOLUTION_NAV_COLLAPSED_KEY = 'solutionNavIsCollapsed'; + +export const withSolutionNav = (WrappedComponent: ComponentType) => { + const WithSolutionNav = (props: SolutionNavProps) => { + const isMediumBreakpoint = useIsWithinBreakpoints(['m']); + const isLargerBreakpoint = useIsWithinBreakpoints(['l', 'xl']); + const [isSideNavOpenOnDesktop, setisSideNavOpenOnDesktop] = useState( + !JSON.parse(String(localStorage.getItem(SOLUTION_NAV_COLLAPSED_KEY))) + ); + const { solutionNav, children, isEmptyState, template } = props; + const toggleOpenOnDesktop = () => { + setisSideNavOpenOnDesktop(!isSideNavOpenOnDesktop); + // Have to store it as the opposite of the default we want + localStorage.setItem(SOLUTION_NAV_COLLAPSED_KEY, JSON.stringify(isSideNavOpenOnDesktop)); + }; + const sideBarClasses = classNames( + 'kbnPageTemplate__pageSideBar', + { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'kbnPageTemplate__pageSideBar--shrink': + isMediumBreakpoint || (isLargerBreakpoint && !isSideNavOpenOnDesktop), + }, + props.pageSideBarProps?.className + ); + + const templateToUse = isEmptyState && !template ? 'centeredContent' : template; + + const pageSideBar = ( + + ); + const pageSideBarProps = { + paddingSize: 'none', + ...props.pageSideBarProps, + className: sideBarClasses, + } as EuiPageSideBarProps; // needed because for some reason 'none' is not recognized as a valid value for paddingSize + return ( + + {children} + + ); + }; + WithSolutionNav.displayName = `WithSolutionNavBar${WrappedComponent}`; + return WithSolutionNav; +}; 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/shared_ux/public/components/index.ts b/src/plugins/shared_ux/public/components/index.ts index 108b2f4b215484..c2f835b97ebde7 100644 --- a/src/plugins/shared_ux/public/components/index.ts +++ b/src/plugins/shared_ux/public/components/index.ts @@ -19,6 +19,12 @@ export const LazyExitFullScreenButton = React.lazy(() => })) ); +export const LazySolutionToolbarButton = React.lazy(() => + import('./toolbar/index').then(({ SolutionToolbarButton }) => ({ + default: SolutionToolbarButton, + })) +); + /** * A `ExitFullScreenButton` component that is wrapped by the `withSuspense` HOC. This component can * be used directly by consumers and will load the `LazyExitFullScreenButton` component lazily with @@ -26,6 +32,13 @@ export const LazyExitFullScreenButton = React.lazy(() => */ export const ExitFullScreenButton = withSuspense(LazyExitFullScreenButton); +/** + * A `SolutionToolbarButton` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `LazySolutionToolbarButton` component lazily with + * a predefined fallback and error boundary. + */ +export const SolutionToolbarButton = withSuspense(LazySolutionToolbarButton); + /** * The Lazily-loaded `NoDataViews` component. Consumers should use `React.Suspennse` or the * `withSuspense` HOC to load this component. diff --git a/src/plugins/shared_ux/public/components/toolbar/index.ts b/src/plugins/shared_ux/public/components/toolbar/index.ts new file mode 100644 index 00000000000000..de15e73eaadeb0 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { SolutionToolbarButton } from './solution_toolbar/button/primary'; diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/__snapshots__/primary.test.tsx.snap b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/__snapshots__/primary.test.tsx.snap new file mode 100644 index 00000000000000..1d7e3acb0b7628 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/__snapshots__/primary.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` is rendered 1`] = ` + + + + + + + + + +`; diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.mdx b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.mdx new file mode 100644 index 00000000000000..6693277b370ae5 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.mdx @@ -0,0 +1,12 @@ +--- +id: sharedUX/Components/SolutionToolbarButton +slug: /shared-ux/components/toolbar/solution_toolbar/button/primary +title: Solution Toolbar Button +summary: An opinionated implementation of the toolbar extracted to just the button. +tags: ['shared-ux', 'component'] +date: 2022-02-17 +--- + +> This documentation is in-progress. + +This button is a part of the solution toolbar component. This button has primary styling and requires a label. OnClick handlers and icon types are supported as an extension of EuiButtonProps. Icons are always on the left of any labels within the button. diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.stories.tsx b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.stories.tsx new file mode 100644 index 00000000000000..56c15ec7749af9 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.stories.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Story } from '@storybook/react'; +import React from 'react'; +import { SolutionToolbarButton } from './primary'; +import mdx from './primary.mdx'; + +export default { + title: 'Solution Toolbar Button', + description: 'A button that is a part of the solution toolbar.', + parameters: { + docs: { + page: mdx, + }, + }, + argTypes: { + iconType: { + control: { + type: 'radio', + expanded: true, + options: ['apps', 'logoGithub', 'folderCheck', 'documents'], + }, + }, + }, +}; + +export const Component: Story<{ + iconType: any; +}> = ({ iconType }) => { + return ; +}; + +Component.args = { + iconType: 'apps', +}; diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.test.tsx b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.test.tsx new file mode 100644 index 00000000000000..c2e5fd1ce7ab84 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { mount as enzymeMount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { ServicesProvider, SharedUXServices } from '../../../../services'; +import { servicesFactory } from '../../../../services/mocks'; + +import { SolutionToolbarButton } from './primary'; + +describe('', () => { + let services: SharedUXServices; + let mount: (element: JSX.Element) => ReactWrapper; + + beforeEach(() => { + services = servicesFactory(); + mount = (element: JSX.Element) => + enzymeMount({element}); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('is rendered', () => { + const component = mount(); + + expect(component).toMatchSnapshot(); + }); + test('it can be passed a functional onClick handler', () => { + const mockHandler = jest.fn(); + const component = mount(); + component.simulate('click'); + expect(mockHandler).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.tsx b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.tsx new file mode 100644 index 00000000000000..b99af852ed7e3d --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EuiButton } from '@elastic/eui'; +import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button'; + +export interface Props extends Pick { + label: string; +} + +export const SolutionToolbarButton = ({ label, ...rest }: Props) => { + return ( + + {label} + + ); +}; 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_default_editor/public/components/__snapshots__/agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap index b25444d16c46a2..dd9a9232692941 100644 --- a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap +++ b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap @@ -17,7 +17,7 @@ exports[`DefaultEditorAgg component should init with the default set of props 1` data-test-subj="visEditorAggAccordion1" element="div" extraAction={ -
+ -
+ } id="visEditorAggAccordion1" initialIsOpen={true} diff --git a/src/plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap index 373ff6b4c3ee41..c9c7b91e8fc137 100644 --- a/src/plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap +++ b/src/plugins/vis_default_editor/public/components/__snapshots__/agg_group.test.tsx.snap @@ -23,6 +23,7 @@ exports[`DefaultEditorAgg component should init with the default set of props 1` > { it('should not have actions', () => { const comp = shallow(); - const actions = shallow(comp.prop('extraAction')); + const actions = comp.prop('extraAction'); - expect(actions.children().exists()).toBeFalsy(); + expect(actions).toBeNull(); }); it('should have disable and remove actions', () => { diff --git a/src/plugins/vis_default_editor/public/components/agg.tsx b/src/plugins/vis_default_editor/public/components/agg.tsx index 0c1ddefa59e420..b813519d8caf99 100644 --- a/src/plugins/vis_default_editor/public/components/agg.tsx +++ b/src/plugins/vis_default_editor/public/components/agg.tsx @@ -13,7 +13,6 @@ import { EuiButtonIcon, EuiButtonIconProps, EuiSpacer, - EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -198,6 +197,7 @@ function DefaultEditorAgg({ if (isDraggable) { actionIcons.push({ id: 'dragHandle', + color: 'text', type: 'grab', tooltip: i18n.translate('visDefaultEditor.agg.modifyPriorityButtonTooltip', { defaultMessage: 'Modify priority of {schemaTitle} {aggTitle} by dragging', @@ -219,39 +219,23 @@ function DefaultEditorAgg({ dataTestSubj: 'removeDimensionBtn', }); } - return ( -
- {actionIcons.map((icon) => { - if (icon.id === 'dragHandle') { - return ( - - ); - } - - return ( - - - - ); - })} -
- ); + return actionIcons.length ? ( + <> + {actionIcons.map((icon) => ( + + + + ))} + + ) : null; }; const buttonContent = ( diff --git a/src/plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index 03b06056c649c9..4d2ae02777663f 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -153,6 +153,7 @@ function DefaultEditorAggGroup({ index={index} draggableId={`agg_group_dnd_${groupName}_${agg.id}`} customDragHandle={true} + disableInteractiveElementBlocking // Allows button to be drag handle > {(provided) => ( ; diff --git a/src/plugins/vis_types/gauge/jest.config.js b/src/plugins/vis_types/gauge/jest.config.js new file mode 100644 index 00000000000000..87fd58fd42dbcc --- /dev/null +++ b/src/plugins/vis_types/gauge/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/src/plugins/vis_types/gauge'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/gauge', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/vis_types/gauge/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/src/plugins/vis_types/gauge/kibana.json b/src/plugins/vis_types/gauge/kibana.json new file mode 100755 index 00000000000000..5eb2794452de90 --- /dev/null +++ b/src/plugins/vis_types/gauge/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "visTypeGauge", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["charts", "data", "expressions", "visualizations"], + "requiredBundles": ["visDefaultEditor"], + "optionalPlugins": ["expressionGauge"], + "extraPublicDirs": ["common/index"], + "owner": { + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" + }, + "description": "Contains the gauge chart implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy charts library advanced setting." +} diff --git a/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 00000000000000..dbc909f9ede22b --- /dev/null +++ b/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`gauge vis toExpressionAst function with minimal params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggs": Array [], + "index": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "id": Array [ + "123", + ], + }, + "function": "indexPatternLoad", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "esaggs", + "type": "function", + }, + Object { + "arguments": Object { + "centralMajorMode": Array [ + "custom", + ], + "colorMode": Array [ + "palette", + ], + "labelMajorMode": Array [ + "auto", + ], + "labelMinor": Array [ + "some custom sublabel", + ], + "metric": Array [], + "shape": Array [ + "circle", + ], + "ticksPosition": Array [ + "hidden", + ], + }, + "function": "gauge", + "type": "function", + }, + ], + "type": "expression", +} +`; diff --git a/src/plugins/vis_types/vislib/public/editor/collections.ts b/src/plugins/vis_types/gauge/public/editor/collections.ts similarity index 67% rename from src/plugins/vis_types/vislib/public/editor/collections.ts rename to src/plugins/vis_types/gauge/public/editor/collections.ts index e7905ccaf1c295..3f52ffbead01c1 100644 --- a/src/plugins/vis_types/vislib/public/editor/collections.ts +++ b/src/plugins/vis_types/gauge/public/editor/collections.ts @@ -7,21 +7,18 @@ */ import { i18n } from '@kbn/i18n'; - import { colorSchemas } from '../../../../charts/public'; -import { getPositions, getScaleTypes } from '../../../xy/public'; - import { Alignment, GaugeType } from '../types'; export const getGaugeTypes = () => [ { - text: i18n.translate('visTypeVislib.gauge.gaugeTypes.arcText', { + text: i18n.translate('visTypeGauge.gauge.gaugeTypes.arcText', { defaultMessage: 'Arc', }), value: GaugeType.Arc, }, { - text: i18n.translate('visTypeVislib.gauge.gaugeTypes.circleText', { + text: i18n.translate('visTypeGauge.gauge.gaugeTypes.circleText', { defaultMessage: 'Circle', }), value: GaugeType.Circle, @@ -30,19 +27,19 @@ export const getGaugeTypes = () => [ export const getAlignments = () => [ { - text: i18n.translate('visTypeVislib.gauge.alignmentAutomaticTitle', { + text: i18n.translate('visTypeGauge.gauge.alignmentAutomaticTitle', { defaultMessage: 'Automatic', }), value: Alignment.Automatic, }, { - text: i18n.translate('visTypeVislib.gauge.alignmentHorizontalTitle', { + text: i18n.translate('visTypeGauge.gauge.alignmentHorizontalTitle', { defaultMessage: 'Horizontal', }), value: Alignment.Horizontal, }, { - text: i18n.translate('visTypeVislib.gauge.alignmentVerticalTitle', { + text: i18n.translate('visTypeGauge.gauge.alignmentVerticalTitle', { defaultMessage: 'Vertical', }), value: Alignment.Vertical, @@ -54,9 +51,3 @@ export const getGaugeCollections = () => ({ alignments: getAlignments(), colorSchemas, }); - -export const getHeatmapCollections = () => ({ - legendPositions: getPositions(), - scales: getScaleTypes(), - colorSchemas, -}); diff --git a/src/plugins/vis_types/vislib/public/editor/components/gauge/index.tsx b/src/plugins/vis_types/gauge/public/editor/components/gauge/index.tsx similarity index 84% rename from src/plugins/vis_types/vislib/public/editor/components/gauge/index.tsx rename to src/plugins/vis_types/gauge/public/editor/components/gauge/index.tsx index 5a741ffbadd83f..8fbe8b1567ae3a 100644 --- a/src/plugins/vis_types/vislib/public/editor/components/gauge/index.tsx +++ b/src/plugins/vis_types/gauge/public/editor/components/gauge/index.tsx @@ -10,19 +10,21 @@ import React, { useCallback } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { GaugeVisParams } from '../../../gauge'; +import { GaugeTypeProps, GaugeVisParams } from '../../../types'; import { RangesPanel } from './ranges_panel'; import { StylePanel } from './style_panel'; import { LabelsPanel } from './labels_panel'; -export type GaugeOptionsInternalProps = VisEditorOptionsProps & { +export interface GaugeOptionsProps extends VisEditorOptionsProps, GaugeTypeProps {} + +export type GaugeOptionsInternalProps = GaugeOptionsProps & { setGaugeValue: ( paramName: T, value: GaugeVisParams['gauge'][T] ) => void; }; -function GaugeOptions(props: VisEditorOptionsProps) { +function GaugeOptions(props: GaugeOptionsProps) { const { stateParams, setValue } = props; const setGaugeValue: GaugeOptionsInternalProps['setGaugeValue'] = useCallback( @@ -37,13 +39,9 @@ function GaugeOptions(props: VisEditorOptionsProps) { return ( <> - - - - ); diff --git a/src/plugins/vis_types/vislib/public/editor/components/gauge/labels_panel.tsx b/src/plugins/vis_types/gauge/public/editor/components/gauge/labels_panel.tsx similarity index 87% rename from src/plugins/vis_types/vislib/public/editor/components/gauge/labels_panel.tsx rename to src/plugins/vis_types/gauge/public/editor/components/gauge/labels_panel.tsx index fb5c1594e601a1..087a43c5dd0059 100644 --- a/src/plugins/vis_types/vislib/public/editor/components/gauge/labels_panel.tsx +++ b/src/plugins/vis_types/gauge/public/editor/components/gauge/labels_panel.tsx @@ -19,7 +19,7 @@ function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInter

@@ -27,7 +27,7 @@ function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInter

@@ -66,13 +67,20 @@ function RangesPanel({ /> + ); + return (

@@ -35,7 +53,7 @@ function StylePanel({ aggs, setGaugeValue, stateParams }: GaugeOptionsInternalPr - - + {showElasticChartsOptions ? ( + <> + + + {alignmentSelect} + + + + ) : ( + alignmentSelect + )}
); } diff --git a/src/plugins/vis_types/vislib/public/editor/components/index.tsx b/src/plugins/vis_types/gauge/public/editor/components/index.tsx similarity index 64% rename from src/plugins/vis_types/vislib/public/editor/components/index.tsx rename to src/plugins/vis_types/gauge/public/editor/components/index.tsx index ab7e34b576e874..7cb1ca9a26c697 100644 --- a/src/plugins/vis_types/vislib/public/editor/components/index.tsx +++ b/src/plugins/vis_types/gauge/public/editor/components/index.tsx @@ -9,10 +9,11 @@ import React, { lazy } from 'react'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { GaugeVisParams } from '../../gauge'; +import { GaugeTypeProps, GaugeVisParams } from '../../types'; const GaugeOptionsLazy = lazy(() => import('./gauge')); -export const GaugeOptions = (props: VisEditorOptionsProps) => ( - -); +export const getGaugeOptions = + ({ showElasticChartsOptions }: GaugeTypeProps) => + (props: VisEditorOptionsProps) => + ; diff --git a/src/plugins/vis_types/vislib/public/editor/index.ts b/src/plugins/vis_types/gauge/public/editor/index.ts similarity index 100% rename from src/plugins/vis_types/vislib/public/editor/index.ts rename to src/plugins/vis_types/gauge/public/editor/index.ts diff --git a/src/plugins/vis_types/gauge/public/index.ts b/src/plugins/vis_types/gauge/public/index.ts new file mode 100755 index 00000000000000..78aa55f59486f7 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/index.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 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 { VisTypeGaugePlugin } from './plugin'; + +export function plugin() { + return new VisTypeGaugePlugin(); +} + +export type { VisTypeGaugePluginSetup, VisTypeGaugePluginStart } from './types'; + +export { gaugeVisType, goalVisType } from './vis_type'; diff --git a/src/plugins/vis_types/gauge/public/plugin.ts b/src/plugins/vis_types/gauge/public/plugin.ts new file mode 100755 index 00000000000000..8c11892192766c --- /dev/null +++ b/src/plugins/vis_types/gauge/public/plugin.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { VisualizationsSetup } from '../../../../plugins/visualizations/public'; +import { DataPublicPluginStart } from '../../../../plugins/data/public'; +import { CoreSetup } from '../../../../core/public'; +import { LEGACY_GAUGE_CHARTS_LIBRARY } from '../common'; +import { VisTypeGaugePluginSetup } from './types'; +import { gaugeVisType, goalVisType } from './vis_type'; + +/** @internal */ +export interface VisTypeGaugeSetupDependencies { + visualizations: VisualizationsSetup; +} + +/** @internal */ +export interface VisTypePiePluginStartDependencies { + data: DataPublicPluginStart; +} + +export class VisTypeGaugePlugin { + public setup( + core: CoreSetup, + { visualizations }: VisTypeGaugeSetupDependencies + ): VisTypeGaugePluginSetup { + if (!core.uiSettings.get(LEGACY_GAUGE_CHARTS_LIBRARY)) { + const visTypeProps = { showElasticChartsOptions: true }; + visualizations.createBaseVisualization(gaugeVisType(visTypeProps)); + visualizations.createBaseVisualization(goalVisType(visTypeProps)); + } + + return {}; + } + + public start() {} +} diff --git a/src/plugins/vis_types/gauge/public/to_ast.test.ts b/src/plugins/vis_types/gauge/public/to_ast.test.ts new file mode 100644 index 00000000000000..4f76e8e5f727e4 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/to_ast.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TimefilterContract } from 'src/plugins/data/public'; +import { Vis } from 'src/plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { GaugeVisParams } from './types'; + +describe('gauge vis toExpressionAst function', () => { + let vis: Vis; + + beforeEach(() => { + vis = { + isHierarchical: () => false, + type: {}, + params: { + gauge: { + gaugeType: 'Circle', + scale: { + show: false, + labels: false, + color: 'rgba(105,112,125,0.2)', + }, + labels: { + show: true, + }, + style: { + subText: 'some custom sublabel', + }, + }, + }, + data: { + indexPattern: { id: '123' } as any, + aggs: { + getResponseAggs: () => [], + aggs: [], + } as any, + }, + } as unknown as Vis; + }); + + it('with minimal params', () => { + const actual = toExpressionAst(vis, { + timefilter: {} as TimefilterContract, + }); + expect(actual).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_types/gauge/public/to_ast.ts b/src/plugins/vis_types/gauge/public/to_ast.ts new file mode 100644 index 00000000000000..041ae765b76969 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/to_ast.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import type { + GaugeExpressionFunctionDefinition, + GaugeShape, +} from '../../../chart_expressions/expression_gauge/common'; +import { GaugeType, GaugeVisParams } from './types'; +import { getStopsWithColorsFromRanges } from './utils'; +import { getEsaggsFn } from './to_ast_esaggs'; + +const prepareDimension = (params: SchemaConfig) => { + const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor }); + + if (params.format) { + visdimension.addArgument('format', params.format.id); + visdimension.addArgument('formatParams', JSON.stringify(params.format.params)); + } + + return buildExpression([visdimension]); +}; + +const gaugeTypeToShape = (type: GaugeType): GaugeShape => { + const arc: GaugeShape = 'arc'; + const circle: GaugeShape = 'circle'; + + return { + [GaugeType.Arc]: arc, + [GaugeType.Circle]: circle, + }[type]; +}; + +export const toExpressionAst: VisToExpressionAst = (vis, params) => { + const schemas = getVisSchemas(vis, params); + + const { + gaugeType, + percentageMode, + percentageFormatPattern, + colorSchema, + colorsRange, + invertColors, + scale, + style, + labels, + } = vis.params.gauge; + + // fix formatter for percentage mode + if (percentageMode === true) { + schemas.metric.forEach((metric: SchemaConfig) => { + metric.format = { + id: 'percent', + params: { pattern: percentageFormatPattern }, + }; + }); + } + + const centralMajorMode = labels.show ? (style.subText ? 'custom' : 'auto') : 'none'; + const gauge = buildExpressionFunction('gauge', { + shape: gaugeTypeToShape(gaugeType), + metric: schemas.metric.map(prepareDimension), + ticksPosition: scale.show ? 'auto' : 'hidden', + labelMajorMode: 'auto', + colorMode: 'palette', + centralMajorMode, + ...(centralMajorMode === 'custom' ? { labelMinor: style.subText } : {}), + }); + + if (colorsRange && colorsRange.length) { + const stopsWithColors = getStopsWithColorsFromRanges(colorsRange, colorSchema, invertColors); + const palette = buildExpressionFunction('palette', { + ...stopsWithColors, + range: percentageMode ? 'percent' : 'number', + continuity: 'none', + gradient: true, + rangeMax: percentageMode ? 100 : Infinity, + rangeMin: 0, + }); + + gauge.addArgument('palette', buildExpression([palette])); + } + + const ast = buildExpression([getEsaggsFn(vis), gauge]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_types/gauge/public/to_ast_esaggs.ts b/src/plugins/vis_types/gauge/public/to_ast_esaggs.ts new file mode 100644 index 00000000000000..ecf3f3e6371777 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/to_ast_esaggs.ts @@ -0,0 +1,33 @@ +/* + * Copyright 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 { Vis } from '../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import { + EsaggsExpressionFunctionDefinition, + IndexPatternLoadExpressionFunctionDefinition, +} from '../../../data/public'; + +import { GaugeVisParams } from './types'; + +/** + * Get esaggs expressions function + * @param vis + */ +export function getEsaggsFn(vis: Vis) { + return buildExpressionFunction('esaggs', { + index: buildExpression([ + buildExpressionFunction('indexPatternLoad', { + id: vis.data.indexPattern!.id!, + }), + ]), + metricsAtAllLevels: vis.isHierarchical(), + partialRows: false, + aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), + }); +} diff --git a/src/plugins/vis_types/gauge/public/types.ts b/src/plugins/vis_types/gauge/public/types.ts new file mode 100755 index 00000000000000..c160b2ccf2f3f0 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/types.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { $Values } from '@kbn/utility-types'; +import { Range } from '../../../expressions/public'; +import { ColorSchemaParams, Labels, Style } from '../../../charts/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisTypeGaugePluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisTypeGaugePluginStart {} + +/** + * Gauge title alignment + */ +export const Alignment = { + Automatic: 'automatic', + Horizontal: 'horizontal', + Vertical: 'vertical', +} as const; + +export type Alignment = $Values; + +export const GaugeType = { + Arc: 'Arc', + Circle: 'Circle', +} as const; + +export type GaugeType = $Values; + +export interface Gauge extends ColorSchemaParams { + backStyle: 'Full'; + gaugeStyle: 'Full'; + orientation: 'vertical'; + type: 'meter'; + alignment: Alignment; + colorsRange: Range[]; + extendRange: boolean; + gaugeType: GaugeType; + labels: Labels; + percentageMode: boolean; + percentageFormatPattern?: string; + outline?: boolean; + scale: { + show: boolean; + labels: false; + color: 'rgba(105,112,125,0.2)'; + }; + style: Style; +} + +export interface GaugeVisParams { + type: 'gauge'; + addTooltip: boolean; + addLegend: boolean; + isDisplayWarning: boolean; + gauge: Gauge; +} + +export interface GaugeTypeProps { + showElasticChartsOptions?: boolean; +} diff --git a/src/plugins/vis_types/gauge/public/utils/index.ts b/src/plugins/vis_types/gauge/public/utils/index.ts new file mode 100644 index 00000000000000..fb23c97d835fe0 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getStopsWithColorsFromRanges } from './palette'; diff --git a/src/plugins/vis_types/gauge/public/utils/palette.ts b/src/plugins/vis_types/gauge/public/utils/palette.ts new file mode 100644 index 00000000000000..a236a0daa6d533 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/utils/palette.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ColorSchemas, getHeatmapColors } from '../../../../charts/common'; +import { Range } from '../../../../expressions'; + +export interface PaletteConfig { + color: Array; + stop: number[]; +} + +const TRANSPARENT = 'rgb(0, 0, 0, 0)'; + +const getColor = ( + index: number, + elementsCount: number, + colorSchema: ColorSchemas, + invertColors: boolean = false +) => { + const divider = Math.max(elementsCount - 1, 1); + const value = invertColors ? 1 - index / divider : index / divider; + return getHeatmapColors(value, colorSchema); +}; + +export const getStopsWithColorsFromRanges = ( + ranges: Range[], + colorSchema: ColorSchemas, + invertColors: boolean = false +) => { + return ranges.reduce( + (acc, range, index, rangesArr) => { + if (index && range.from !== rangesArr[index - 1].to) { + acc.color.push(TRANSPARENT); + acc.stop.push(range.from); + } + + acc.color.push(getColor(index, rangesArr.length, colorSchema, invertColors)); + acc.stop.push(range.to); + + return acc; + }, + { color: [], stop: [] } + ); +}; diff --git a/src/plugins/vis_types/gauge/public/vis_type/gauge.tsx b/src/plugins/vis_types/gauge/public/vis_type/gauge.tsx new file mode 100644 index 00000000000000..648d34cdee7bdb --- /dev/null +++ b/src/plugins/vis_types/gauge/public/vis_type/gauge.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ColorMode, ColorSchemas } from '../../../../charts/public'; +import { AggGroupNames } from '../../../../data/public'; +import { VisTypeDefinition, VIS_EVENT_TO_TRIGGER } from '../../../../visualizations/public'; + +import { Alignment, GaugeType, GaugeTypeProps } from '../types'; +import { toExpressionAst } from '../to_ast'; +import { getGaugeOptions } from '../editor/components'; +import { GaugeVisParams } from '../types'; +import { SplitTooltip } from './split_tooltip'; + +export const getGaugeVisTypeDefinition = ( + props: GaugeTypeProps +): VisTypeDefinition => ({ + name: 'gauge', + title: i18n.translate('visTypeGauge.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), + icon: 'visGauge', + description: i18n.translate('visTypeGauge.gauge.gaugeDescription', { + defaultMessage: 'Show the status of a metric.', + }), + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], + toExpressionAst, + visConfig: { + defaults: { + type: 'gauge', + addTooltip: true, + addLegend: true, + isDisplayWarning: false, + gauge: { + alignment: Alignment.Automatic, + extendRange: true, + percentageMode: false, + gaugeType: GaugeType.Arc, + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: ColorSchemas.GreenToRed, + gaugeColorMode: ColorMode.Labels, + colorsRange: [ + { from: 0, to: 50 }, + { from: 50, to: 75 }, + { from: 75, to: 100 }, + ], + invertColors: false, + labels: { + show: true, + color: 'black', + }, + scale: { + show: true, + labels: false, + color: 'rgba(105,112,125,0.2)', + }, + type: 'meter', + style: { + bgWidth: 0.9, + width: 0.9, + mask: false, + bgMask: false, + maskBars: 50, + bgFill: 'rgba(105,112,125,0.2)', + bgColor: true, + subText: '', + fontSize: 60, + }, + }, + }, + }, + editorConfig: { + optionsTemplate: getGaugeOptions(props), + schemas: [ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeGauge.gauge.metricTitle', { defaultMessage: 'Metric' }), + min: 1, + ...(props.showElasticChartsOptions ? { max: 1 } : {}), + aggFilter: [ + '!std_dev', + '!geo_centroid', + '!percentiles', + '!percentile_ranks', + '!derivative', + '!serial_diff', + '!moving_avg', + '!cumulative_sum', + '!geo_bounds', + '!filtered_metric', + '!single_percentile', + ], + defaults: [{ schema: 'metric', type: 'count' }], + }, + { + group: AggGroupNames.Buckets, + name: 'group', + // TODO: Remove when split chart aggs are supported + ...(props.showElasticChartsOptions && { + disabled: true, + tooltip: , + }), + title: i18n.translate('visTypeGauge.gauge.groupTitle', { + defaultMessage: 'Split group', + }), + min: 0, + max: 1, + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!rare_terms', + '!multi_terms', + '!significant_text', + ], + }, + ], + }, + requiresSearch: true, +}); diff --git a/src/plugins/vis_types/gauge/public/vis_type/goal.tsx b/src/plugins/vis_types/gauge/public/vis_type/goal.tsx new file mode 100644 index 00000000000000..e56e87ee70dff8 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/vis_type/goal.tsx @@ -0,0 +1,122 @@ +/* + * Copyright 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 { AggGroupNames } from '../../../../data/public'; +import { ColorMode, ColorSchemas } from '../../../../charts/public'; +import { VisTypeDefinition } from '../../../../visualizations/public'; + +import { getGaugeOptions } from '../editor/components'; +import { toExpressionAst } from '../to_ast'; +import { GaugeVisParams, GaugeType, GaugeTypeProps } from '../types'; +import { SplitTooltip } from './split_tooltip'; + +export const getGoalVisTypeDefinition = ( + props: GaugeTypeProps +): VisTypeDefinition => ({ + name: 'goal', + title: i18n.translate('visTypeGauge.goal.goalTitle', { defaultMessage: 'Goal' }), + icon: 'visGoal', + description: i18n.translate('visTypeGauge.goal.goalDescription', { + defaultMessage: 'Track how a metric progresses to a goal.', + }), + toExpressionAst, + visConfig: { + defaults: { + addTooltip: true, + addLegend: false, + isDisplayWarning: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: true, + gaugeType: GaugeType.Arc, + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + useRanges: false, + colorSchema: ColorSchemas.GreenToRed, + gaugeColorMode: ColorMode.None, + colorsRange: [{ from: 0, to: 10000 }], + invertColors: false, + labels: { + show: true, + color: 'black', + }, + scale: { + show: false, + labels: false, + color: 'rgba(105,112,125,0.2)', + width: 2, + }, + type: 'meter', + style: { + bgFill: 'rgba(105,112,125,0.2)', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 60, + }, + }, + }, + }, + editorConfig: { + optionsTemplate: getGaugeOptions(props), + schemas: [ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeGauge.goal.metricTitle', { defaultMessage: 'Metric' }), + min: 1, + ...(props.showElasticChartsOptions ? { max: 1 } : {}), + aggFilter: [ + '!std_dev', + '!geo_centroid', + '!percentiles', + '!percentile_ranks', + '!derivative', + '!serial_diff', + '!moving_avg', + '!cumulative_sum', + '!geo_bounds', + '!filtered_metric', + '!single_percentile', + ], + defaults: [{ schema: 'metric', type: 'count' }], + }, + { + group: AggGroupNames.Buckets, + name: 'group', + // TODO: Remove when split chart aggs are supported + ...(props.showElasticChartsOptions && { + disabled: true, + tooltip: , + }), + title: i18n.translate('visTypeGauge.goal.groupTitle', { + defaultMessage: 'Split group', + }), + min: 0, + max: 1, + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!rare_terms', + '!multi_terms', + '!significant_text', + ], + }, + ], + }, + requiresSearch: true, +}); diff --git a/src/plugins/vis_types/gauge/public/vis_type/index.ts b/src/plugins/vis_types/gauge/public/vis_type/index.ts new file mode 100644 index 00000000000000..cc78afedc02bd0 --- /dev/null +++ b/src/plugins/vis_types/gauge/public/vis_type/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { GaugeTypeProps } from '../types'; +import { getGaugeVisTypeDefinition } from './gauge'; +import { getGoalVisTypeDefinition } from './goal'; + +export const gaugeVisType = (props: GaugeTypeProps) => { + return getGaugeVisTypeDefinition(props); +}; + +export const goalVisType = (props: GaugeTypeProps) => { + return getGoalVisTypeDefinition(props); +}; diff --git a/src/plugins/vis_types/gauge/public/vis_type/split_tooltip.tsx b/src/plugins/vis_types/gauge/public/vis_type/split_tooltip.tsx new file mode 100644 index 00000000000000..8c92b6d65ff77e --- /dev/null +++ b/src/plugins/vis_types/gauge/public/vis_type/split_tooltip.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; + +import { FormattedMessage } from '@kbn/i18n-react'; + +export function SplitTooltip() { + return ( + + ); +} diff --git a/src/plugins/vis_types/gauge/server/index.ts b/src/plugins/vis_types/gauge/server/index.ts new file mode 100755 index 00000000000000..8d958e63356e2a --- /dev/null +++ b/src/plugins/vis_types/gauge/server/index.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 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 { PluginConfigDescriptor } from 'src/core/server'; +import { configSchema, ConfigSchema } from '../config'; +import { VisTypeGaugeServerPlugin } from './plugin'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; + +export const plugin = () => new VisTypeGaugeServerPlugin(); diff --git a/src/plugins/vis_types/gauge/server/plugin.ts b/src/plugins/vis_types/gauge/server/plugin.ts new file mode 100755 index 00000000000000..0334f963c720c1 --- /dev/null +++ b/src/plugins/vis_types/gauge/server/plugin.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { schema } from '@kbn/config-schema'; + +import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; + +import { LEGACY_GAUGE_CHARTS_LIBRARY } from '../common'; + +export const getUiSettingsConfig: () => Record> = () => ({ + [LEGACY_GAUGE_CHARTS_LIBRARY]: { + name: i18n.translate( + 'visTypeGauge.advancedSettings.visualization.legacyGaugeChartsLibrary.name', + { + defaultMessage: 'Gauge legacy charts library', + } + ), + requiresPageReload: true, + value: true, + description: i18n.translate( + 'visTypeGauge.advancedSettings.visualization.legacyGaugeChartsLibrary.description', + { + defaultMessage: 'Enables legacy charts library for gauge charts in visualize.', + } + ), + category: ['visualization'], + schema: schema.boolean(), + }, +}); + +export class VisTypeGaugeServerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(getUiSettingsConfig()); + + return {}; + } + + public start() { + return {}; + } +} diff --git a/src/plugins/vis_types/gauge/tsconfig.json b/src/plugins/vis_types/gauge/tsconfig.json new file mode 100644 index 00000000000000..b1717173757e7d --- /dev/null +++ b/src/plugins/vis_types/gauge/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../../core/tsconfig.json" }, + { "path": "../../charts/tsconfig.json" }, + { "path": "../../data/tsconfig.json" }, + { "path": "../../expressions/tsconfig.json" }, + { "path": "../../chart_expressions/expression_gauge/tsconfig.json" }, + { "path": "../../visualizations/tsconfig.json" }, + { "path": "../../usage_collection/tsconfig.json" }, + { "path": "../../vis_default_editor/tsconfig.json" }, + { "path": "../../field_formats/tsconfig.json" }, + { "path": "../../chart_expressions/expression_partition_vis/tsconfig.json" } + ] + } \ No newline at end of file diff --git a/src/plugins/vis_types/vislib/kibana.json b/src/plugins/vis_types/vislib/kibana.json index feb252f1bb0f56..7c55aba21e7e47 100644 --- a/src/plugins/vis_types/vislib/kibana.json +++ b/src/plugins/vis_types/vislib/kibana.json @@ -4,7 +4,7 @@ "server": true, "ui": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations"], - "requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy", "visTypePie", "visTypeHeatmap", "fieldFormats", "kibanaReact"], + "requiredBundles": ["kibanaUtils", "visTypeXy", "visTypePie", "visTypeHeatmap", "visTypeGauge", "fieldFormats", "kibanaReact"], "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" diff --git a/src/plugins/vis_types/vislib/public/gauge.ts b/src/plugins/vis_types/vislib/public/gauge.ts index 128c0758bfd034..5edc33edb84fae 100644 --- a/src/plugins/vis_types/vislib/public/gauge.ts +++ b/src/plugins/vis_types/vislib/public/gauge.ts @@ -6,16 +6,13 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; - -import { ColorMode, ColorSchemas, ColorSchemaParams, Labels, Style } from '../../../charts/public'; +import { ColorSchemaParams, Labels, Style } from '../../../charts/public'; import { RangeValues } from '../../../vis_default_editor/public'; -import { AggGroupNames } from '../../../data/public'; -import { VisTypeDefinition, VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; +import { gaugeVisType } from '../../gauge/public'; +import { VisTypeDefinition } from '../../../visualizations/public'; -import { Alignment, GaugeType, VislibChartType } from './types'; +import { Alignment, GaugeType } from './types'; import { toExpressionAst } from './to_ast'; -import { GaugeOptions } from './editor/components'; export interface Gauge extends ColorSchemaParams { backStyle: 'Full'; @@ -46,104 +43,7 @@ export interface GaugeVisParams { gauge: Gauge; } -export const gaugeVisTypeDefinition: VisTypeDefinition = { - name: 'gauge', - title: i18n.translate('visTypeVislib.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), - icon: 'visGauge', - description: i18n.translate('visTypeVislib.gauge.gaugeDescription', { - defaultMessage: 'Show the status of a metric.', - }), - getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], +export const gaugeVisTypeDefinition = { + ...gaugeVisType({}), toExpressionAst, - visConfig: { - defaults: { - type: VislibChartType.Gauge, - addTooltip: true, - addLegend: true, - isDisplayWarning: false, - gauge: { - alignment: Alignment.Automatic, - extendRange: true, - percentageMode: false, - gaugeType: GaugeType.Arc, - gaugeStyle: 'Full', - backStyle: 'Full', - orientation: 'vertical', - colorSchema: ColorSchemas.GreenToRed, - gaugeColorMode: ColorMode.Labels, - colorsRange: [ - { from: 0, to: 50 }, - { from: 50, to: 75 }, - { from: 75, to: 100 }, - ], - invertColors: false, - labels: { - show: true, - color: 'black', - }, - scale: { - show: true, - labels: false, - color: 'rgba(105,112,125,0.2)', - }, - type: 'meter', - style: { - bgWidth: 0.9, - width: 0.9, - mask: false, - bgMask: false, - maskBars: 50, - bgFill: 'rgba(105,112,125,0.2)', - bgColor: true, - subText: '', - fontSize: 60, - }, - }, - }, - }, - editorConfig: { - optionsTemplate: GaugeOptions, - schemas: [ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.gauge.metricTitle', { defaultMessage: 'Metric' }), - min: 1, - aggFilter: [ - '!std_dev', - '!geo_centroid', - '!percentiles', - '!percentile_ranks', - '!derivative', - '!serial_diff', - '!moving_avg', - '!cumulative_sum', - '!geo_bounds', - '!filtered_metric', - '!single_percentile', - ], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.gauge.groupTitle', { - defaultMessage: 'Split group', - }), - min: 0, - max: 1, - aggFilter: [ - '!geohash_grid', - '!geotile_grid', - '!filter', - '!sampler', - '!diversified_sampler', - '!rare_terms', - '!multi_terms', - '!significant_text', - ], - }, - ], - }, - requiresSearch: true, -}; +} as VisTypeDefinition; diff --git a/src/plugins/vis_types/vislib/public/goal.ts b/src/plugins/vis_types/vislib/public/goal.ts index 9dd5fdbc92b5f0..205b3a7a4280a0 100644 --- a/src/plugins/vis_types/vislib/public/goal.ts +++ b/src/plugins/vis_types/vislib/public/goal.ts @@ -6,108 +6,13 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; - -import { AggGroupNames } from '../../../data/public'; -import { ColorMode, ColorSchemas } from '../../../charts/public'; import { VisTypeDefinition } from '../../../visualizations/public'; +import { goalVisType } from '../../gauge/public'; -import { GaugeOptions } from './editor'; import { toExpressionAst } from './to_ast'; -import { GaugeType } from './types'; import { GaugeVisParams } from './gauge'; -export const goalVisTypeDefinition: VisTypeDefinition = { - name: 'goal', - title: i18n.translate('visTypeVislib.goal.goalTitle', { defaultMessage: 'Goal' }), - icon: 'visGoal', - description: i18n.translate('visTypeVislib.goal.goalDescription', { - defaultMessage: 'Track how a metric progresses to a goal.', - }), +export const goalVisTypeDefinition = { + ...goalVisType({}), toExpressionAst, - visConfig: { - defaults: { - addTooltip: true, - addLegend: false, - isDisplayWarning: false, - type: 'gauge', - gauge: { - verticalSplit: false, - autoExtend: false, - percentageMode: true, - gaugeType: GaugeType.Arc, - gaugeStyle: 'Full', - backStyle: 'Full', - orientation: 'vertical', - useRanges: false, - colorSchema: ColorSchemas.GreenToRed, - gaugeColorMode: ColorMode.None, - colorsRange: [{ from: 0, to: 10000 }], - invertColors: false, - labels: { - show: true, - color: 'black', - }, - scale: { - show: false, - labels: false, - color: 'rgba(105,112,125,0.2)', - width: 2, - }, - type: 'meter', - style: { - bgFill: 'rgba(105,112,125,0.2)', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 60, - }, - }, - }, - }, - editorConfig: { - optionsTemplate: GaugeOptions, - schemas: [ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.goal.metricTitle', { defaultMessage: 'Metric' }), - min: 1, - aggFilter: [ - '!std_dev', - '!geo_centroid', - '!percentiles', - '!percentile_ranks', - '!derivative', - '!serial_diff', - '!moving_avg', - '!cumulative_sum', - '!geo_bounds', - '!filtered_metric', - '!single_percentile', - ], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.goal.groupTitle', { - defaultMessage: 'Split group', - }), - min: 0, - max: 1, - aggFilter: [ - '!geohash_grid', - '!geotile_grid', - '!filter', - '!sampler', - '!diversified_sampler', - '!rare_terms', - '!multi_terms', - '!significant_text', - ], - }, - ], - }, - requiresSearch: true, -}; +} as VisTypeDefinition; diff --git a/src/plugins/vis_types/vislib/public/plugin.ts b/src/plugins/vis_types/vislib/public/plugin.ts index 8c54df99bb9888..23013bc582387e 100644 --- a/src/plugins/vis_types/vislib/public/plugin.ts +++ b/src/plugins/vis_types/vislib/public/plugin.ts @@ -14,13 +14,16 @@ import { ChartsPluginSetup } from '../../../charts/public'; import { DataPublicPluginStart } from '../../../data/public'; import { LEGACY_PIE_CHARTS_LIBRARY } from '../../pie/common/index'; import { LEGACY_HEATMAP_CHARTS_LIBRARY } from '../../heatmap/common/index'; +import { LEGACY_GAUGE_CHARTS_LIBRARY } from '../../gauge/common/index'; import { heatmapVisTypeDefinition } from './heatmap'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; import { createPieVisFn } from './pie_fn'; -import { visLibVisTypeDefinitions, pieVisTypeDefinition } from './vis_type_vislib_vis_types'; +import { pieVisTypeDefinition } from './pie'; import { setFormatService, setDataActions, setTheme } from './services'; import { getVislibVisRenderer } from './vis_renderer'; +import { gaugeVisTypeDefinition } from './gauge'; +import { goalVisTypeDefinition } from './goal'; /** @internal */ export interface VisTypeVislibPluginSetupDependencies { @@ -48,7 +51,7 @@ export class VisTypeVislibPlugin { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { // register vislib XY axis charts - visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization); + expressions.registerRenderer(getVislibVisRenderer(core, charts)); expressions.registerFunction(createVisTypeVislibVisFn()); @@ -57,10 +60,16 @@ export class VisTypeVislibPlugin visualizations.createBaseVisualization(pieVisTypeDefinition); expressions.registerFunction(createPieVisFn()); } + if (core.uiSettings.get(LEGACY_HEATMAP_CHARTS_LIBRARY)) { // register vislib heatmap chart visualizations.createBaseVisualization(heatmapVisTypeDefinition); - expressions.registerFunction(createVisTypeVislibVisFn()); + } + + if (core.uiSettings.get(LEGACY_GAUGE_CHARTS_LIBRARY)) { + // register vislib gauge and goal charts + visualizations.createBaseVisualization(gaugeVisTypeDefinition); + visualizations.createBaseVisualization(goalVisTypeDefinition); } } diff --git a/src/plugins/vis_types/vislib/tsconfig.json b/src/plugins/vis_types/vislib/tsconfig.json index 6c0b13e36a6199..ef4d0a97fd2a4a 100644 --- a/src/plugins/vis_types/vislib/tsconfig.json +++ b/src/plugins/vis_types/vislib/tsconfig.json @@ -18,7 +18,7 @@ { "path": "../../expressions/tsconfig.json" }, { "path": "../../visualizations/tsconfig.json" }, { "path": "../../kibana_utils/tsconfig.json" }, - { "path": "../../vis_default_editor/tsconfig.json" }, + { "path": "../../vis_types/gauge/tsconfig.json" }, { "path": "../../vis_types/xy/tsconfig.json" }, { "path": "../../vis_types/pie/tsconfig.json" }, { "path": "../../vis_types/heatmap/tsconfig.json" }, diff --git a/src/plugins/visualizations/common/utils/accessors.ts b/src/plugins/visualizations/common/utils/accessors.ts index 57a2d434dfd762..27940e1fb6890b 100644 --- a/src/plugins/visualizations/common/utils/accessors.ts +++ b/src/plugins/visualizations/common/utils/accessors.ts @@ -37,7 +37,7 @@ export const getAccessorByDimension = ( dimension: string | ExpressionValueVisDimension, columns: DatatableColumn[] ) => { - if (typeof dimension === 'string') { + if (!isVisDimension(dimension)) { return dimension; } @@ -48,3 +48,13 @@ export const getAccessorByDimension = ( return accessor.id; }; + +export function isVisDimension( + accessor: string | ExpressionValueVisDimension | undefined +): accessor is ExpressionValueVisDimension { + if (typeof accessor === 'string' || accessor === undefined) { + return false; + } + + return true; +} diff --git a/src/plugins/visualizations/common/utils/index.ts b/src/plugins/visualizations/common/utils/index.ts index 59833b3e54e461..3e92e878bb5cf7 100644 --- a/src/plugins/visualizations/common/utils/index.ts +++ b/src/plugins/visualizations/common/utils/index.ts @@ -8,4 +8,4 @@ export { prepareLogTable } from './prepare_log_table'; export type { Dimension } from './prepare_log_table'; -export { findAccessorOrFail, getAccessorByDimension } from './accessors'; +export { findAccessorOrFail, getAccessorByDimension, isVisDimension } from './accessors'; diff --git a/src/plugins/visualizations/public/visualize_app/components/split_chart_warning.tsx b/src/plugins/visualizations/public/visualize_app/components/split_chart_warning.tsx index 942c6269f15f8e..679aa6aa2fbe1c 100644 --- a/src/plugins/visualizations/public/visualize_app/components/split_chart_warning.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/split_chart_warning.tsx @@ -6,56 +6,117 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { useKibana } from '../../../../kibana_react/public'; import { VisualizeServices } from '../types'; +import { CHARTS_WITHOUT_SMALL_MULTIPLES } from '../utils/split_chart_warning_helpers'; +import type { CHARTS_WITHOUT_SMALL_MULTIPLES as CHART_WITHOUT_SMALL_MULTIPLES } from '../utils/split_chart_warning_helpers'; -export const NEW_HEATMAP_CHARTS_LIBRARY = 'visualization:visualize:legacyHeatmapChartsLibrary'; +interface Props { + chartType: CHART_WITHOUT_SMALL_MULTIPLES; + chartConfigToken: string; +} -export const SplitChartWarning = () => { +interface WarningMessageProps { + canEditAdvancedSettings: boolean | Readonly<{ [x: string]: boolean }>; + advancedSettingsLink: string; +} + +const SwitchToOldLibraryMessage: FC = ({ + canEditAdvancedSettings, + advancedSettingsLink, +}) => { + return ( + <> + {canEditAdvancedSettings && ( + + + + ), + }} + /> + )} + + ); +}; + +const ContactAdminMessage: FC = ({ canEditAdvancedSettings }) => { + return ( + <> + {!canEditAdvancedSettings && ( + + )} + + ); +}; + +const GaugeWarningFormatMessage: FC = (props) => { + return ( + + + + + ), + }} + /> + ); +}; + +const HeatmapWarningFormatMessage: FC = (props) => { + return ( + + + + + ), + }} + /> + ); +}; + +const warningMessages = { + [CHARTS_WITHOUT_SMALL_MULTIPLES.heatmap]: HeatmapWarningFormatMessage, + [CHARTS_WITHOUT_SMALL_MULTIPLES.gauge]: GaugeWarningFormatMessage, +}; + +export const SplitChartWarning: FC = ({ chartType, chartConfigToken }) => { const { services } = useKibana(); const canEditAdvancedSettings = services.application.capabilities.advancedSettings.save; const advancedSettingsLink = services.application.getUrlForApp('management', { - path: `/kibana/settings?query=${NEW_HEATMAP_CHARTS_LIBRARY}`, + path: `/kibana/settings?query=${chartConfigToken}`, }); + const WarningMessage = warningMessages[chartType]; return ( - {canEditAdvancedSettings && ( - - - - ), - }} - /> - )} - {!canEditAdvancedSettings && ( - - )} - - ), - }} + } iconType="alert" diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx index 7d6594e05ae180..c76515072a1e2e 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx @@ -17,7 +17,7 @@ import { ExperimentalVisInfo } from './experimental_vis_info'; import { useKibana } from '../../../../kibana_react/public'; import { urlFor } from '../../../../visualizations/public'; import { getUISettings } from '../../services'; -import { SplitChartWarning, NEW_HEATMAP_CHARTS_LIBRARY } from './split_chart_warning'; +import { SplitChartWarning } from './split_chart_warning'; import { SavedVisInstance, VisualizeAppState, @@ -25,6 +25,11 @@ import { VisualizeAppStateContainer, VisualizeEditorVisInstance, } from '../types'; +import { + CHARTS_CONFIG_TOKENS, + CHARTS_WITHOUT_SMALL_MULTIPLES, + isSplitChart as isSplitChartFn, +} from '../utils/split_chart_warning_helpers'; interface VisualizeEditorCommonProps { visInstance?: VisualizeEditorVisInstance; @@ -110,8 +115,17 @@ export const VisualizeEditorCommon = ({ return null; }, [visInstance?.savedVis, services, visInstance?.vis?.type.title]); // Adds a notification for split chart on the new implementation as it is not supported yet - const isSplitChart = visInstance?.vis?.data?.aggs?.aggs.some((agg) => agg.schema === 'split'); - const hasHeatmapLegacyhartsEnabled = getUISettings().get(NEW_HEATMAP_CHARTS_LIBRARY); + const chartName = visInstance?.vis.type.name; + const isSplitChart = isSplitChartFn(chartName, visInstance?.vis?.data?.aggs); + + const chartsWithoutSmallMultiples: string[] = Object.values(CHARTS_WITHOUT_SMALL_MULTIPLES); + const chartNeedsWarning = chartName ? chartsWithoutSmallMultiples.includes(chartName) : false; + const chartToken = + chartName && chartNeedsWarning + ? CHARTS_CONFIG_TOKENS[chartName as CHARTS_WITHOUT_SMALL_MULTIPLES] + : undefined; + + const hasLegacyChartsEnabled = chartToken ? getUISettings().get(chartToken) : true; return (
@@ -134,9 +148,12 @@ export const VisualizeEditorCommon = ({ /> )} {visInstance?.vis?.type?.stage === 'experimental' && } - {!hasHeatmapLegacyhartsEnabled && - isSplitChart && - visInstance?.vis.type.name === 'heatmap' && } + {!hasLegacyChartsEnabled && isSplitChart && chartNeedsWarning && chartToken && chartName && ( + + )} {visInstance?.vis?.type?.getInfoMessage?.(visInstance.vis)} {getLegacyUrlConflictCallout()} {visInstance && ( diff --git a/src/plugins/visualizations/public/visualize_app/constants.ts b/src/plugins/visualizations/public/visualize_app/constants.ts new file mode 100644 index 00000000000000..fd256cb5bbb864 --- /dev/null +++ b/src/plugins/visualizations/public/visualize_app/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const NEW_HEATMAP_CHARTS_LIBRARY = 'visualization:visualize:legacyHeatmapChartsLibrary'; +export const NEW_GAUGE_CHARTS_LIBRARY = 'visualization:visualize:legacyGaugeChartsLibrary'; diff --git a/src/plugins/visualizations/public/visualize_app/utils/split_chart_warning_helpers.ts b/src/plugins/visualizations/public/visualize_app/utils/split_chart_warning_helpers.ts new file mode 100644 index 00000000000000..d40f15aa08657a --- /dev/null +++ b/src/plugins/visualizations/public/visualize_app/utils/split_chart_warning_helpers.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { $Values } from '@kbn/utility-types'; +import { AggConfigs } from '../../../../data/common'; +import { NEW_HEATMAP_CHARTS_LIBRARY, NEW_GAUGE_CHARTS_LIBRARY } from '../constants'; + +export const CHARTS_WITHOUT_SMALL_MULTIPLES = { + heatmap: 'heatmap', + gauge: 'gauge', +} as const; + +export type CHARTS_WITHOUT_SMALL_MULTIPLES = $Values; + +export const CHARTS_CONFIG_TOKENS = { + [CHARTS_WITHOUT_SMALL_MULTIPLES.heatmap]: NEW_HEATMAP_CHARTS_LIBRARY, + [CHARTS_WITHOUT_SMALL_MULTIPLES.gauge]: NEW_GAUGE_CHARTS_LIBRARY, +} as const; + +export const isSplitChart = (chartType: string | undefined, aggs?: AggConfigs) => { + const defaultIsSplitChart = () => aggs?.aggs.some((agg) => agg.schema === 'split'); + + const knownCheckers = { + [CHARTS_WITHOUT_SMALL_MULTIPLES.heatmap]: defaultIsSplitChart, + [CHARTS_WITHOUT_SMALL_MULTIPLES.gauge]: () => aggs?.aggs.some((agg) => agg.schema === 'group'), + }; + + return (knownCheckers[chartType as CHARTS_WITHOUT_SMALL_MULTIPLES] ?? defaultIsSplitChart)(); +}; 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/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index 1ba4bcaa76b365..cd17244b1f4983 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -13,24 +13,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const PageObjects = getPageObjects(['common', 'console']); - // Failing: See https://github.com/elastic/kibana/issues/126421 - describe.skip('console autocomplete feature', function describeIndexTests() { + describe('console autocomplete feature', function describeIndexTests() { this.tags('includeFirefox'); before(async () => { log.debug('navigateTo console'); await PageObjects.common.navigateToApp('console'); // Ensure that the text area can be interacted with await PageObjects.console.dismissTutorial(); + await PageObjects.console.clearTextArea(); }); it('should provide basic auto-complete functionality', async () => { await PageObjects.console.enterRequest(); + await PageObjects.console.enterText(`{\n\t"query": {`); + await PageObjects.console.pressEnter(); await PageObjects.console.promptAutocomplete(); expect(PageObjects.console.isAutocompleteVisible()).to.be.eql(true); }); - // FLAKY: https://github.com/elastic/kibana/issues/126414 - describe.skip('with a missing comma in query', () => { + describe('with a missing comma in query', () => { const LINE_NUMBER = 4; beforeEach(async () => { await PageObjects.console.clearTextArea(); diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/create_and_add_embeddables.ts index e18e66f3a49923..1390a7517cdbf8 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/create_and_add_embeddables.ts @@ -26,12 +26,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); + }); + + it('ensure toolbar popover closes on add', async () => { await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - await PageObjects.dashboard.loadSavedDashboard('few panels'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.switchToEditMode(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewEmbeddableLink('LOG_STREAM_EMBEDDABLE'); + await dashboardAddPanel.expectEditorMenuClosed(); }); describe('add new visualization link', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + }); + it('adds new visualization via the top nav link', async () => { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); await PageObjects.dashboard.switchToEditMode(); 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/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.js index 117b8747c5a0a8..abae9a300994dc 100644 --- a/test/functional/apps/management/_scripted_fields_filter.js +++ b/test/functional/apps/management/_scripted_fields_filter.js @@ -16,8 +16,7 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['settings']); - // FLAKY: https://github.com/elastic/kibana/issues/126027 - describe.skip('filter scripted fields', function describeIndexTests() { + describe('filter scripted fields', function describeIndexTests() { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await browser.setWindowSize(1200, 800); @@ -67,6 +66,7 @@ export default function ({ getService, getPageObjects }) { expect(lang).to.be('painless'); } }); + await PageObjects.settings.clearScriptedFieldLanguageFilter('painless'); await PageObjects.settings.setScriptedFieldLanguageFilter('expression'); 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/console_page.ts b/test/functional/page_objects/console_page.ts index e6450480bbb023..32c859cc1aed9e 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -85,8 +85,8 @@ export class ConsolePageObject extends FtrService { public async promptAutocomplete() { const textArea = await this.testSubjects.find('console-textarea'); - // There should be autocomplete for this on all license levels - await textArea.pressKeys([Key.CONTROL, Key.SPACE]); + await textArea.clickMouseButton(); + await textArea.type('b'); await this.retry.waitFor('autocomplete to be visible', () => this.isAutocompleteVisible()); } diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index b1e4aa823821b7..70cdbea7fa8970 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -288,7 +288,10 @@ export class SettingsPageObject extends FtrService { } async setScriptedFieldLanguageFilter(language: string) { - await this.testSubjects.clickWhenNotDisabled('scriptedFieldLanguageFilterDropdown'); + await this.retry.try(async () => { + await this.testSubjects.clickWhenNotDisabled('scriptedFieldLanguageFilterDropdown'); + return await this.find.byCssSelector('div.euiPopover__panel-isOpen'); + }); await this.testSubjects.existOrFail('scriptedFieldLanguageFilterDropdown-popover'); await this.testSubjects.existOrFail(`scriptedFieldLanguageFilterDropdown-option-${language}`); await this.testSubjects.click(`scriptedFieldLanguageFilterDropdown-option-${language}`); diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index 43ab1f966bc9a0..e42c221a494759 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -46,6 +46,11 @@ export class DashboardAddPanelService extends FtrService { async clickEditorMenuButton() { this.log.debug('DashboardAddPanel.clickEditorMenuButton'); await this.testSubjects.click('dashboardEditorMenuButton'); + await this.testSubjects.existOrFail('dashboardEditorContextMenu'); + } + + async expectEditorMenuClosed() { + await this.testSubjects.missingOrFail('dashboardEditorContextMenu'); } async clickAggBasedVisualizations() { diff --git a/test/package/Vagrantfile b/test/package/Vagrantfile index 34c29eb2cefe71..58fdd55734184d 100644 --- a/test/package/Vagrantfile +++ b/test/package/Vagrantfile @@ -2,6 +2,9 @@ Vagrant.configure("2") do |config| config.vm.synced_folder '../../target/', '/packages' config.vm.define "deb" do |deb| + deb.vm.provider :virtualbox do |vb| + vb.memory = 2048 + end deb.vm.box = 'elastic/debian-9-x86_64' deb.vm.provision "ansible" do |ansible| ansible.playbook = "deb.yml" @@ -10,6 +13,9 @@ Vagrant.configure("2") do |config| end config.vm.define "rpm" do |rpm| + rpm.vm.provider :virtualbox do |vb| + vb.memory = 2048 + end rpm.vm.box = 'elastic/centos-7-x86_64' rpm.vm.provision "ansible" do |ansible| ansible.playbook = "rpm.yml" @@ -18,6 +24,9 @@ Vagrant.configure("2") do |config| end config.vm.define "docker" do |docker| + docker.vm.provider :virtualbox do |vb| + vb.memory = 2048 + end docker.vm.box = 'elastic/ubuntu-18.04-x86_64' docker.vm.provision "ansible" do |ansible| ansible.playbook = "docker.yml" diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index c48041c1e18839..dfe34988c4d270 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -52,6 +52,7 @@ "xpack.security": "plugins/security", "xpack.server": "legacy/server", "xpack.securitySolution": "plugins/security_solution", + "xpack.sessionView": "plugins/session_view", "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": "plugins/spaces", "xpack.savedObjectsTagging": ["plugins/saved_objects_tagging"], diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts index c6265a17b122e0..ecb2e0e0b9ea3c 100644 --- a/x-pack/plugins/actions/server/feature.ts +++ b/x-pack/plugins/actions/server/feature.ts @@ -13,6 +13,12 @@ import { } from './constants/saved_objects'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +/** + * The order of appearance in the feature privilege page + * under the management section. + */ +const FEATURE_ORDER = 3000; + export const ACTIONS_FEATURE = { id: 'actions', name: i18n.translate('xpack.actions.featureRegistry.actionsFeatureName', { @@ -20,6 +26,7 @@ export const ACTIONS_FEATURE = { }), category: DEFAULT_APP_CATEGORIES.management, app: [], + order: FEATURE_ORDER, management: { insightsAndAlerting: ['triggersActions'], }, diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 35058aa343b1a1..da916ee7ed98aa 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -63,6 +63,13 @@ export interface AlertAggregations { ruleMutedStatus: { muted: number; unmuted: number }; } +export interface MappedParamsProperties { + risk_score?: number; + severity?: string; +} + +export type MappedParams = SavedObjectAttributes & MappedParamsProperties; + export interface Alert { id: string; enabled: boolean; @@ -73,6 +80,7 @@ export interface Alert { schedule: IntervalSchedule; actions: AlertAction[]; params: Params; + mapped_params?: MappedParams; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts new file mode 100644 index 00000000000000..d8618d0ed6c210 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromKueryExpression } from '@kbn/es-query'; +import { + getMappedParams, + getModifiedFilter, + getModifiedField, + getModifiedSearchFields, + getModifiedSearch, + getModifiedValue, + modifyFilterKueryNode, +} from './mapped_params_utils'; + +describe('getModifiedParams', () => { + it('converts params to mapped params', () => { + const params = { + riskScore: 42, + severity: 'medium', + a: 'test', + b: 'test', + c: 'test,', + }; + + expect(getMappedParams(params)).toEqual({ + risk_score: 42, + severity: '40-medium', + }); + }); + + it('returns empty mapped params if nothing exists in the input params', () => { + const params = { + a: 'test', + b: 'test', + c: 'test', + }; + + expect(getMappedParams(params)).toEqual({}); + }); +}); + +describe('getModifiedFilter', () => { + it('converts params filters to mapped params filters', () => { + // Make sure it works for both camel and snake case params + const filter = 'alert.attributes.params.risk_score: 45'; + + expect(getModifiedFilter(filter)).toEqual('alert.attributes.mapped_params.risk_score: 45'); + }); +}); + +describe('getModifiedField', () => { + it('converts sort field to mapped params sort field', () => { + expect(getModifiedField('params.risk_score')).toEqual('mapped_params.risk_score'); + expect(getModifiedField('params.riskScore')).toEqual('mapped_params.risk_score'); + expect(getModifiedField('params.invalid')).toEqual('params.invalid'); + }); +}); + +describe('getModifiedSearchFields', () => { + it('converts a list of params search fields to mapped param search fields', () => { + const searchFields = [ + 'params.risk_score', + 'params.riskScore', + 'params.severity', + 'params.invalid', + 'invalid', + ]; + + expect(getModifiedSearchFields(searchFields)).toEqual([ + 'mapped_params.risk_score', + 'mapped_params.risk_score', + 'mapped_params.severity', + 'params.invalid', + 'invalid', + ]); + }); +}); + +describe('getModifiedSearch', () => { + it('converts the search value depending on the search field', () => { + const searchFields = ['params.severity', 'another']; + + expect(getModifiedSearch(searchFields, 'medium')).toEqual('40-medium'); + expect(getModifiedSearch(searchFields, 'something else')).toEqual('something else'); + expect(getModifiedSearch('params.risk_score', 'something else')).toEqual('something else'); + expect(getModifiedSearch('mapped_params.severity', 'medium')).toEqual('40-medium'); + }); +}); + +describe('getModifiedValue', () => { + it('converts severity strings to sortable strings', () => { + expect(getModifiedValue('severity', 'low')).toEqual('20-low'); + expect(getModifiedValue('severity', 'medium')).toEqual('40-medium'); + expect(getModifiedValue('severity', 'high')).toEqual('60-high'); + expect(getModifiedValue('severity', 'critical')).toEqual('80-critical'); + }); +}); + +describe('modifyFilterKueryNode', () => { + it('modifies the resulting kuery node AST filter for alert params', () => { + const astFilter = fromKueryExpression( + 'alert.attributes.name: "Rule I" and alert.attributes.tags: "fast" and alert.attributes.params.severity > medium' + ); + + expect(astFilter.arguments[2].arguments[0]).toEqual({ + type: 'literal', + value: 'alert.attributes.params.severity', + }); + + expect(astFilter.arguments[2].arguments[2]).toEqual({ + type: 'literal', + value: 'medium', + }); + + modifyFilterKueryNode({ astFilter }); + + expect(astFilter.arguments[2].arguments[0]).toEqual({ + type: 'literal', + value: 'alert.attributes.mapped_params.severity', + }); + + expect(astFilter.arguments[2].arguments[2]).toEqual({ + type: 'literal', + value: '40-medium', + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts b/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts new file mode 100644 index 00000000000000..b4d82990654c25 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/mapped_params_utils.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { snakeCase } from 'lodash'; +import { AlertTypeParams, MappedParams, MappedParamsProperties } from '../../types'; +import { SavedObjectAttribute } from '../../../../../../src/core/server'; +import { + iterateFilterKureyNode, + IterateFilterKureyNodeParams, + IterateActionProps, + getFieldNameAttribute, +} from './validate_attributes'; + +export const MAPPED_PARAMS_PROPERTIES: Array = [ + 'risk_score', + 'severity', +]; + +const SEVERITY_MAP: Record = { + low: '20-low', + medium: '40-medium', + high: '60-high', + critical: '80-critical', +}; + +/** + * Returns the mapped_params object when given a params object. + * The function will match params present in MAPPED_PARAMS_PROPERTIES and + * return an empty object if nothing is matched. + */ +export const getMappedParams = (params: AlertTypeParams) => { + return Object.entries(params).reduce((result, [key, value]) => { + const snakeCaseKey = snakeCase(key); + + if (MAPPED_PARAMS_PROPERTIES.includes(snakeCaseKey as keyof MappedParamsProperties)) { + result[snakeCaseKey] = getModifiedValue( + snakeCaseKey, + value as string + ) as SavedObjectAttribute; + } + + return result; + }, {}); +}; + +/** + * Returns a string of the filter, but with params replaced with mapped_params. + * This function will check both camel and snake case to make sure we're consistent + * with the naming + * + * i.e.: 'alerts.attributes.params.riskScore' -> 'alerts.attributes.mapped_params.risk_score' + */ +export const getModifiedFilter = (filter: string) => { + return filter.replace('.params.', '.mapped_params.'); +}; + +/** + * Returns modified field with mapped_params instead of params. + * + * i.e.: 'params.riskScore' -> 'mapped_params.risk_score' + */ +export const getModifiedField = (field: string | undefined) => { + if (!field) { + return field; + } + + const sortFieldToReplace = `${snakeCase(field.replace('params.', ''))}`; + + if (MAPPED_PARAMS_PROPERTIES.includes(sortFieldToReplace as keyof MappedParamsProperties)) { + return `mapped_params.${sortFieldToReplace}`; + } + + return field; +}; + +/** + * Returns modified search fields with mapped_params instead of params. + * + * i.e.: + * [ + * 'params.riskScore', + * 'params.severity', + * ] + * -> + * [ + * 'mapped_params.riskScore', + * 'mapped_params.severity', + * ] + */ +export const getModifiedSearchFields = (searchFields: string[] | undefined) => { + if (!searchFields) { + return searchFields; + } + + return searchFields.reduce((result, field) => { + const modifiedField = getModifiedField(field); + if (modifiedField) { + return [...result, modifiedField]; + } + return result; + }, []); +}; + +export const getModifiedValue = (key: string, value: string) => { + if (key === 'severity') { + return SEVERITY_MAP[value] || ''; + } + return value; +}; + +export const getModifiedSearch = (searchFields: string | string[] | undefined, value: string) => { + if (!searchFields) { + return value; + } + + const fieldNames = Array.isArray(searchFields) ? searchFields : [searchFields]; + + const modifiedSearchValues = fieldNames.map((fieldName) => { + const firstAttribute = getFieldNameAttribute(fieldName, [ + 'alert', + 'attributes', + 'params', + 'mapped_params', + ]); + return getModifiedValue(firstAttribute, value); + }); + + return modifiedSearchValues.find((search) => search !== value) || value; +}; + +export const modifyFilterKueryNode = ({ + astFilter, + hasNestedKey = false, + nestedKeys, + storeValue, + path = 'arguments', +}: IterateFilterKureyNodeParams) => { + const action = ({ index, ast, fieldName, localFieldName }: IterateActionProps) => { + // First index, assuming ast value is the attribute name + if (index === 0) { + const firstAttribute = getFieldNameAttribute(fieldName, ['alert', 'attributes']); + // Replace the ast.value for params to mapped_params + if (firstAttribute === 'params') { + ast.value = getModifiedFilter(ast.value); + } + } + + // Subsequent indices, assuming ast value is the filtering value + else { + const firstAttribute = getFieldNameAttribute(localFieldName, ['alert', 'attributes']); + + // Replace the ast.value for params value to the modified mapped_params value + if (firstAttribute === 'params' && ast.value) { + const attribute = getFieldNameAttribute(localFieldName, ['alert', 'attributes', 'params']); + ast.value = getModifiedValue(attribute, ast.value); + } + } + }; + + iterateFilterKureyNode({ + astFilter, + hasNestedKey, + nestedKeys, + storeValue, + path, + action, + }); +}; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts index 652c30ff380c55..1777a36d80a2f1 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.test.ts @@ -13,7 +13,7 @@ import { } from './validate_attributes'; describe('Validate attributes', () => { - const excludedFieldNames = ['monitoring']; + const excludedFieldNames = ['monitoring', 'mapped_params']; describe('validateSortField', () => { test('should NOT throw an error, when sort field is not part of the field to exclude', () => { expect(() => validateSortField('name.keyword', excludedFieldNames)).not.toThrow(); @@ -86,6 +86,17 @@ describe('Validate attributes', () => { ).not.toThrow(); }); + test('should NOT throw an error, when filter contains params with validate properties', () => { + expect(() => + validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'alert.attributes.name: "Rule I" and alert.attributes.tags: "fast" and alert.attributes.params.risk_score > 50' + ), + excludedFieldNames, + }) + ).not.toThrow(); + }); + test('should throw an error, when filter contains the field to exclude', () => { expect(() => validateFilterKueryNode({ @@ -111,5 +122,18 @@ describe('Validate attributes', () => { `"Filter is not supported on this field alert.attributes.actions"` ); }); + + test('should throw an error, when filtering contains a property that is not valid', () => { + expect(() => + validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'alert.attributes.name: "Rule I" and alert.attributes.tags: "fast" and alert.attributes.mapped_params.risk_score > 50' + ), + excludedFieldNames, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Filter is not supported on this field alert.attributes.mapped_params.risk_score"` + ); + }); }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts index fa65f4c2f0999b..ad17ede1b99adc 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_attributes.ts @@ -7,11 +7,18 @@ import { KueryNode } from '@kbn/es-query'; import { get, isEmpty } from 'lodash'; - import mappings from '../../saved_objects/mappings.json'; const astFunctionType = ['is', 'range', 'nested']; +export const getFieldNameAttribute = (fieldName: string, attributesToIgnore: string[]) => { + const fieldNameSplit = (fieldName || '') + .split('.') + .filter((fn: string) => !attributesToIgnore.includes(fn)); + + return fieldNameSplit.length > 0 ? fieldNameSplit[0] : ''; +}; + export const validateOperationOnAttributes = ( astFilter: KueryNode | null, sortField: string | undefined, @@ -44,28 +51,41 @@ export const validateSearchFields = (searchFields: string[], excludedFieldNames: } }; -interface ValidateFilterKueryNodeParams { +export interface IterateActionProps { + ast: KueryNode; + index: number; + fieldName: string; + localFieldName: string; +} + +export interface IterateFilterKureyNodeParams { astFilter: KueryNode; - excludedFieldNames: string[]; hasNestedKey?: boolean; nestedKeys?: string; storeValue?: boolean; path?: string; + action?: (props: IterateActionProps) => void; } -export const validateFilterKueryNode = ({ +export interface ValidateFilterKueryNodeParams extends IterateFilterKureyNodeParams { + excludedFieldNames: string[]; +} + +export const iterateFilterKureyNode = ({ astFilter, - excludedFieldNames, hasNestedKey = false, nestedKeys, storeValue, path = 'arguments', -}: ValidateFilterKueryNodeParams) => { + action = () => {}, +}: IterateFilterKureyNodeParams) => { let localStoreValue = storeValue; let localNestedKeys: string | undefined; + let localFieldName: string = ''; if (localStoreValue === undefined) { localStoreValue = astFilter.type === 'function' && astFunctionType.includes(astFilter.function); } + astFilter.arguments.forEach((ast: KueryNode, index: number) => { if (hasNestedKey && ast.type === 'literal' && ast.value != null) { localNestedKeys = ast.value; @@ -80,25 +100,56 @@ export const validateFilterKueryNode = ({ if (ast.arguments) { const myPath = `${path}.${index}`; - validateFilterKueryNode({ + iterateFilterKureyNode({ astFilter: ast, - excludedFieldNames, storeValue: ast.type === 'function' && astFunctionType.includes(ast.function), path: `${myPath}.arguments`, hasNestedKey: ast.type === 'function' && ast.function === 'nested', nestedKeys: localNestedKeys || nestedKeys, + action, }); } - if (localStoreValue && index === 0) { + if (localStoreValue) { const fieldName = nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value; - const fieldNameSplit = fieldName - .split('.') - .filter((fn: string) => !['alert', 'attributes'].includes(fn)); - const firstAttribute = fieldNameSplit.length > 0 ? fieldNameSplit[0] : ''; + + if (index === 0) { + localFieldName = fieldName; + } + + action({ + ast, + index, + fieldName, + localFieldName, + }); + } + }); +}; + +export const validateFilterKueryNode = ({ + astFilter, + excludedFieldNames, + hasNestedKey = false, + nestedKeys, + storeValue, + path = 'arguments', +}: ValidateFilterKueryNodeParams) => { + const action = ({ index, fieldName }: IterateActionProps) => { + if (index === 0) { + const firstAttribute = getFieldNameAttribute(fieldName, ['alert', 'attributes']); if (excludedFieldNames.includes(firstAttribute)) { throw new Error(`Filter is not supported on this field ${fieldName}`); } } + }; + + iterateFilterKureyNode({ + astFilter, + hasNestedKey, + nestedKeys, + storeValue, + path, + action, }); }; diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 6d3ffc822a626d..86f0d3becdce77 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -78,6 +78,13 @@ import { Alert } from '../alert'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object'; import { getDefaultRuleMonitoring } from '../task_runner/task_runner'; +import { + getMappedParams, + getModifiedField, + getModifiedSearchFields, + getModifiedSearch, + modifyFilterKueryNode, +} from './lib/mapped_params_utils'; export interface RegistryAlertTypeWithAuth extends RegistryRuleType { authorizedConsumers: string[]; @@ -251,7 +258,10 @@ export class RulesClient { private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; private readonly auditLogger?: AuditLogger; private readonly eventLogger?: IEventLogger; - private readonly fieldsToExcludeFromPublicApi: Array = ['monitoring']; + private readonly fieldsToExcludeFromPublicApi: Array = [ + 'monitoring', + 'mapped_params', + ]; constructor({ ruleTypeRegistry, @@ -371,6 +381,12 @@ export class RulesClient { monitoring: getDefaultRuleMonitoring(), }; + const mappedParams = getMappedParams(updatedParams); + + if (Object.keys(mappedParams).length) { + rawRule.mapped_params = mappedParams; + } + this.auditLogger?.log( ruleAuditEvent({ action: RuleAuditAction.CREATE, @@ -634,9 +650,10 @@ export class RulesClient { ); throw error; } + const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple; const filterKueryNode = options.filter ? esKuery.fromKueryExpression(options.filter) : null; - const sortField = mapSortField(options.sortField); + let sortField = mapSortField(options.sortField); if (excludeFromPublicApi) { try { validateOperationOnAttributes( @@ -650,6 +667,24 @@ export class RulesClient { } } + sortField = mapSortField(getModifiedField(options.sortField)); + + // Generate new modified search and search fields, translating certain params properties + // to mapped_params. Thus, allowing for sort/search/filtering on params. + // We do the modifcation after the validate check to make sure the public API does not + // use the mapped_params in their queries. + options = { + ...options, + ...(options.searchFields && { searchFields: getModifiedSearchFields(options.searchFields) }), + ...(options.search && { search: getModifiedSearch(options.searchFields, options.search) }), + }; + + // Modifies kuery node AST to translate params filter and the filter value to mapped_params. + // This translation is done in place, and therefore is not a pure function. + if (filterKueryNode) { + modifyFilterKueryNode({ astFilter: filterKueryNode }); + } + const { page, per_page: perPage, @@ -1027,6 +1062,13 @@ export class RulesClient { updatedBy: username, updatedAt: new Date().toISOString(), }); + + const mappedParams = getMappedParams(updatedParams); + + if (Object.keys(mappedParams).length) { + createAttributes.mapped_params = mappedParams; + } + try { updatedObject = await this.unsecuredSavedObjectsClient.create( 'alert', diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 6ccc640dcc1351..8cecb47f23a886 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -1878,6 +1878,167 @@ describe('create()', () => { `); }); + test('should create alerts with mapped_params', async () => { + const data = getMockData({ + params: { + bar: true, + risk_score: 42, + severity: 'low', + }, + }); + + const createdAttributes = { + ...data, + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + risk_score: 42, + severity: 'low', + }, + createdAt: '2019-02-12T21:01:22.479Z', + createdBy: 'elastic', + updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', + muteAll: false, + mutedInstanceIds: [], + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }; + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '123', + type: 'alert', + attributes: createdAttributes, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + const result = await rulesClient.create({ data }); + + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'alert', + { + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: '123', + consumer: 'bar', + schedule: { + interval: '1m', + }, + throttle: null, + notifyWhen: 'onActiveAlert', + params: { + bar: true, + risk_score: 42, + severity: 'low', + }, + actions: [ + { + group: 'default', + params: { + foo: true, + }, + actionRef: 'action_0', + actionTypeId: 'test', + }, + ], + apiKeyOwner: null, + apiKey: null, + legacyId: null, + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2019-02-12T21:01:22.479Z', + updatedAt: '2019-02-12T21:01:22.479Z', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'pending', + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + }, + monitoring: { + execution: { + history: [], + calculated_metrics: { + success_ratio: 0, + }, + }, + }, + mapped_params: { + risk_score: 42, + severity: '20-low', + }, + meta: { + versionApiKeyLastmodified: 'v8.0.0', + }, + }, + { + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + id: 'mock-saved-object-id', + } + ); + + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "consumer": "bar", + "createdAt": 2019-02-12T21:01:22.479Z, + "createdBy": "elastic", + "enabled": true, + "id": "123", + "muteAll": false, + "mutedInstanceIds": Array [], + "name": "abc", + "notifyWhen": null, + "params": Object { + "bar": true, + "risk_score": 42, + "severity": "low", + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": null, + "updatedAt": 2019-02-12T21:01:22.479Z, + "updatedBy": "elastic", + } + `); + }); + test('should validate params', async () => { const data = getMockData(); ruleTypeRegistry.get.mockReturnValue({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts index 60aac3f266e785..bd382faa6d6cb0 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts @@ -290,6 +290,37 @@ describe('find()', () => { expect(jest.requireMock('../lib/map_sort_field').mapSortField).toHaveBeenCalledWith('name'); }); + test('should translate filter/sort/search on params to mapped_params', async () => { + const filter = esKuery.fromKueryExpression( + '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))' + ); + authorization.getFindAuthorizationFilter.mockResolvedValue({ + filter, + ensureRuleTypeIsAuthorized() {}, + }); + + const rulesClient = new RulesClient(rulesClientParams); + await rulesClient.find({ + options: { + sortField: 'params.risk_score', + searchFields: ['params.risk_score', 'params.severity'], + filter: 'alert.attributes.params.risk_score > 50', + }, + excludeFromPublicApi: true, + }); + + const findCallParams = unsecuredSavedObjectsClient.find.mock.calls[0][0]; + + expect(findCallParams.searchFields).toEqual([ + 'mapped_params.risk_score', + 'mapped_params.severity', + ]); + + expect(findCallParams.filter.arguments[0].arguments[0].value).toEqual( + 'alert.attributes.mapped_params.risk_score' + ); + }); + test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => { jest.resetAllMocks(); authorization.getFindAuthorizationFilter.mockResolvedValue({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index 1def4b7d60f4e1..be2f859ac96b3f 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -252,6 +252,8 @@ describe('update()', () => { tags: ['foo'], params: { bar: true, + risk_score: 40, + severity: 'low', }, throttle: null, notifyWhen: 'onActiveAlert', @@ -362,6 +364,10 @@ describe('update()', () => { "apiKeyOwner": null, "consumer": "myApp", "enabled": true, + "mapped_params": Object { + "risk_score": 40, + "severity": "20-low", + }, "meta": Object { "versionApiKeyLastmodified": "v7.10.0", }, @@ -369,6 +375,8 @@ describe('update()', () => { "notifyWhen": "onActiveAlert", "params": Object { "bar": true, + "risk_score": 40, + "severity": "low", }, "schedule": Object { "interval": "1m", diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json index d6ebd25d4af375..e6eedced78914a 100644 --- a/x-pack/plugins/alerting/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerting/server/saved_objects/mappings.json @@ -53,6 +53,16 @@ "type": "flattened", "ignore_above": 4096 }, + "mapped_params": { + "properties": { + "risk_score": { + "type": "float" + }, + "severity": { + "type": "keyword" + } + } + }, "scheduledTaskId": { "type": "keyword" }, @@ -155,6 +165,10 @@ } } } + }, + "snoozeEndTime": { + "type": "date", + "format": "strict_date_time" } } } diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 1d7d3d2a362a99..28b1f599f9575a 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -2229,6 +2229,30 @@ describe('successful migrations', () => { ); }); + describe('8.2.0', () => { + test('migrates params to mapped_params', () => { + const migration820 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.2.0']; + const alert = getMockData( + { + params: { + risk_score: 60, + severity: 'high', + foo: 'bar', + }, + alertTypeId: 'siem.signals', + }, + true + ); + + const migratedAlert820 = migration820(alert, migrationContext); + + expect(migratedAlert820.attributes.mapped_params).toEqual({ + risk_score: 60, + severity: '60-high', + }); + }); + }); + describe('Metrics Inventory Threshold rule', () => { test('Migrates incorrect action group spelling', () => { const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 6e6c886d91b531..09d505aec0f0c4 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -21,6 +21,7 @@ import { RawRule, RawAlertAction } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; import type { IsMigrationNeededPredicate } from '../../../encrypted_saved_objects/server'; import { extractRefsFromGeoContainmentAlert } from './geo_containment/migrations'; +import { getMappedParams } from '../../server/rules_client/lib/mapped_params_utils'; const SIEM_APP_ID = 'securitySolution'; const SIEM_SERVER_APP_ID = 'siem'; @@ -145,6 +146,12 @@ export function getMigrations( pipeMigrations(addSecuritySolutionAADRuleTypeTags) ); + const migrationRules820 = createEsoMigration( + encryptedSavedObjects, + (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true, + pipeMigrations(addMappedParams) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), @@ -155,6 +162,7 @@ export function getMigrations( '7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'), '8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'), '8.0.1': executeMigrationWithErrorHandling(migrationRules801, '8.0.1'), + '8.2.0': executeMigrationWithErrorHandling(migrationRules820, '8.2.0'), }; } @@ -822,6 +830,28 @@ function fixInventoryThresholdGroupId( } } +function addMappedParams( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { + attributes: { params }, + } = doc; + + const mappedParams = getMappedParams(params); + + if (Object.keys(mappedParams).length) { + return { + ...doc, + attributes: { + ...doc.attributes, + mapped_params: mappedParams, + }, + }; + } + + return doc; +} + function getCorrespondingAction( actions: SavedObjectAttribute, connectorRef: string diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 1642cc13d4dec8..6b06f7efe30660 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -40,6 +40,7 @@ import { ActionVariable, SanitizedRuleConfig, RuleMonitoring, + MappedParams, } from '../common'; import { LicenseType } from '../../licensing/server'; import { IAbortableClusterClient } from './lib/create_abortable_es_client_factory'; @@ -236,6 +237,7 @@ export interface RawRule extends SavedObjectAttributes { schedule: SavedObjectAttributes; actions: RawAlertAction[]; params: SavedObjectAttributes; + mapped_params?: MappedParams; scheduledTaskId?: string | null; createdBy: string | null; updatedBy: string | null; @@ -250,6 +252,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/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/constants.ts b/x-pack/plugins/cases/common/constants.ts index c57b40dbcf0024..0f3fd6345a672f 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -7,16 +7,33 @@ import { ConnectorTypes } from './api'; import { CasesContextFeatures } from './ui/types'; -export const DEFAULT_DATE_FORMAT = 'dateFormat'; -export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; +export const DEFAULT_DATE_FORMAT = 'dateFormat' as const; +export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; -export const APP_ID = 'cases'; +/** + * Application + */ + +export const APP_ID = 'cases' as const; +export const FEATURE_ID = 'generalCases' as const; +export const APP_OWNER = 'cases' as const; +export const APP_PATH = '/app/management/insightsAndAlerting/cases' as const; +/** + * The main Cases application is in the stack management under the + * Alerts and Insights section. To do that, Cases registers to the management + * application. This constant holds the application ID of the management plugin + */ +export const STACK_APP_ID = 'management' as const; + +/** + * Saved objects + */ -export const CASE_SAVED_OBJECT = 'cases'; -export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings'; -export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions'; -export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; -export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; +export const CASE_SAVED_OBJECT = 'cases' as const; +export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings' as const; +export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions' as const; +export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments' as const; +export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure' as const; /** * If more values are added here please also add them here: x-pack/test/cases_api_integration/common/fixtures/plugins @@ -33,32 +50,32 @@ export const SAVED_OBJECT_TYPES = [ * Case routes */ -export const CASES_URL = '/api/cases'; -export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`; -export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`; -export const CASE_CONFIGURE_DETAILS_URL = `${CASES_URL}/configure/{configuration_id}`; -export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`; +export const CASES_URL = '/api/cases' as const; +export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}` as const; +export const CASE_CONFIGURE_URL = `${CASES_URL}/configure` as const; +export const CASE_CONFIGURE_DETAILS_URL = `${CASES_URL}/configure/{configuration_id}` as const; +export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors` as const; -export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`; -export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`; -export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push`; -export const CASE_REPORTERS_URL = `${CASES_URL}/reporters`; -export const CASE_STATUS_URL = `${CASES_URL}/status`; -export const CASE_TAGS_URL = `${CASES_URL}/tags`; -export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; +export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments` as const; +export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}` as const; +export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push` as const; +export const CASE_REPORTERS_URL = `${CASES_URL}/reporters` as const; +export const CASE_STATUS_URL = `${CASES_URL}/status` as const; +export const CASE_TAGS_URL = `${CASES_URL}/tags` as const; +export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions` as const; -export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}`; -export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts`; +export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}` as const; +export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts` as const; -export const CASE_METRICS_DETAILS_URL = `${CASES_URL}/metrics/{case_id}`; +export const CASE_METRICS_DETAILS_URL = `${CASES_URL}/metrics/{case_id}` as const; /** * Action routes */ -export const ACTION_URL = '/api/actions'; -export const ACTION_TYPES_URL = `${ACTION_URL}/connector_types`; -export const CONNECTORS_URL = `${ACTION_URL}/connectors`; +export const ACTION_URL = '/api/actions' as const; +export const ACTION_TYPES_URL = `${ACTION_URL}/connector_types` as const; +export const CONNECTORS_URL = `${ACTION_URL}/connectors` as const; export const SUPPORTED_CONNECTORS = [ `${ConnectorTypes.serviceNowITSM}`, @@ -71,10 +88,10 @@ export const SUPPORTED_CONNECTORS = [ /** * Alerts */ -export const MAX_ALERTS_PER_CASE = 5000; +export const MAX_ALERTS_PER_CASE = 5000 as const; -export const SECURITY_SOLUTION_OWNER = 'securitySolution'; -export const OBSERVABILITY_OWNER = 'observability'; +export const SECURITY_SOLUTION_OWNER = 'securitySolution' as const; +export const OBSERVABILITY_OWNER = 'observability' as const; export const OWNER_INFO = { [SECURITY_SOLUTION_OWNER]: { @@ -85,16 +102,16 @@ export const OWNER_INFO = { label: 'Observability', iconType: 'logoObservability', }, -}; +} as const; -export const MAX_DOCS_PER_PAGE = 10000; -export const MAX_CONCURRENT_SEARCHES = 10; +export const MAX_DOCS_PER_PAGE = 10000 as const; +export const MAX_CONCURRENT_SEARCHES = 10 as const; /** * Validation */ -export const MAX_TITLE_LENGTH = 64; +export const MAX_TITLE_LENGTH = 64 as const; /** * Cases features diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 170ac2a96aaa8d..c96372b57593db 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -10,8 +10,10 @@ "id":"cases", "kibanaVersion":"kibana", "optionalPlugins":[ + "home", "security", "spaces", + "features", "usageCollection" ], "owner":{ @@ -20,14 +22,18 @@ }, "requiredPlugins":[ "actions", + "data", + "embeddable", "esUiShared", "lens", "features", "kibanaReact", "kibanaUtils", - "triggersActionsUi" + "triggersActionsUi", + "management" ], "requiredBundles": [ + "home", "savedObjects" ], "server":true, diff --git a/x-pack/plugins/cases/public/application.tsx b/x-pack/plugins/cases/public/application.tsx new file mode 100644 index 00000000000000..a528fa2376dbb5 --- /dev/null +++ b/x-pack/plugins/cases/public/application.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Router } from 'react-router-dom'; +import { I18nProvider } from '@kbn/i18n-react'; +import { EuiErrorBoundary } from '@elastic/eui'; + +import { + KibanaContextProvider, + KibanaThemeProvider, + useUiSetting$, +} from '../../../../src/plugins/kibana_react/public'; +import { EuiThemeProvider as StyledComponentsThemeProvider } from '../../../../src/plugins/kibana_react/common'; +import { RenderAppProps } from './types'; +import { CasesApp } from './components/app'; + +export const renderApp = (deps: RenderAppProps) => { + const { mountParams } = deps; + const { element } = mountParams; + + ReactDOM.render(, element); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; + +const CasesAppWithContext = () => { + const [darkMode] = useUiSetting$('theme:darkMode'); + + return ( + + + + ); +}; + +CasesAppWithContext.displayName = 'CasesAppWithContext'; + +export const App: React.FC<{ deps: RenderAppProps }> = ({ deps }) => { + const { mountParams, coreStart, pluginsStart, storage, kibanaVersion } = deps; + const { history, theme$ } = mountParams; + + return ( + + + + + + + + + + + + ); +}; + +App.displayName = 'App'; diff --git a/x-pack/plugins/cases/public/common/hooks.test.tsx b/x-pack/plugins/cases/public/common/hooks.test.tsx new file mode 100644 index 00000000000000..f122d3312a6435 --- /dev/null +++ b/x-pack/plugins/cases/public/common/hooks.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; + +import { TestProviders } from '../common/mock'; +import { useIsMainApplication } from './hooks'; +import { useApplication } from '../components/cases_context/use_application'; + +jest.mock('../components/cases_context/use_application'); + +const useApplicationMock = useApplication as jest.Mock; + +describe('hooks', () => { + beforeEach(() => { + jest.clearAllMocks(); + useApplicationMock.mockReturnValue({ appId: 'management', appTitle: 'Management' }); + }); + + describe('useIsMainApplication', () => { + it('returns true if it is the main application', () => { + const { result } = renderHook(() => useIsMainApplication(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current).toBe(true); + }); + + it('returns false if it is not the main application', () => { + useApplicationMock.mockReturnValue({ appId: 'testAppId', appTitle: 'Test app' }); + const { result } = renderHook(() => useIsMainApplication(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/common/hooks.ts b/x-pack/plugins/cases/public/common/hooks.ts new file mode 100644 index 00000000000000..f65b56fecfd848 --- /dev/null +++ b/x-pack/plugins/cases/public/common/hooks.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { STACK_APP_ID } from '../../common/constants'; +import { useCasesContext } from '../components/cases_context/use_cases_context'; + +export const useIsMainApplication = () => { + const { appId } = useCasesContext(); + + return appId === STACK_APP_ID; +}; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index 08eb2ebf3df7a6..bf81e92af92bd7 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -10,7 +10,11 @@ import moment from 'moment-timezone'; import { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants'; +import { + FEATURE_ID, + DEFAULT_DATE_FORMAT, + DEFAULT_DATE_FORMAT_TZ, +} from '../../../../common/constants'; import { AuthenticatedUser } from '../../../../../security/common/model'; import { convertToCamelCase } from '../../../containers/utils'; import { StartServices } from '../../../types'; @@ -155,3 +159,17 @@ export const useNavigation = (appId: string) => { const { getAppUrl } = useAppUrl(appId); return { navigateTo, getAppUrl }; }; + +/** + * Returns the capabilities of the main cases application + * + */ +export const useApplicationCapabilities = (): { crud: boolean; read: boolean } => { + const capabilities = useKibana().services.application.capabilities; + const casesCapabilities = capabilities[FEATURE_ID]; + + return { + crud: !!casesCapabilities?.crud_cases, + read: !!casesCapabilities?.read_cases, + }; +}; diff --git a/x-pack/plugins/cases/public/common/navigation/hooks.test.tsx b/x-pack/plugins/cases/public/common/navigation/hooks.test.tsx index 96e34d6c69cc99..cd6cf13e7256da 100644 --- a/x-pack/plugins/cases/public/common/navigation/hooks.test.tsx +++ b/x-pack/plugins/cases/public/common/navigation/hooks.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; +import { APP_ID } from '../../../common/constants'; import { useNavigation } from '../../common/lib/kibana'; import { TestProviders } from '../../common/mock'; import { @@ -33,9 +34,12 @@ describe('hooks', () => { describe('useCasesNavigation', () => { it('it calls getAppUrl with correct arguments', () => { - const { result } = renderHook(() => useCasesNavigation(CasesDeepLinkId.cases), { - wrapper: ({ children }) => {children}, - }); + const { result } = renderHook( + () => useCasesNavigation({ deepLinkId: CasesDeepLinkId.cases }), + { + wrapper: ({ children }) => {children}, + } + ); const [getCasesUrl] = result.current; @@ -43,20 +47,23 @@ describe('hooks', () => { getCasesUrl(false); }); - expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases' }); + expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: APP_ID }); }); it('it calls navigateToAllCases with correct arguments', () => { - const { result } = renderHook(() => useCasesNavigation(CasesDeepLinkId.cases), { - wrapper: ({ children }) => {children}, - }); + const { result } = renderHook( + () => useCasesNavigation({ deepLinkId: CasesDeepLinkId.cases }), + { + wrapper: ({ children }) => {children}, + } + ); const [, navigateToCases] = result.current; act(() => { navigateToCases(); }); - expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases' }); + expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: APP_ID }); }); }); @@ -70,7 +77,7 @@ describe('hooks', () => { result.current.getAllCasesUrl(false); }); - expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases' }); + expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, path: '/', deepLinkId: APP_ID }); }); it('it calls navigateToAllCases with correct arguments', () => { @@ -82,7 +89,7 @@ describe('hooks', () => { result.current.navigateToAllCases(); }); - expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases' }); + expect(navigateTo).toHaveBeenCalledWith({ path: '/', deepLinkId: APP_ID }); }); }); @@ -96,7 +103,11 @@ describe('hooks', () => { result.current.getCreateCaseUrl(false); }); - expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases_create' }); + expect(getAppUrl).toHaveBeenCalledWith({ + absolute: false, + path: '/create', + deepLinkId: APP_ID, + }); }); it('it calls navigateToAllCases with correct arguments', () => { @@ -108,7 +119,7 @@ describe('hooks', () => { result.current.navigateToCreateCase(); }); - expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases_create' }); + expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: APP_ID, path: '/create' }); }); }); @@ -122,7 +133,11 @@ describe('hooks', () => { result.current.getConfigureCasesUrl(false); }); - expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases_configure' }); + expect(getAppUrl).toHaveBeenCalledWith({ + absolute: false, + path: '/configure', + deepLinkId: APP_ID, + }); }); it('it calls navigateToAllCases with correct arguments', () => { @@ -134,7 +149,7 @@ describe('hooks', () => { result.current.navigateToConfigureCases(); }); - expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases_configure' }); + expect(navigateTo).toHaveBeenCalledWith({ path: '/configure', deepLinkId: APP_ID }); }); }); @@ -150,7 +165,7 @@ describe('hooks', () => { expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, - deepLinkId: 'cases', + deepLinkId: APP_ID, path: '/test', }); }); @@ -164,7 +179,7 @@ describe('hooks', () => { result.current.navigateToCaseView({ detailName: 'test' }); }); - expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases', path: '/test' }); + expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: APP_ID, path: '/test' }); }); }); }); diff --git a/x-pack/plugins/cases/public/common/navigation/hooks.ts b/x-pack/plugins/cases/public/common/navigation/hooks.ts index b6dcae1c0c1ce0..c5488b40607959 100644 --- a/x-pack/plugins/cases/public/common/navigation/hooks.ts +++ b/x-pack/plugins/cases/public/common/navigation/hooks.ts @@ -7,10 +7,17 @@ import { useCallback } from 'react'; import { useParams } from 'react-router-dom'; + +import { APP_ID } from '../../../common/constants'; import { useNavigation } from '../lib/kibana'; import { useCasesContext } from '../../components/cases_context/use_cases_context'; -import { CasesDeepLinkId, ICasesDeepLinkId } from './deep_links'; -import { CaseViewPathParams, generateCaseViewPath } from './paths'; +import { ICasesDeepLinkId } from './deep_links'; +import { + CASES_CONFIGURE_PATH, + CASES_CREATE_PATH, + CaseViewPathParams, + generateCaseViewPath, +} from './paths'; export const useCaseViewParams = () => useParams(); @@ -18,34 +25,60 @@ type GetCasesUrl = (absolute?: boolean) => string; type NavigateToCases = () => void; type UseCasesNavigation = [GetCasesUrl, NavigateToCases]; -export const useCasesNavigation = (deepLinkId: ICasesDeepLinkId): UseCasesNavigation => { +export const useCasesNavigation = ({ + path, + deepLinkId, +}: { + path?: string; + deepLinkId?: ICasesDeepLinkId; +}): UseCasesNavigation => { const { appId } = useCasesContext(); const { navigateTo, getAppUrl } = useNavigation(appId); const getCasesUrl = useCallback( - (absolute) => getAppUrl({ deepLinkId, absolute }), - [getAppUrl, deepLinkId] + (absolute) => getAppUrl({ path, deepLinkId, absolute }), + [getAppUrl, deepLinkId, path] ); const navigateToCases = useCallback( - () => navigateTo({ deepLinkId }), - [navigateTo, deepLinkId] + () => navigateTo({ path, deepLinkId }), + [navigateTo, deepLinkId, path] ); return [getCasesUrl, navigateToCases]; }; +/** + * Cases can be either be part of a solution or a standalone application + * The standalone application is registered from the cases plugin and is called + * the main application. The main application uses paths and the solutions + * deep links. + */ +const navigationMapping = { + all: { path: '/' }, + create: { path: CASES_CREATE_PATH }, + configure: { path: CASES_CONFIGURE_PATH }, +}; + export const useAllCasesNavigation = () => { - const [getAllCasesUrl, navigateToAllCases] = useCasesNavigation(CasesDeepLinkId.cases); + const [getAllCasesUrl, navigateToAllCases] = useCasesNavigation({ + path: navigationMapping.all.path, + deepLinkId: APP_ID, + }); + return { getAllCasesUrl, navigateToAllCases }; }; export const useCreateCaseNavigation = () => { - const [getCreateCaseUrl, navigateToCreateCase] = useCasesNavigation(CasesDeepLinkId.casesCreate); + const [getCreateCaseUrl, navigateToCreateCase] = useCasesNavigation({ + path: navigationMapping.create.path, + deepLinkId: APP_ID, + }); return { getCreateCaseUrl, navigateToCreateCase }; }; export const useConfigureCasesNavigation = () => { - const [getConfigureCasesUrl, navigateToConfigureCases] = useCasesNavigation( - CasesDeepLinkId.casesConfigure - ); + const [getConfigureCasesUrl, navigateToConfigureCases] = useCasesNavigation({ + path: navigationMapping.configure.path, + deepLinkId: APP_ID, + }); return { getConfigureCasesUrl, navigateToConfigureCases }; }; @@ -55,19 +88,25 @@ type NavigateToCaseView = (pathParams: CaseViewPathParams) => void; export const useCaseViewNavigation = () => { const { appId } = useCasesContext(); const { navigateTo, getAppUrl } = useNavigation(appId); + const deepLinkId = APP_ID; + const getCaseViewUrl = useCallback( (pathParams, absolute) => getAppUrl({ - deepLinkId: CasesDeepLinkId.cases, + deepLinkId, absolute, path: generateCaseViewPath(pathParams), }), - [getAppUrl] + [deepLinkId, getAppUrl] ); + const navigateToCaseView = useCallback( (pathParams) => - navigateTo({ deepLinkId: CasesDeepLinkId.cases, path: generateCaseViewPath(pathParams) }), - [navigateTo] + navigateTo({ + deepLinkId, + path: generateCaseViewPath(pathParams), + }), + [navigateTo, deepLinkId] ); return { getCaseViewUrl, navigateToCaseView }; }; diff --git a/x-pack/plugins/cases/public/common/navigation/paths.test.ts b/x-pack/plugins/cases/public/common/navigation/paths.test.ts index a3fa042042a2d5..3750dc4d12eb9b 100644 --- a/x-pack/plugins/cases/public/common/navigation/paths.test.ts +++ b/x-pack/plugins/cases/public/common/navigation/paths.test.ts @@ -18,24 +18,40 @@ describe('Paths', () => { it('returns the correct path', () => { expect(getCreateCasePath('test')).toBe('test/create'); }); + + it('normalize the path correctly', () => { + expect(getCreateCasePath('//test//page')).toBe('/test/page/create'); + }); }); describe('getCasesConfigurePath', () => { it('returns the correct path', () => { expect(getCasesConfigurePath('test')).toBe('test/configure'); }); + + it('normalize the path correctly', () => { + expect(getCasesConfigurePath('//test//page')).toBe('/test/page/configure'); + }); }); describe('getCaseViewPath', () => { it('returns the correct path', () => { expect(getCaseViewPath('test')).toBe('test/:detailName'); }); + + it('normalize the path correctly', () => { + expect(getCaseViewPath('//test//page')).toBe('/test/page/:detailName'); + }); }); describe('getCaseViewWithCommentPath', () => { it('returns the correct path', () => { expect(getCaseViewWithCommentPath('test')).toBe('test/:detailName/:commentId'); }); + + it('normalize the path correctly', () => { + expect(getCaseViewWithCommentPath('//test//page')).toBe('/test/page/:detailName/:commentId'); + }); }); describe('generateCaseViewPath', () => { diff --git a/x-pack/plugins/cases/public/common/navigation/paths.ts b/x-pack/plugins/cases/public/common/navigation/paths.ts index 1cd7a99630b857..a8660b5cf63ab2 100644 --- a/x-pack/plugins/cases/public/common/navigation/paths.ts +++ b/x-pack/plugins/cases/public/common/navigation/paths.ts @@ -18,12 +18,16 @@ export const CASES_CONFIGURE_PATH = '/configure' as const; export const CASE_VIEW_PATH = '/:detailName' as const; export const CASE_VIEW_COMMENT_PATH = `${CASE_VIEW_PATH}/:commentId` as const; -export const getCreateCasePath = (casesBasePath: string) => `${casesBasePath}${CASES_CREATE_PATH}`; +const normalizePath = (path: string): string => path.replaceAll('//', '/'); + +export const getCreateCasePath = (casesBasePath: string) => + normalizePath(`${casesBasePath}${CASES_CREATE_PATH}`); export const getCasesConfigurePath = (casesBasePath: string) => - `${casesBasePath}${CASES_CONFIGURE_PATH}`; -export const getCaseViewPath = (casesBasePath: string) => `${casesBasePath}${CASE_VIEW_PATH}`; + normalizePath(`${casesBasePath}${CASES_CONFIGURE_PATH}`); +export const getCaseViewPath = (casesBasePath: string) => + normalizePath(`${casesBasePath}${CASE_VIEW_PATH}`); export const getCaseViewWithCommentPath = (casesBasePath: string) => - `${casesBasePath}${CASE_VIEW_COMMENT_PATH}`; + normalizePath(`${casesBasePath}${CASE_VIEW_COMMENT_PATH}`); export const generateCaseViewPath = (params: CaseViewPathParams): string => { const { commentId } = params; @@ -31,7 +35,7 @@ export const generateCaseViewPath = (params: CaseViewPathParams): string => { const pathParams = params as unknown as { [paramName: string]: string }; if (commentId) { - return generatePath(CASE_VIEW_COMMENT_PATH, pathParams); + return normalizePath(generatePath(CASE_VIEW_COMMENT_PATH, pathParams)); } - return generatePath(CASE_VIEW_PATH, pathParams); + return normalizePath(generatePath(CASE_VIEW_PATH, pathParams)); }; diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 046eb67d38b248..61554f5191dc83 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -268,3 +268,11 @@ export const CASE_SUCCESS_SYNC_TEXT = i18n.translate('xpack.cases.actions.caseSu export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', { defaultMessage: 'View Case', }); + +export const APP_TITLE = i18n.translate('xpack.cases.common.appTitle', { + defaultMessage: 'Cases', +}); + +export const APP_DESC = i18n.translate('xpack.cases.common.appDescription', { + defaultMessage: 'Open and track issues, push information to third party systems.', +}); diff --git a/x-pack/plugins/cases/public/components/app/index.tsx b/x-pack/plugins/cases/public/components/app/index.tsx index 0ac336adb94a91..bf3dbcf1dc95b3 100644 --- a/x-pack/plugins/cases/public/components/app/index.tsx +++ b/x-pack/plugins/cases/public/components/app/index.tsx @@ -5,9 +5,33 @@ * 2.0. */ -import { CasesRoutes } from './routes'; +import React from 'react'; +import { APP_OWNER } from '../../../common/constants'; +import { useApplicationCapabilities } from '../../common/lib/kibana'; + +import { getCasesLazy } from '../../methods'; +import { Wrapper } from '../wrappers'; import { CasesRoutesProps } from './types'; export type CasesProps = CasesRoutesProps; -// eslint-disable-next-line import/no-default-export -export { CasesRoutes as default }; + +const CasesAppComponent: React.FC = () => { + const userCapabilities = useApplicationCapabilities(); + + return ( + + {getCasesLazy({ + owner: [APP_OWNER], + useFetchAlertData: () => [false, {}], + userCanCrud: userCapabilities.crud, + basePath: '/', + features: { alerts: { sync: false } }, + releasePhase: 'experimental', + })} + + ); +}; + +CasesAppComponent.displayName = 'CasesApp'; + +export const CasesApp = React.memo(CasesAppComponent); diff --git a/x-pack/plugins/cases/public/components/app/routes.tsx b/x-pack/plugins/cases/public/components/app/routes.tsx index 6222c413a11674..6fc87f691b2a22 100644 --- a/x-pack/plugins/cases/public/components/app/routes.tsx +++ b/x-pack/plugins/cases/public/components/app/routes.tsx @@ -91,3 +91,5 @@ const CasesRoutesComponent: React.FC = ({ CasesRoutesComponent.displayName = 'CasesRoutes'; export const CasesRoutes = React.memo(CasesRoutesComponent); +// eslint-disable-next-line import/no-default-export +export { CasesRoutes as default }; diff --git a/x-pack/plugins/cases/public/components/app/translations.ts b/x-pack/plugins/cases/public/components/app/translations.ts index 6796f0e03aa77b..4958ce4358c1c5 100644 --- a/x-pack/plugins/cases/public/components/app/translations.ts +++ b/x-pack/plugins/cases/public/components/app/translations.ts @@ -7,21 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const NO_PRIVILEGES_MSG = (pageName: string) => - i18n.translate('xpack.cases.noPrivileges.message', { - values: { pageName }, - defaultMessage: - 'To view {pageName} page, you must update privileges. For more information, contact your Kibana administrator.', - }); - -export const NO_PRIVILEGES_TITLE = i18n.translate('xpack.cases.noPrivileges.title', { - defaultMessage: 'Privileges required', -}); - -export const NO_PRIVILEGES_BUTTON = i18n.translate('xpack.cases.noPrivileges.button', { - defaultMessage: 'Back to Cases', -}); - export const CREATE_CASE_PAGE_NAME = i18n.translate('xpack.cases.createCase', { defaultMessage: 'Create Case', }); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx index 5179aed6518b5e..2105618ae03151 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx @@ -36,6 +36,7 @@ import type { EmbeddablePackageState } from '../../../../../../../../src/plugins import { SavedObjectFinderUi } from './saved_objects_finder'; import { useLensDraftComment } from './use_lens_draft_comment'; import { VISUALIZATION } from './translations'; +import { useIsMainApplication } from '../../../../common/hooks'; const BetaBadgeWrapper = styled.span` display: inline-flex; @@ -84,6 +85,7 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ const { draftComment, clearDraftComment } = useLensDraftComment(); const commentEditorContext = useContext(CommentEditorContext); const markdownContext = useContext(EuiMarkdownContext); + const isMainApplication = useIsMainApplication(); const handleClose = useCallback(() => { if (currentAppId) { @@ -126,8 +128,11 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ ); const originatingPath = useMemo( - () => `${location.pathname}${location.search}`, - [location.pathname, location.search] + () => + isMainApplication + ? `/insightsAndAlerting/cases${location.pathname}${location.search}` + : `${location.pathname}${location.search}`, + [isMainApplication, location.pathname, location.search] ); const handleCreateInLensClick = useCallback(() => { diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 1fafe5afe6990e..5ff675a31ce613 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -8,7 +8,7 @@ import { IconType } from '@elastic/eui'; import { ConnectorTypes } from '../../common/api'; import { FieldConfig, ValidationConfig } from '../common/shared_imports'; -import { StartPlugins } from '../types'; +import { CasesPluginStart } from '../types'; import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; import { connectorValidator as servicenowConnectorValidator } from './connectors/servicenow/validator'; import { CaseActionConnector } from './types'; @@ -48,7 +48,7 @@ export const getConnectorsFormValidators = ({ }); export const getConnectorIcon = ( - triggersActionsUi: StartPlugins['triggersActionsUi'], + triggersActionsUi: CasesPluginStart['triggersActionsUi'], type?: string ): IconType => { /** diff --git a/x-pack/plugins/cases/public/components/wrappers/index.tsx b/x-pack/plugins/cases/public/components/wrappers/index.tsx index 7a3d611413be65..d412ef34451b28 100644 --- a/x-pack/plugins/cases/public/components/wrappers/index.tsx +++ b/x-pack/plugins/cases/public/components/wrappers/index.tsx @@ -27,3 +27,8 @@ export const ContentWrapper = styled.div` padding: ${theme.eui.paddingSizes.l} 0 ${gutterTimeline} 0; `}; `; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; +`; diff --git a/x-pack/plugins/cases/public/methods/get_cases.tsx b/x-pack/plugins/cases/public/methods/get_cases.tsx index 3c1d3294d38ce0..8acb61326902b0 100644 --- a/x-pack/plugins/cases/public/methods/get_cases.tsx +++ b/x-pack/plugins/cases/public/methods/get_cases.tsx @@ -12,7 +12,8 @@ import { CasesProvider, CasesContextProps } from '../components/cases_context'; export type GetCasesProps = CasesProps & CasesContextProps; -const CasesLazy: React.FC = lazy(() => import('../components/app')); +const CasesRoutesLazy: React.FC = lazy(() => import('../components/app/routes')); + export const getCasesLazy = ({ owner, userCanCrud, @@ -29,7 +30,7 @@ export const getCasesLazy = ({ }: GetCasesProps) => ( }> - { - private kibanaVersion: string; +export class CasesUiPlugin + implements Plugin +{ + private readonly kibanaVersion: string; + private readonly storage = new Storage(localStorage); constructor(private readonly initializerContext: PluginInitializerContext) { this.kibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup, plugins: SetupPlugins) {} - public start(core: CoreStart, plugins: StartPlugins): CasesUiStart { + public setup(core: CoreSetup, plugins: CasesPluginSetup) { + const kibanaVersion = this.kibanaVersion; + const storage = this.storage; + + if (plugins.home) { + plugins.home.featureCatalogue.register({ + id: APP_ID, + title: APP_TITLE, + description: APP_DESC, + icon: 'watchesApp', + path: APP_PATH, + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + } + + plugins.management.sections.section.insightsAndAlerting.registerApp({ + id: APP_ID, + title: APP_TITLE, + order: 0, + async mount(params: ManagementAppMountParams) { + const [coreStart, pluginsStart] = (await core.getStartServices()) as [ + CoreStart, + CasesPluginStart, + unknown + ]; + + const { renderApp } = await import('./application'); + + return renderApp({ + mountParams: params, + coreStart, + pluginsStart, + storage, + kibanaVersion, + }); + }, + }); + + // Return methods that should be available to other plugins + return {}; + } + + public start(core: CoreStart, plugins: CasesPluginStart): CasesUiStart { const config = this.initializerContext.config.get(); KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion, config }); return { diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index b2198aad50911b..c756a5f73a0a73 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -10,6 +10,12 @@ import { ReactElement, ReactNode } from 'react'; import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import type { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { + ManagementSetup, + ManagementAppMountParams, +} from '../../../../src/plugins/management/public'; +import { FeaturesPluginStart } from '../..//features/public'; import type { LensPublicStart } from '../../lens/public'; import type { SecurityPluginSetup } from '../../security/public'; import type { SpacesPluginStart } from '../../spaces/public'; @@ -18,6 +24,7 @@ import { CommentRequestAlertType, CommentRequestUserType } from '../common/api'; import { UseCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal'; import { CreateCaseFlyoutProps } from './components/create/flyout'; import { UseCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout'; + import type { CasesOwners, GetAllCasesSelectorModalProps, @@ -28,16 +35,19 @@ import type { import { GetCasesContextProps } from './methods/get_cases_context'; import { getRuleIdFromEvent } from './methods/get_rule_id_from_event'; -export interface SetupPlugins { +export interface CasesPluginSetup { security: SecurityPluginSetup; + management: ManagementSetup; + home?: HomePublicPluginSetup; } -export interface StartPlugins { +export interface CasesPluginStart { data: DataPublicPluginStart; embeddable: EmbeddableStart; lens: LensPublicStart; storage: Storage; triggersActionsUi: TriggersActionsStart; + features: FeaturesPluginStart; spaces?: SpacesPluginStart; } @@ -48,10 +58,18 @@ export interface StartPlugins { */ export type StartServices = CoreStart & - StartPlugins & { + CasesPluginStart & { security: SecurityPluginSetup; }; +export interface RenderAppProps { + mountParams: ManagementAppMountParams; + coreStart: CoreStart; + pluginsStart: CasesPluginStart; + storage: Storage; + kibanaVersion: string; +} + export interface CasesUiStart { /** * Returns an object denoting the current user's ability to read and crud cases. diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 112fd6ef2c04cd..06b0922cacc475 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -16,6 +16,7 @@ import { ExternalServiceResponse, CasesConfigureAttributes, ActionTypes, + OWNER_FIELD, } from '../../../common/api'; import { createIncident, getCommentContextFromAttributes } from './utils'; @@ -25,6 +26,7 @@ import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; import { Operations } from '../../authorization'; import { casesConnectors } from '../../connectors'; import { getAlerts } from '../alerts/get'; +import { buildFilter } from '../utils'; /** * Returns true if the case should be closed based on the configuration settings. @@ -139,12 +141,19 @@ export const push = async ( /* End of push to external service */ + const ownerFilter = buildFilter({ + filters: theCase.owner, + field: OWNER_FIELD, + operator: 'or', + type: Operations.findConfigurations.savedObjectType, + }); + /* Start of update case with push information */ const [myCase, myCaseConfigure, comments] = await Promise.all([ caseService.getCase({ id: caseId, }), - caseConfigureService.find({ unsecuredSavedObjectsClient }), + caseConfigureService.find({ unsecuredSavedObjectsClient, options: { filter: ownerFilter } }), caseService.getAllCaseComments({ id: caseId, options: { diff --git a/x-pack/plugins/cases/server/features.ts b/x-pack/plugins/cases/server/features.ts new file mode 100644 index 00000000000000..33a3315b7d9d3f --- /dev/null +++ b/x-pack/plugins/cases/server/features.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +import { KibanaFeatureConfig } from '../../features/common'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; + +import { APP_ID, FEATURE_ID } from '../common/constants'; + +/** + * The order of appearance in the feature privilege page + * under the management section. Cases should be under + * the Actions and Connectors feature + */ + +const FEATURE_ORDER = 3100; + +export const getCasesKibanaFeature = (): KibanaFeatureConfig => ({ + id: FEATURE_ID, + name: i18n.translate('xpack.cases.features.casesFeatureName', { + defaultMessage: 'Cases', + }), + category: DEFAULT_APP_CATEGORIES.management, + app: [], + order: FEATURE_ORDER, + management: { + insightsAndAlerting: [APP_ID], + }, + cases: [APP_ID], + privileges: { + all: { + cases: { + all: [APP_ID], + }, + management: { + insightsAndAlerting: [APP_ID], + }, + savedObject: { + all: [], + read: [], + }, + ui: ['crud_cases', 'read_cases'], + }, + read: { + cases: { + read: [APP_ID], + }, + management: { + insightsAndAlerting: [APP_ID], + }, + savedObject: { + all: [], + read: [], + }, + ui: ['read_cases'], + }, + }, +}); diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index e6c4faac939389..9d2915491c4469 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -28,14 +28,19 @@ import { CasesClient } from './client'; import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; -import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { + PluginStartContract as FeaturesPluginStart, + PluginSetupContract as FeaturesPluginSetup, +} from '../../features/server'; import { LensServerPluginSetup } from '../../lens/server'; +import { getCasesKibanaFeature } from './features'; import { registerRoutes } from './routes/api/register_routes'; import { getExternalRoutes } from './routes/api/get_external_routes'; export interface PluginsSetup { actions: ActionsPluginSetup; lens: LensServerPluginSetup; + features: FeaturesPluginSetup; usageCollection?: UsageCollectionSetup; security?: SecurityPluginSetup; } @@ -77,6 +82,8 @@ export class CasePlugin { this.securityPluginSetup = plugins.security; this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory; + plugins.features.registerKibanaFeature(getCasesKibanaFeature()); + core.savedObjects.registerType( createCaseCommentSavedObjectType({ migrationDeps: { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_table.test.tsx index 0fa106fda1c8fa..085baae19899d0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_table.test.tsx @@ -54,7 +54,8 @@ const getFakeFindings = (): CspFinding & { id: string } => ({ 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/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/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/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/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 9c79397e25e103..6db1459d90c64c 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -3838,6 +3838,14 @@ "logs" ] } + }, + "data_output_id": { + "type": "string", + "nullable": true + }, + "monitoring_output_id": { + "type": "string", + "nullable": true } }, "required": [ @@ -3994,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 1ec0df0e51641b..6aaeaeaf160817 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -2411,6 +2411,12 @@ components: enum: - metrics - logs + data_output_id: + type: string + nullable: true + monitoring_output_id: + type: string + nullable: true required: - name - namespace @@ -2510,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/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/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/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/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/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/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index b44f3ffa20df71..7b7c256d5ad593 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -150,6 +150,7 @@ export function SavedViewsToolbarControls(props: Props) { data-test-subj="savedViews-openPopover" iconType="arrowDown" iconSide="right" + color="text" > {currentView ? currentView.name diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx index 3681d740d93d07..ad548a632573fd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useUiTracker } from '../../../../../../observability/public'; import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; @@ -57,17 +57,6 @@ export const BottomDrawer: React.FC<{ {isOpen ? hideHistory : showHistory} - - {children} - - @@ -97,7 +86,3 @@ const BottomActionTopBar = euiStyled(EuiFlexGroup).attrs({ const ShowHideButton = euiStyled(EuiButtonEmpty).attrs({ size: 's' })` width: 140px; `; - -const RightSideSpacer = euiStyled(EuiSpacer).attrs({ size: 'xs' })` - width: 140px; -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 5a3dafaabbd170..7f3de57b610a4d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -17,8 +17,12 @@ import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; import { PageContent } from '../../../../components/page'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; -import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; -import { InfraFormatterType } from '../../../../lib/lib'; +import { + DEFAULT_LEGEND, + useWaffleOptionsContext, + WaffleLegendOptions, +} from '../hooks/use_waffle_options'; +import { InfraFormatterType, InfraWaffleMapBounds } from '../../../../lib/lib'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Toolbar } from './toolbars/toolbar'; import { ViewSwitcher } from './waffle/view_switcher'; @@ -26,7 +30,7 @@ import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_f import { createLegend } from '../lib/create_legend'; import { useWaffleViewState } from '../hooks/use_waffle_view_state'; import { BottomDrawer } from './bottom_drawer'; -import { Legend } from './waffle/legend'; +import { LegendControls } from './waffle/legend_controls'; interface Props { shouldLoadDefault: boolean; @@ -37,149 +41,184 @@ interface Props { loading: boolean; } -export const Layout = ({ - shouldLoadDefault, - currentView, - reload, - interval, - nodes, - loading, -}: Props) => { - const [showLoading, setShowLoading] = useState(true); - const { metric, groupBy, sort, nodeType, changeView, view, autoBounds, boundsOverride, legend } = - useWaffleOptionsContext(); - const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); - const { applyFilterQuery } = useWaffleFiltersContext(); - const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette; - const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps; - const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors; - - const options = { - formatter: InfraFormatterType.percent, - formatTemplate: '{{value}}', - legend: createLegend(legendPalette, legendSteps, legendReverseColors), - metric, - sort, - groupBy, - }; - - useInterval( - () => { - if (!loading) { - jumpToTime(Date.now()); - } - }, - isAutoReloading ? 5000 : null - ); - - const dataBounds = calculateBoundsFromNodes(nodes); - const bounds = autoBounds ? dataBounds : boundsOverride; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); - const { onViewChange } = useWaffleViewState(); - - useEffect(() => { - if (currentView) { - onViewChange(currentView); - } - }, [currentView, onViewChange]); - - useEffect(() => { - // load snapshot data after default view loaded, unless we're not loading a view - if (currentView != null || !shouldLoadDefault) { - reload(); - } - - /** - * INFO: why disable exhaustive-deps - * We need to wait on the currentView not to be null because it is loaded async and could change the view state. - * We don't actually need to watch the value of currentView though, since the view state will be synched up by the - * changing params in the reload method so we should only "watch" the reload method. - * - * TODO: Should refactor this in the future to make it more clear where all the view state is coming - * from and it's precedence [query params, localStorage, defaultView, out of the box view] - */ +interface LegendControlOptions { + auto: boolean; + bounds: InfraWaffleMapBounds; + legend: WaffleLegendOptions; +} + +export const Layout = React.memo( + ({ shouldLoadDefault, currentView, reload, interval, nodes, loading }: Props) => { + const [showLoading, setShowLoading] = useState(true); + const { + metric, + groupBy, + sort, + nodeType, + changeView, + view, + autoBounds, + boundsOverride, + legend, + changeBoundsOverride, + changeAutoBounds, + changeLegend, + } = useWaffleOptionsContext(); + const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); + const { applyFilterQuery } = useWaffleFiltersContext(); + const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette; + const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps; + const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors; + + const options = { + formatter: InfraFormatterType.percent, + formatTemplate: '{{value}}', + legend: createLegend(legendPalette, legendSteps, legendReverseColors), + metric, + sort, + groupBy, + }; + + useInterval( + () => { + if (!loading) { + jumpToTime(Date.now()); + } + }, + isAutoReloading ? 5000 : null + ); + + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [reload, shouldLoadDefault]); - - useEffect(() => { - setShowLoading(true); - }, [options.metric, nodeType]); - - useEffect(() => { - const hasNodes = nodes && nodes.length; - // Don't show loading screen when we're auto-reloading - setShowLoading(!hasNodes); - }, [nodes]); - - return ( - <> - - - {({ measureRef: pageMeasureRef, bounds: { width = 0 } }) => ( - - - {({ measureRef: topActionMeasureRef, bounds: { height: topActionHeight = 0 } }) => ( - <> - - - - - - - - - - {({ measureRef, bounds: { height = 0 } }) => ( - <> - - {view === 'map' && ( - { + if (currentView) { + onViewChange(currentView); + } + }, [currentView, onViewChange]); + + useEffect(() => { + // load snapshot data after default view loaded, unless we're not loading a view + if (currentView != null || !shouldLoadDefault) { + reload(); + } + + /** + * INFO: why disable exhaustive-deps + * We need to wait on the currentView not to be null because it is loaded async and could change the view state. + * We don't actually need to watch the value of currentView though, since the view state will be synched up by the + * changing params in the reload method so we should only "watch" the reload method. + * + * TODO: Should refactor this in the future to make it more clear where all the view state is coming + * from and it's precedence [query params, localStorage, defaultView, out of the box view] + */ + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [reload, shouldLoadDefault]); + + useEffect(() => { + setShowLoading(true); + }, [options.metric, nodeType]); + + useEffect(() => { + const hasNodes = nodes && nodes.length; + // Don't show loading screen when we're auto-reloading + setShowLoading(!hasNodes); + }, [nodes]); + + const handleLegendControlChange = useCallback( + (opts: LegendControlOptions) => { + changeBoundsOverride(opts.bounds); + changeAutoBounds(opts.auto); + changeLegend(opts.legend); + }, + [changeBoundsOverride, changeAutoBounds, changeLegend] + ); + + return ( + <> + + + {({ measureRef: pageMeasureRef, bounds: { width = 0 } }) => ( + + + {({ + measureRef: topActionMeasureRef, + bounds: { height: topActionHeight = 0 }, + }) => ( + <> + + + + + {view === 'map' && ( + + + + )} + + + + + + + + {({ measureRef, bounds: { height = 0 } }) => ( + <> + - + {view === 'map' && ( + - - )} - - )} - - - )} - - - )} - - - - ); -}; + )} + + )} + + + )} + + + )} + + + + ); + } +); const MainContainer = euiStyled.div` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index 297f24e95bc4f1..cec595e4be3d66 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -18,6 +18,7 @@ import { Map } from './waffle/map'; import { TableView } from './table_view'; import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; +import { Legend } from './waffle/legend'; export interface KueryFilterQuery { kind: 'kuery'; @@ -131,6 +132,12 @@ export const NodesOverview = ({ bottomMargin={bottomMargin} staticHeight={isStatic} /> + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx index d305203b738c37..853aa98bf62447 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { @@ -17,13 +17,7 @@ import { GradientLegendRT, } from '../../../../../lib/lib'; import { GradientLegend } from './gradient_legend'; -import { LegendControls } from './legend_controls'; import { StepLegend } from './steps_legend'; -import { - DEFAULT_LEGEND, - useWaffleOptionsContext, - WaffleLegendOptions, -} from '../../hooks/use_waffle_options'; import { SteppedGradientLegend } from './stepped_gradient_legend'; interface Props { legend: InfraWaffleMapLegend; @@ -32,39 +26,9 @@ interface Props { formatter: InfraFormatter; } -interface LegendControlOptions { - auto: boolean; - bounds: InfraWaffleMapBounds; - legend: WaffleLegendOptions; -} - -export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter }) => { - const { - changeBoundsOverride, - changeAutoBounds, - autoBounds, - legend: legendOptions, - changeLegend, - boundsOverride, - } = useWaffleOptionsContext(); - const handleChange = useCallback( - (options: LegendControlOptions) => { - changeBoundsOverride(options.bounds); - changeAutoBounds(options.auto); - changeLegend(options.legend); - }, - [changeBoundsOverride, changeAutoBounds, changeLegend] - ); +export const Legend: React.FC = ({ legend, bounds, formatter }) => { return ( - {GradientLegendRT.is(legend) && ( )} @@ -77,8 +41,6 @@ export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter }; const LegendContainer = euiStyled.div` - position: absolute; - bottom: 0px; - left: 10px; - right: 10px; + margin: 0 10px; + display: flex; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index c7479434424a63..61b293888b85dc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -26,7 +26,6 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { SyntheticEvent, useState, useCallback, useEffect } from 'react'; import { first, last } from 'lodash'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, InventoryColorPalette, PALETTES } from '../../../../../lib/lib'; import { WaffleLegendOptions } from '../../hooks/use_waffle_options'; import { getColorPalette } from '../../lib/get_color_palette'; @@ -78,8 +77,10 @@ export const LegendControls = ({ const buttonComponent = ( - - Legend Options - - - <> - - - - - - - + Legend Options + + + <> + - - + + + + + - + + + + + + + + + } + isInvalid={!boundsValidRange} + display="columnCompressed" + error={errors} + > +
+ - - - + + + } + isInvalid={!boundsValidRange} + error={errors} + > +
+ - - + + + + + + - } - isInvalid={!boundsValidRange} - display="columnCompressed" - error={errors} - > -
- + + + + -
- - - } - isInvalid={!boundsValidRange} - error={errors} - > -
- -
-
- - - - - - - - - - - - - - - - + +
+
+ + ); }; - -const ControlContainer = euiStyled.div` - position: absolute; - top: -20px; - right: 6px; - bottom: 0; -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx index a9bcfa7995c200..339426b126b9e6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { EuiText } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, @@ -22,18 +23,19 @@ type TickValue = 0 | 1; export const SteppedGradientLegend: React.FC = ({ legend, bounds, formatter }) => { return ( - - - - + - {legend.rules.map((rule, index) => ( - - ))} + {legend.rules + .slice() + .reverse() + .map((rule, index) => ( + + ))} + ); }; @@ -46,62 +48,38 @@ interface TickProps { const TickLabel = ({ value, bounds, formatter }: TickProps) => { const normalizedValue = value === 0 ? bounds.min : bounds.max * value; - const style = { left: `${value * 100}%` }; const label = formatter(normalizedValue); - return {label}; + return ( +
+ {label} +
+ ); }; -const GradientStep = euiStyled.div` - height: ${(props) => props.theme.eui.paddingSizes.s}; - flex: 1 1 auto; - &:first-child { - border-radius: ${(props) => props.theme.eui.euiBorderRadius} 0 0 ${(props) => - props.theme.eui.euiBorderRadius}; - } - &:last-child { - border-radius: 0 ${(props) => props.theme.eui.euiBorderRadius} ${(props) => - props.theme.eui.euiBorderRadius} 0; - } +const LegendContainer = euiStyled.div` + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; `; -const Ticks = euiStyled.div` - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - top: -18px; +const GradientContainer = euiStyled.div` + height: 200px; + width: 10px; + display: flex; + flex-direction: column; + align-items: stretch; `; -const Tick = euiStyled.div` - position: absolute; - font-size: 11px; - text-align: center; - top: 0; - left: 0; - white-space: nowrap; - transform: translate(-50%, 0); +const GradientStep = euiStyled.div` + flex: 1 1 auto; &:first-child { - padding-left: 5px; - transform: translate(0, 0); + border-radius: ${(props) => props.theme.eui.euiBorderRadius} ${(props) => + props.theme.eui.euiBorderRadius} 0 0; } &:last-child { - padding-right: 5px; - transform: translate(-100%, 0); + border-radius: 0 0 ${(props) => props.theme.eui.euiBorderRadius} ${(props) => + props.theme.eui.euiBorderRadius}; } `; - -const GradientContainer = euiStyled.div` - display: flex; - flex-direction; row; - align-items: stretch; - flex-grow: 1; -`; - -const LegendContainer = euiStyled.div` - position: absolute; - height: 10px; - bottom: 0; - left: 0; - right: 40px; -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx index 4dc288caa98332..8e911f7f829177 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx @@ -37,8 +37,8 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { defaultMessage: 'Switch between table and map view', })} options={buttons} - color="primary" - buttonSize="m" + color="text" + buttonSize="s" idSelected={view} onChange={onChange} isIconOnly diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index b16afbfc56a4ab..c4e82aca9ad454 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -635,6 +635,18 @@ describe('Lens App', () => { ); }); + it('applies all changes on-save', async () => { + const { lensStore } = await save({ + initialSavedObjectId: undefined, + newCopyOnSave: false, + newTitle: 'hello there', + preloadedState: { + applyChangesCounter: 0, + }, + }); + expect(lensStore.getState().lens.applyChangesCounter).toBe(1); + }); + it('adds to the recently accessed list on save', async () => { const { services } = await save({ initialSavedObjectId: undefined, diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 5ef9e05cf590b0..6312225af579b6 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -24,6 +24,7 @@ import { Document } from '../persistence/saved_object_store'; import { setState, + applyChanges, useLensSelector, useLensDispatch, LensAppState, @@ -276,6 +277,7 @@ export function App({ const runSave = useCallback( (saveProps: SaveProps, options: { saveToLibrary: boolean }) => { + dispatch(applyChanges()); return runSaveLensVisualization( { lastKnownDoc, @@ -316,6 +318,7 @@ export function App({ redirectTo, lensAppServices, dispatchSetState, + dispatch, setIsSaveModalVisible, ] ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx index f7402e78ebd96f..5bc6a69b2efaf1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx @@ -24,6 +24,26 @@ import { import { i18n } from '@kbn/i18n'; +/** + * The dimension container is set up to close when it detects a click outside it. + * Use this CSS class to exclude particular elements from this behavior. + */ +export const DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS = + 'lensDontCloseDimensionContainerOnClick'; + +function fromExcludedClickTarget(event: Event) { + for ( + let node: HTMLElement | null = event.target as HTMLElement; + node !== null; + node = node!.parentElement + ) { + if (node.classList!.contains(DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS)) { + return true; + } + } + return false; +} + export function DimensionContainer({ isOpen, groupLabel, @@ -77,8 +97,8 @@ export function DimensionContainer({ { - if (isFullscreen) { + onOutsideClick={(event) => { + if (isFullscreen || fromExcludedClickTarget(event)) { return; } closeFlyout(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx new file mode 100644 index 00000000000000..64656a2eedf63c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { DataPanelWrapper } from './data_panel_wrapper'; +import { Datasource, DatasourceDataPanelProps } from '../../types'; +import { DragDropIdentifier } from '../../drag_drop'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { mockStoreDeps, mountWithProvider } from '../../mocks'; +import { disableAutoApply } from '../../state_management/lens_slice'; +import { selectTriggerApplyChanges } from '../../state_management'; + +describe('Data Panel Wrapper', () => { + describe('Datasource data panel properties', () => { + let datasourceDataPanelProps: DatasourceDataPanelProps; + let lensStore: Awaited>['lensStore']; + beforeEach(async () => { + const renderDataPanel = jest.fn(); + + const datasourceMap = { + activeDatasource: { + renderDataPanel, + } as unknown as Datasource, + }; + + const mountResult = await mountWithProvider( + {}} + core={{} as DatasourceDataPanelProps['core']} + dropOntoWorkspace={(field: DragDropIdentifier) => {}} + hasSuggestionForField={(field: DragDropIdentifier) => true} + plugins={{ uiActions: {} as UiActionsStart }} + />, + { + preloadedState: { + activeDatasourceId: 'activeDatasource', + datasourceStates: { + activeDatasource: { + isLoading: false, + state: { + age: 'old', + }, + }, + }, + }, + storeDeps: mockStoreDeps({ datasourceMap }), + } + ); + + lensStore = mountResult.lensStore; + + datasourceDataPanelProps = renderDataPanel.mock.calls[0][1] as DatasourceDataPanelProps; + }); + + describe('setState', () => { + it('applies state immediately when option true', async () => { + lensStore.dispatch(disableAutoApply()); + selectTriggerApplyChanges(lensStore.getState()); + + const newDatasourceState = { age: 'new' }; + datasourceDataPanelProps.setState(newDatasourceState, { applyImmediately: true }); + + expect(lensStore.getState().lens.datasourceStates.activeDatasource.state).toEqual( + newDatasourceState + ); + expect(selectTriggerApplyChanges(lensStore.getState())).toBeTruthy(); + }); + + it('does not apply state immediately when option false', async () => { + lensStore.dispatch(disableAutoApply()); + selectTriggerApplyChanges(lensStore.getState()); + + const newDatasourceState = { age: 'new' }; + datasourceDataPanelProps.setState(newDatasourceState, { applyImmediately: false }); + + const lensState = lensStore.getState().lens; + expect(lensState.datasourceStates.activeDatasource.state).toEqual(newDatasourceState); + expect(selectTriggerApplyChanges(lensStore.getState())).toBeFalsy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index b77d313973432c..17f3d385123c20 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -20,6 +20,7 @@ import { updateDatasourceState, useLensSelector, setState, + applyChanges, selectExecutionContext, selectActiveDatasourceId, selectDatasourceStates, @@ -45,8 +46,8 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { : true; const dispatchLens = useLensDispatch(); - const setDatasourceState: StateSetter = useMemo(() => { - return (updater) => { + const setDatasourceState: StateSetter = useMemo(() => { + return (updater: unknown | ((prevState: unknown) => unknown), options) => { dispatchLens( updateDatasourceState({ updater, @@ -54,6 +55,9 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { clearStagedPreview: true, }) ); + if (options?.applyImmediately) { + dispatchLens(applyChanges()); + } }; }, [activeDatasourceId, dispatchLens]); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index f2e4af61ddbdb6..7f1c673d0d1ddc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -79,7 +79,7 @@ export function EditorFrame(props: EditorFrameProps) { const suggestion = getSuggestionForField.current!(field); if (suggestion) { trackUiEvent('drop_onto_workspace'); - switchToSuggestion(dispatchLens, suggestion, true); + switchToSuggestion(dispatchLens, suggestion, { clearStagedPreview: true }); } }, [getSuggestionForField, dispatchLens] diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss index c8c0a6e2ebbd22..b49c77bb8b4198 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss @@ -73,7 +73,6 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ } &.lnsFrameLayout__pageBody-isFullscreen { - background: $euiColorEmptyShade; flex: 1; padding: 0; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index b8ce851f25349c..c9d237961b4752 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -27,6 +27,7 @@ import { switchVisualization, DatasourceStates, VisualizationState, + applyChanges, } from '../../state_management'; /** @@ -232,7 +233,10 @@ export function switchToSuggestion( Suggestion, 'visualizationId' | 'visualizationState' | 'datasourceState' | 'datasourceId' >, - clearStagedPreview?: boolean + options?: { + clearStagedPreview?: boolean; + applyImmediately?: boolean; + } ) { dispatchLens( switchVisualization({ @@ -242,9 +246,12 @@ export function switchToSuggestion( datasourceState: suggestion.datasourceState, datasourceId: suggestion.datasourceId!, }, - clearStagedPreview, + clearStagedPreview: options?.clearStagedPreview, }) ); + if (options?.applyImmediately) { + dispatchLens(applyChanges()); + } } export function getTopSuggestionForField( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.scss index 37a4a88c32f22e..804bfbf11d7406 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.scss @@ -88,3 +88,7 @@ text-align: center; flex-grow: 0; } + +.lnsSuggestionPanel__applyChangesPrompt { + height: $lnsSuggestionHeight; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index c9ddc0ea6551c7..8d9ea9b3c70b71 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -21,7 +21,21 @@ import { getSuggestions } from './suggestion_helpers'; import { EuiIcon, EuiPanel, EuiToolTip, EuiAccordion } from '@elastic/eui'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; import { mountWithProvider } from '../../mocks'; -import { LensAppState, PreviewState, setState, setToggleFullscreen } from '../../state_management'; +import { + applyChanges, + LensAppState, + PreviewState, + setState, + setToggleFullscreen, + VisualizationState, +} from '../../state_management'; +import { setChangesApplied } from '../../state_management/lens_slice'; + +const SELECTORS = { + APPLY_CHANGES_BUTTON: 'button[data-test-subj="lnsSuggestionApplyChanges"]', + SUGGESTIONS_PANEL: '[data-test-subj="lnsSuggestionsPanel"]', + SUGGESTION_TILE_BUTTON: 'button[data-test-subj="lnsSuggestion"]', +}; jest.mock('./suggestion_helpers'); @@ -108,6 +122,38 @@ describe('suggestion_panel', () => { expect(instance.find(SuggestionPanel).exists()).toBe(true); }); + it('should display apply-changes prompt when changes not applied', async () => { + const { instance, lensStore } = await mountWithProvider(, { + preloadedState: { + ...preloadedState, + visualization: { + ...preloadedState.visualization, + state: { + something: 'changed', + }, + } as VisualizationState, + changesApplied: false, + autoApplyDisabled: true, + }, + }); + + expect(instance.exists(SELECTORS.APPLY_CHANGES_BUTTON)).toBeTruthy(); + expect(instance.exists(SELECTORS.SUGGESTION_TILE_BUTTON)).toBeFalsy(); + + instance.find(SELECTORS.APPLY_CHANGES_BUTTON).simulate('click'); + + // check changes applied + expect(lensStore.dispatch).toHaveBeenCalledWith(applyChanges()); + + // simulate workspace panel behavior + lensStore.dispatch(setChangesApplied(true)); + instance.update(); + + // check UI updated + expect(instance.exists(SELECTORS.APPLY_CHANGES_BUTTON)).toBeFalsy(); + expect(instance.exists(SELECTORS.SUGGESTION_TILE_BUTTON)).toBeTruthy(); + }); + it('should list passed in suggestions', async () => { const { instance } = await mountWithProvider(, { preloadedState, @@ -173,12 +219,12 @@ describe('suggestion_panel', () => { preloadedState, }); act(() => { - instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click'); + instance.find(SELECTORS.SUGGESTION_TILE_BUTTON).at(2).simulate('click'); }); instance.update(); - expect(instance.find('[data-test-subj="lnsSuggestion"]').at(2).prop('className')).toContain( + expect(instance.find(SELECTORS.SUGGESTION_TILE_BUTTON).at(2).prop('className')).toContain( 'lnsSuggestionPanel__button-isSelected' ); }); @@ -189,13 +235,13 @@ describe('suggestion_panel', () => { ); act(() => { - instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click'); + instance.find(SELECTORS.SUGGESTION_TILE_BUTTON).at(2).simulate('click'); }); instance.update(); act(() => { - instance.find('[data-test-subj="lnsSuggestion"]').at(0).simulate('click'); + instance.find(SELECTORS.SUGGESTION_TILE_BUTTON).at(0).simulate('click'); }); instance.update(); @@ -203,6 +249,10 @@ describe('suggestion_panel', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/rollbackSuggestion', }); + // check that it immediately applied any state changes in case auto-apply disabled + expect(lensStore.dispatch).toHaveBeenLastCalledWith({ + type: applyChanges.type, + }); }); }); @@ -212,7 +262,7 @@ describe('suggestion_panel', () => { }); act(() => { - instance.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click'); + instance.find(SELECTORS.SUGGESTION_TILE_BUTTON).at(1).simulate('click'); }); expect(lensStore.dispatch).toHaveBeenCalledWith( @@ -228,6 +278,7 @@ describe('suggestion_panel', () => { }, }) ); + expect(lensStore.dispatch).toHaveBeenLastCalledWith({ type: applyChanges.type }); }); it('should render render icon if there is no preview expression', async () => { @@ -264,10 +315,10 @@ describe('suggestion_panel', () => { preloadedState, }); - expect(instance.find('[data-test-subj="lnsSuggestionsPanel"]').find(EuiIcon)).toHaveLength(1); - expect( - instance.find('[data-test-subj="lnsSuggestionsPanel"]').find(EuiIcon).prop('type') - ).toEqual(LensIconChartDatatable); + expect(instance.find(SELECTORS.SUGGESTIONS_PANEL).find(EuiIcon)).toHaveLength(1); + expect(instance.find(SELECTORS.SUGGESTIONS_PANEL).find(EuiIcon).prop('type')).toEqual( + LensIconChartDatatable + ); }); it('should return no suggestion if visualization has missing index-patterns', async () => { @@ -301,7 +352,7 @@ describe('suggestion_panel', () => { instance.find(EuiAccordion).at(0).simulate('change'); }); - expect(instance.find('[data-test-subj="lnsSuggestionsPanel"]')).toEqual({}); + expect(instance.find(SELECTORS.SUGGESTIONS_PANEL)).toEqual({}); }); it('should render preview expression if there is one', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index f6fccbb831eae2..e6a9831e0aae59 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -19,6 +19,10 @@ import { EuiToolTip, EuiButtonEmpty, EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSpacer, } from '@elastic/eui'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import { Ast, toExpression } from '@kbn/interpreter'; @@ -55,7 +59,10 @@ import { selectActiveDatasourceId, selectActiveData, selectDatasourceStates, + selectChangesApplied, + applyChanges, } from '../../state_management'; +import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from './config_panel/dimension_container'; const MAX_SUGGESTIONS_DISPLAYED = 5; const LOCAL_STORAGE_SUGGESTIONS_PANEL = 'LENS_SUGGESTIONS_PANEL_HIDDEN'; @@ -190,6 +197,7 @@ export function SuggestionPanel({ const existsStagedPreview = useLensSelector((state) => Boolean(state.lens.stagedPreview)); const currentVisualization = useLensSelector(selectCurrentVisualization); const currentDatasourceStates = useLensSelector(selectCurrentDatasourceStates); + const changesApplied = useLensSelector(selectChangesApplied); // get user's selection from localStorage, this key defines if the suggestions panel will be hidden or not const [hideSuggestions, setHideSuggestions] = useLocalStorage( LOCAL_STORAGE_SUGGESTIONS_PANEL, @@ -327,9 +335,92 @@ export function SuggestionPanel({ trackSuggestionEvent('back_to_current'); setLastSelectedSuggestion(-1); dispatchLens(rollbackSuggestion()); + dispatchLens(applyChanges()); } } + const applyChangesPrompt = ( + + + +

+ +

+ + dispatchLens(applyChanges())} + data-test-subj="lnsSuggestionApplyChanges" + > + + +
+
+
+ ); + + const suggestionsUI = ( + <> + {currentVisualization.activeId && !hideSuggestions && ( + + )} + {!hideSuggestions && + suggestions.map((suggestion, index) => { + return ( + { + trackUiEvent('suggestion_clicked'); + if (lastSelectedSuggestion === index) { + rollbackToCurrentVisualization(); + } else { + setLastSelectedSuggestion(index); + switchToSuggestion(dispatchLens, suggestion, { applyImmediately: true }); + } + }} + selected={index === lastSelectedSuggestion} + /> + ); + })} + + ); + return (
- {currentVisualization.activeId && !hideSuggestions && ( - - )} - {!hideSuggestions && - suggestions.map((suggestion, index) => { - return ( - { - trackUiEvent('suggestion_clicked'); - if (lastSelectedSuggestion === index) { - rollbackToCurrentVisualization(); - } else { - setLastSelectedSuggestion(index); - switchToSuggestion(dispatchLens, suggestion); - } - }} - selected={index === lastSelectedSuggestion} - /> - ); - })} + {changesApplied ? suggestionsUI : applyChangesPrompt}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx index c325e6d516c8b7..a486b6315c3f46 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx @@ -31,6 +31,7 @@ jest.mock('react-virtualized-auto-sizer', () => { import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types'; import { ChartSwitch } from './chart_switch'; import { PaletteOutput } from 'src/plugins/charts/public'; +import { applyChanges } from '../../../state_management'; describe('chart_switch', () => { function generateVisualization(id: string): jest.Mocked { @@ -189,6 +190,7 @@ describe('chart_switch', () => { clearStagedPreview: true, }, }); + expect(lensStore.dispatch).not.toHaveBeenCalledWith({ type: applyChanges.type }); // should not apply changes automatically }); it('should use initial state if there is no suggestion from the target visualization', async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index d24ed0a736ae24..5c528832ac5b28 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -132,7 +132,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { ...selection, visualizationState: selection.getVisualizationState(), }, - true + { clearStagedPreview: true } ); if ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index ccd9e8aace2ab2..7359f7cdc185b2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -35,9 +35,15 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; -import { LensRootStore, setState } from '../../../state_management'; +import { + applyChanges, + setState, + updateDatasourceState, + updateVisualizationState, +} from '../../../state_management'; import { getLensInspectorService } from '../../../lens_inspector_service'; import { inspectorPluginMock } from '../../../../../../../src/plugins/inspector/public/mocks'; +import { disableAutoApply, enableAutoApply } from '../../../state_management/lens_slice'; const defaultPermissions: Record>> = { navLinks: { management: true }, @@ -102,12 +108,13 @@ describe('workspace_panel', () => { }} ExpressionRenderer={expressionRendererMock} />, - { preloadedState: { visualization: { activeId: null, state: {} }, datasourceStates: {} }, } ); instance = mounted.instance; + instance.update(); + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -124,6 +131,7 @@ describe('workspace_panel', () => { { preloadedState: { datasourceStates: {} } } ); instance = mounted.instance; + instance.update(); expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); @@ -141,6 +149,7 @@ describe('workspace_panel', () => { { preloadedState: { datasourceStates: {} } } ); instance = mounted.instance; + instance.update(); expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); @@ -170,6 +179,8 @@ describe('workspace_panel', () => { instance = mounted.instance; + instance.update(); + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} @@ -177,6 +188,188 @@ describe('workspace_panel', () => { `); }); + it('should give user control when auto-apply disabled', async () => { + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + + const mounted = await mountWithProvider( + 'testVis' }, + }} + ExpressionRenderer={expressionRendererMock} + />, + { + preloadedState: { + autoApplyDisabled: true, + }, + } + ); + + instance = mounted.instance; + instance.update(); + + // allows initial render + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + "kibana + | lens_merge_tables layerIds=\\"first\\" tables={datasource} + | testVis" + `); + + mockDatasource.toExpression.mockReturnValue('new-datasource'); + act(() => { + instance.setProps({ + visualizationMap: { + testVis: { ...mockVisualization, toExpression: () => 'new-vis' }, + }, + }); + }); + + // nothing should change + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + "kibana + | lens_merge_tables layerIds=\\"first\\" tables={datasource} + | testVis" + `); + + act(() => { + mounted.lensStore.dispatch(applyChanges()); + }); + instance.update(); + + // should update + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + "kibana + | lens_merge_tables layerIds=\\"first\\" tables={new-datasource} + | new-vis" + `); + + mockDatasource.toExpression.mockReturnValue('other-new-datasource'); + act(() => { + instance.setProps({ + visualizationMap: { + testVis: { ...mockVisualization, toExpression: () => 'other-new-vis' }, + }, + }); + }); + + // should not update + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + "kibana + | lens_merge_tables layerIds=\\"first\\" tables={new-datasource} + | new-vis" + `); + + act(() => { + mounted.lensStore.dispatch(enableAutoApply()); + }); + instance.update(); + + // reenabling auto-apply triggers an update as well + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + "kibana + | lens_merge_tables layerIds=\\"first\\" tables={other-new-datasource} + | other-new-vis" + `); + }); + + it('should base saveability on working changes when auto-apply disabled', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockVisualization.getErrorMessages.mockImplementation((currentVisualizationState: any) => { + if (currentVisualizationState.hasProblem) { + return [{ shortMessage: 'An error occurred', longMessage: 'An long description here' }]; + } else { + return []; + } + }); + + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + + const mounted = await mountWithProvider( + 'testVis' }, + }} + ExpressionRenderer={expressionRendererMock} + /> + ); + + instance = mounted.instance; + const isSaveable = () => mounted.lensStore.getState().lens.isSaveable; + + instance.update(); + + // allows initial render + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + "kibana + | lens_merge_tables layerIds=\\"first\\" tables={datasource} + | testVis" + `); + expect(isSaveable()).toBe(true); + + act(() => { + mounted.lensStore.dispatch( + updateVisualizationState({ + visualizationId: 'testVis', + newState: { activeId: 'testVis', hasProblem: true }, + }) + ); + }); + instance.update(); + + expect(isSaveable()).toBe(false); + }); + + it('should allow empty workspace as initial render when auto-apply disabled', async () => { + mockVisualization.toExpression.mockReturnValue('testVis'); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + const mounted = await mountWithProvider( + , + { + preloadedState: { + autoApplyDisabled: true, + }, + } + ); + + instance = mounted.instance; + instance.update(); + + expect(instance.exists('[data-test-subj="empty-workspace"]')).toBeTruthy(); + }); + it('should execute a trigger on expression event', async () => { const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { @@ -289,6 +482,7 @@ describe('workspace_panel', () => { } ); instance = mounted.instance; + instance.update(); const ast = fromExpression(instance.find(expressionRendererMock).prop('expression') as string); @@ -342,27 +536,25 @@ describe('workspace_panel', () => { expressionRendererMock = jest.fn((_arg) => ); - await act(async () => { - const mounted = await mountWithProvider( - 'testVis' }, - }} - ExpressionRenderer={expressionRendererMock} - /> - ); - instance = mounted.instance; - }); + const mounted = await mountWithProvider( + 'testVis' }, + }} + ExpressionRenderer={expressionRendererMock} + /> + ); + instance = mounted.instance; instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(1); + expect(expressionRendererMock).toHaveBeenCalledTimes(2); - await act(async () => { + act(() => { instance.setProps({ framePublicAPI: { ...framePublicAPI, @@ -373,7 +565,7 @@ describe('workspace_panel', () => { instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(2); + expect(expressionRendererMock).toHaveBeenCalledTimes(3); }); it('should run the expression again if the filters change', async () => { @@ -388,31 +580,29 @@ describe('workspace_panel', () => { .mockReturnValueOnce('datasource second'); expressionRendererMock = jest.fn((_arg) => ); - await act(async () => { - const mounted = await mountWithProvider( - 'testVis' }, - }} - ExpressionRenderer={expressionRendererMock} - /> - ); - instance = mounted.instance; - }); + const mounted = await mountWithProvider( + 'testVis' }, + }} + ExpressionRenderer={expressionRendererMock} + /> + ); + instance = mounted.instance; instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(1); + expect(expressionRendererMock).toHaveBeenCalledTimes(2); const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; - await act(async () => { + act(() => { instance.setProps({ framePublicAPI: { ...framePublicAPI, @@ -423,7 +613,7 @@ describe('workspace_panel', () => { instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(2); + expect(expressionRendererMock).toHaveBeenCalledTimes(3); }); it('should show an error message if there are missing indexpatterns in the visualization', async () => { @@ -572,6 +762,9 @@ describe('workspace_panel', () => { /> ); instance = mounted.instance; + act(() => { + instance.update(); + }); expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); @@ -642,6 +835,97 @@ describe('workspace_panel', () => { expect(instance.find(expressionRendererMock)).toHaveLength(0); }); + it('should NOT display errors for unapplied changes', async () => { + // this test is important since we don't want the workspace panel to + // display errors if the user has disabled auto-apply, messed something up, + // but not yet applied their changes + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockDatasource.getErrorMessages.mockImplementation((currentDatasourceState: any) => { + if (currentDatasourceState.hasProblem) { + return [{ shortMessage: 'An error occurred', longMessage: 'An long description here' }]; + } else { + return []; + } + }); + mockDatasource.getLayers.mockReturnValue(['first']); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockVisualization.getErrorMessages.mockImplementation((currentVisualizationState: any) => { + if (currentVisualizationState.hasProblem) { + return [{ shortMessage: 'An error occurred', longMessage: 'An long description here' }]; + } else { + return []; + } + }); + mockVisualization.toExpression.mockReturnValue('testVis'); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + const mounted = await mountWithProvider( + + ); + + instance = mounted.instance; + const lensStore = mounted.lensStore; + + const showingErrors = () => + instance.exists('[data-test-subj="configuration-failure-error"]') || + instance.exists('[data-test-subj="configuration-failure-more-errors"]'); + + expect(showingErrors()).toBeFalsy(); + + act(() => { + lensStore.dispatch(disableAutoApply()); + }); + instance.update(); + + expect(showingErrors()).toBeFalsy(); + + // introduce some issues + act(() => { + lensStore.dispatch( + updateDatasourceState({ + datasourceId: 'testDatasource', + updater: { hasProblem: true }, + }) + ); + }); + instance.update(); + + expect(showingErrors()).toBeFalsy(); + + act(() => { + lensStore.dispatch( + updateVisualizationState({ + visualizationId: 'testVis', + newState: { activeId: 'testVis', hasProblem: true }, + }) + ); + }); + instance.update(); + + expect(showingErrors()).toBeFalsy(); + + // errors should appear when problem changes are applied + act(() => { + lensStore.dispatch(applyChanges()); + }); + instance.update(); + + expect(showingErrors()).toBeTruthy(); + }); + it('should show an error message if the expression fails to parse', async () => { mockDatasource.toExpression.mockReturnValue('|||'); mockDatasource.getLayers.mockReturnValue(['first']); @@ -676,30 +960,27 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - await act(async () => { - const mounted = await mountWithProvider( - 'testVis' }, - }} - ExpressionRenderer={expressionRendererMock} - /> - ); - instance = mounted.instance; - }); - + const mounted = await mountWithProvider( + 'testVis' }, + }} + ExpressionRenderer={expressionRendererMock} + /> + ); + instance = mounted.instance; instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(1); + expect(expressionRendererMock).toHaveBeenCalledTimes(2); instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(1); + expect(expressionRendererMock).toHaveBeenCalledTimes(2); }); it('should attempt to run the expression again if it changes', async () => { @@ -709,28 +990,25 @@ describe('workspace_panel', () => { framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - let lensStore: LensRootStore; - await act(async () => { - const mounted = await mountWithProvider( - 'testVis' }, - }} - ExpressionRenderer={expressionRendererMock} - /> - ); - instance = mounted.instance; - lensStore = mounted.lensStore; - }); + const mounted = await mountWithProvider( + 'testVis' }, + }} + ExpressionRenderer={expressionRendererMock} + /> + ); + instance = mounted.instance; + const lensStore = mounted.lensStore; instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(1); + expect(expressionRendererMock).toHaveBeenCalledTimes(2); expressionRendererMock.mockImplementation((_) => { return ; @@ -746,7 +1024,7 @@ describe('workspace_panel', () => { ); instance.update(); - expect(expressionRendererMock).toHaveBeenCalledTimes(2); + expect(expressionRendererMock).toHaveBeenCalledTimes(3); expect(instance.find(expressionRendererMock)).toHaveLength(1); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index a26d72f1b4fc2d..acebc640e3a09b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect, useMemo, useContext, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useContext, useCallback, useRef } from 'react'; import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n-react'; import { toExpression } from '@kbn/interpreter'; @@ -66,9 +66,12 @@ import { selectDatasourceStates, selectActiveDatasourceId, selectSearchSessionId, + selectAutoApplyEnabled, + selectTriggerApplyChanges, } from '../../../state_management'; import type { LensInspector } from '../../../lens_inspector_service'; import { inferTimeField } from '../../../utils'; +import { setChangesApplied } from '../../../state_management/lens_slice'; export interface WorkspacePanelProps { visualizationMap: VisualizationMap; @@ -88,6 +91,7 @@ interface WorkspaceState { fixAction?: DatasourceFixAction; }>; expandError: boolean; + expressionToRender: string | null | undefined; } const dropProps = { @@ -136,13 +140,22 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ const visualization = useLensSelector(selectVisualization); const activeDatasourceId = useLensSelector(selectActiveDatasourceId); const datasourceStates = useLensSelector(selectDatasourceStates); + const autoApplyEnabled = useLensSelector(selectAutoApplyEnabled); + const triggerApply = useLensSelector(selectTriggerApplyChanges); - const { datasourceLayers } = framePublicAPI; const [localState, setLocalState] = useState({ expressionBuildError: undefined, expandError: false, + expressionToRender: undefined, }); + // const expressionToRender = useRef(); + const initialRenderComplete = useRef(); + + const shouldApplyExpression = autoApplyEnabled || !initialRenderComplete.current || triggerApply; + + const { datasourceLayers } = framePublicAPI; + const activeVisualization = visualization.activeId ? visualizationMap[visualization.activeId] : null; @@ -186,7 +199,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ [activeVisualization, visualization.state, activeDatasourceId, datasourceMap, datasourceStates] ); - const expression = useMemo(() => { + const _expression = useMemo(() => { if (!configurationValidationError?.length && !missingRefsErrors.length && !unknownVisError) { try { const ast = buildExpression({ @@ -238,10 +251,32 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ visualization.activeId, ]); - const expressionExists = Boolean(expression); useEffect(() => { - dispatchLens(setSaveable(expressionExists)); - }, [expressionExists, dispatchLens]); + dispatchLens(setSaveable(Boolean(_expression))); + }, [_expression, dispatchLens]); + + useEffect(() => { + if (!autoApplyEnabled) { + dispatchLens(setChangesApplied(_expression === localState.expressionToRender)); + } + }); + + useEffect(() => { + if (shouldApplyExpression) { + setLocalState((s) => ({ ...s, expressionToRender: _expression })); + } + }, [_expression, shouldApplyExpression]); + + const expressionExists = Boolean(localState.expressionToRender); + useEffect(() => { + // null signals an empty workspace which should count as an initial render + if ( + (expressionExists || localState.expressionToRender === null) && + !initialRenderComplete.current + ) { + initialRenderComplete.current = true; + } + }, [expressionExists, localState.expressionToRender]); const onEvent = useCallback( (event: ExpressionRendererEvent) => { @@ -291,7 +326,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ if (suggestionForDraggedField) { trackUiEvent('drop_onto_workspace'); trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty'); - switchToSuggestion(dispatchLens, suggestionForDraggedField, true); + switchToSuggestion(dispatchLens, suggestionForDraggedField, { clearStagedPreview: true }); } }, [suggestionForDraggedField, expressionExists, dispatchLens]); @@ -343,12 +378,12 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ }; const renderVisualization = () => { - if (expression === null) { + if (localState.expressionToRender === null) { return renderEmptyWorkspace(); } return ( { @@ -392,7 +425,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ order={dropProps.order} > - {element} + {renderVisualization()} ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss index 9a87f1ba46e94a..9b4502ea819445 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss @@ -32,6 +32,7 @@ &.lnsWorkspacePanelWrapper--fullscreen { margin-bottom: 0; } + } .lnsWorkspacePanel__dragDrop { @@ -80,6 +81,14 @@ .lnsWorkspacePanelWrapper__toolbar { margin-bottom: 0; + + &.lnsWorkspacePanelWrapper__toolbar--fullscreen { + padding: $euiSizeS $euiSizeS 0 $euiSizeS; + } + + & > .euiFlexItem { + min-height: $euiButtonHeightSmall; + } } .lnsDropIllustration__adjustFill { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx index fb77ff75324f09..3aab4d6e7d85c4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx @@ -10,6 +10,14 @@ import { Visualization } from '../../../types'; import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../../../mocks'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { mountWithProvider } from '../../../mocks'; +import { ReactWrapper } from 'enzyme'; +import { + selectAutoApplyEnabled, + updateVisualizationState, + disableAutoApply, + selectTriggerApplyChanges, +} from '../../../state_management'; +import { setChangesApplied } from '../../../state_management/lens_slice'; describe('workspace_panel_wrapper', () => { let mockVisualization: jest.Mocked; @@ -61,4 +69,144 @@ describe('workspace_panel_wrapper', () => { setState: expect.anything(), }); }); + + describe('auto-apply controls', () => { + class Harness { + private _instance: ReactWrapper; + + constructor(instance: ReactWrapper) { + this._instance = instance; + } + + update() { + this._instance.update(); + } + + private get applyChangesButton() { + return this._instance.find('button[data-test-subj="lensApplyChanges"]'); + } + + private get autoApplyToggleSwitch() { + return this._instance.find('button[data-test-subj="lensToggleAutoApply"]'); + } + + toggleAutoApply() { + this.autoApplyToggleSwitch.simulate('click'); + } + + public get autoApplySwitchOn() { + return this.autoApplyToggleSwitch.prop('aria-checked'); + } + + applyChanges() { + this.applyChangesButton.simulate('click'); + } + + public get applyChangesExists() { + return this.applyChangesButton.exists(); + } + + public get applyChangesDisabled() { + if (!this.applyChangesExists) { + throw Error('apply changes button doesnt exist'); + } + return this.applyChangesButton.prop('disabled'); + } + } + + let store: Awaited>['lensStore']; + let harness: Harness; + beforeEach(async () => { + const { instance, lensStore } = await mountWithProvider( + +
+ + ); + + store = lensStore; + harness = new Harness(instance); + }); + + it('toggles auto-apply', async () => { + store.dispatch(disableAutoApply()); + harness.update(); + + expect(selectAutoApplyEnabled(store.getState())).toBeFalsy(); + expect(harness.autoApplySwitchOn).toBeFalsy(); + expect(harness.applyChangesExists).toBeTruthy(); + + harness.toggleAutoApply(); + + expect(selectAutoApplyEnabled(store.getState())).toBeTruthy(); + expect(harness.autoApplySwitchOn).toBeTruthy(); + expect(harness.applyChangesExists).toBeFalsy(); + + harness.toggleAutoApply(); + + expect(selectAutoApplyEnabled(store.getState())).toBeFalsy(); + expect(harness.autoApplySwitchOn).toBeFalsy(); + expect(harness.applyChangesExists).toBeTruthy(); + }); + + it('apply-changes button works', () => { + store.dispatch(disableAutoApply()); + harness.update(); + + expect(selectAutoApplyEnabled(store.getState())).toBeFalsy(); + expect(harness.applyChangesDisabled).toBeTruthy(); + + // make a change + store.dispatch( + updateVisualizationState({ + visualizationId: store.getState().lens.visualization.activeId as string, + newState: { something: 'changed' }, + }) + ); + // simulate workspace panel behavior + store.dispatch(setChangesApplied(false)); + harness.update(); + + expect(harness.applyChangesDisabled).toBeFalsy(); + + harness.applyChanges(); + + expect(selectTriggerApplyChanges(store.getState())).toBeTruthy(); + // simulate workspace panel behavior + store.dispatch(setChangesApplied(true)); + harness.update(); + + expect(harness.applyChangesDisabled).toBeTruthy(); + }); + + it('enabling auto apply while having unapplied changes works', () => { + // setup + store.dispatch(disableAutoApply()); + store.dispatch( + updateVisualizationState({ + visualizationId: store.getState().lens.visualization.activeId as string, + newState: { something: 'changed' }, + }) + ); + store.dispatch(setChangesApplied(false)); // simulate workspace panel behavior + harness.update(); + + expect(harness.applyChangesDisabled).toBeFalsy(); + expect(harness.autoApplySwitchOn).toBeFalsy(); + expect(harness.applyChangesExists).toBeTruthy(); + + // enable auto apply + harness.toggleAutoApply(); + + expect(harness.autoApplySwitchOn).toBeTruthy(); + expect(harness.applyChangesExists).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index be230634886105..274992c5f5e6d9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -8,8 +8,12 @@ import './workspace_panel_wrapper.scss'; import React, { useCallback } from 'react'; -import { EuiPageContent, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiPageContent, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiButton } from '@elastic/eui'; import classNames from 'classnames'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; import { ChartSwitch } from './chart_switch'; @@ -20,8 +24,18 @@ import { DatasourceStates, VisualizationState, updateDatasourceState, + useLensSelector, + selectChangesApplied, + applyChanges, + enableAutoApply, + disableAutoApply, + selectAutoApplyEnabled, } from '../../../state_management'; import { WorkspaceTitle } from './title'; +import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../config_panel/dimension_container'; +import { writeToStorage } from '../../../settings_storage'; + +export const AUTO_APPLY_DISABLED_STORAGE_KEY = 'autoApplyDisabled'; export interface WorkspacePanelWrapperProps { children: React.ReactNode | React.ReactNode[]; @@ -46,6 +60,9 @@ export function WorkspacePanelWrapper({ }: WorkspacePanelWrapperProps) { const dispatchLens = useLensDispatch(); + const changesApplied = useLensSelector(selectChangesApplied); + const autoApplyEnabled = useLensSelector(selectAutoApplyEnabled); + const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null; const setVisualizationState = useCallback( (newState: unknown) => { @@ -72,6 +89,18 @@ export function WorkspacePanelWrapper({ }, [dispatchLens] ); + + const toggleAutoApply = useCallback(() => { + trackUiEvent('toggle_autoapply'); + + writeToStorage( + new Storage(localStorage), + AUTO_APPLY_DISABLED_STORAGE_KEY, + String(autoApplyEnabled) + ); + dispatchLens(autoApplyEnabled ? disableAutoApply() : enableAutoApply()); + }, [dispatchLens, autoApplyEnabled]); + const warningMessages: React.ReactNode[] = []; if (activeVisualization?.getWarningMessages) { warningMessages.push( @@ -93,44 +122,93 @@ export function WorkspacePanelWrapper({
- {!isFullscreen ? ( - - + + + {!isFullscreen && ( - + + + + + {activeVisualization && activeVisualization.renderToolbar && ( + + + + )} + - {activeVisualization && activeVisualization.renderToolbar && ( + )} + + - - )} - - - ) : null} + {!autoApplyEnabled && ( + +
+ dispatchLens(applyChanges())} + size="s" + data-test-subj="lensApplyChanges" + > + + +
+
+ )} +
+
+
+
{warningMessages && warningMessages.length ? ( {warningMessages} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 199131564f7c43..d8b5874050b2a8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -52,7 +52,7 @@ export type Props = Omit, 'co changeIndexPattern: ( id: string, state: IndexPatternPrivateState, - setState: StateSetter + setState: StateSetter ) => void; charts: ChartsPluginSetup; core: CoreStart; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 0ac77696d5987c..f40f3b9623ca85 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -138,7 +138,7 @@ export function getIndexPatternDatasource({ const handleChangeIndexPattern = ( id: string, state: IndexPatternPrivateState, - setState: StateSetter + setState: StateSetter ) => { changeIndexPattern({ id, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 9099b68cdaf0e8..d992e36a0c6d80 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -854,7 +854,9 @@ describe('loader', () => { }); expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0](state)).toMatchObject({ + const [fn, options] = setState.mock.calls[0]; + expect(options).toEqual({ applyImmediately: true }); + expect(fn(state)).toMatchObject({ currentIndexPatternId: '1', indexPatterns: { '1': { @@ -1071,7 +1073,8 @@ describe('loader', () => { expect(fetchJson).toHaveBeenCalledTimes(3); expect(setState).toHaveBeenCalledTimes(1); - const [fn] = setState.mock.calls[0]; + const [fn, options] = setState.mock.calls[0]; + expect(options).toEqual({ applyImmediately: true }); const newState = fn({ foo: 'bar', existingFields: {}, @@ -1155,7 +1158,8 @@ describe('loader', () => { await syncExistingFields(args); - const [fn] = setState.mock.calls[0]; + const [fn, options] = setState.mock.calls[0]; + expect(options).toEqual({ applyImmediately: true }); const newState = fn({ foo: 'bar', existingFields: {}, @@ -1204,7 +1208,8 @@ describe('loader', () => { await syncExistingFields(args); - const [fn] = setState.mock.calls[0]; + const [fn, options] = setState.mock.calls[0]; + expect(options).toEqual({ applyImmediately: true }); const newState = fn({ foo: 'bar', existingFields: {}, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 8b3a0556b03202..9495276f15960e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -9,7 +9,11 @@ import { uniq, mapValues, difference } from 'lodash'; import type { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import type { DataView } from 'src/plugins/data_views/public'; import type { HttpSetup, SavedObjectReference } from 'kibana/public'; -import type { InitializationOptions, StateSetter, VisualizeEditorContext } from '../types'; +import type { + DatasourceDataPanelProps, + InitializationOptions, + VisualizeEditorContext, +} from '../types'; import { IndexPattern, IndexPatternRef, @@ -33,7 +37,7 @@ import { readFromStorage, writeToStorage } from '../settings_storage'; import { getFieldByNameFactory } from './pure_helpers'; import { memoizedGetAvailableOperationsByMetadata } from './operations'; -type SetState = StateSetter; +type SetState = DatasourceDataPanelProps['setState']; type IndexPatternsService = Pick; type ErrorHandler = (err: Error) => void; @@ -326,17 +330,20 @@ export async function changeIndexPattern({ } try { - setState((s) => ({ - ...s, - layers: isSingleEmptyLayer(state.layers) - ? mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) - : state.layers, - indexPatterns: { - ...s.indexPatterns, - [id]: indexPatterns[id], - }, - currentIndexPatternId: id, - })); + setState( + (s) => ({ + ...s, + layers: isSingleEmptyLayer(state.layers) + ? mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) + : state.layers, + indexPatterns: { + ...s.indexPatterns, + [id]: indexPatterns[id], + }, + currentIndexPatternId: id, + }), + { applyImmediately: true } + ); setLastUsedIndexPatternId(storage, id); } catch (err) { onError(err); @@ -458,33 +465,39 @@ export async function syncExistingFields({ } } - setState((state) => ({ - ...state, - isFirstExistenceFetch: false, - existenceFetchFailed: false, - existenceFetchTimeout: false, - existingFields: emptinessInfo.reduce( - (acc, info) => { - acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames); - return acc; - }, - { ...state.existingFields } - ), - })); + setState( + (state) => ({ + ...state, + isFirstExistenceFetch: false, + existenceFetchFailed: false, + existenceFetchTimeout: false, + existingFields: emptinessInfo.reduce( + (acc, info) => { + acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames); + return acc; + }, + { ...state.existingFields } + ), + }), + { applyImmediately: true } + ); } catch (e) { // show all fields as available if fetch failed or timed out - setState((state) => ({ - ...state, - existenceFetchFailed: e.res?.status !== 408, - existenceFetchTimeout: e.res?.status === 408, - existingFields: indexPatterns.reduce( - (acc, pattern) => { - acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name)); - return acc; - }, - { ...state.existingFields } - ), - })); + setState( + (state) => ({ + ...state, + existenceFetchFailed: e.res?.status !== 408, + existenceFetchTimeout: e.res?.status === 408, + existingFields: indexPatterns.reduce( + (acc, pattern) => { + acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name)); + return acc; + }, + { ...state.existingFields } + ), + }), + { applyImmediately: true } + ); } } 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/settings_storage.tsx b/x-pack/plugins/lens/public/settings_storage.tsx index fa59bff166c309..ebe812915242ed 100644 --- a/x-pack/plugins/lens/public/settings_storage.tsx +++ b/x-pack/plugins/lens/public/settings_storage.tsx @@ -14,5 +14,5 @@ export const readFromStorage = (storage: IStorageWrapper, key: string) => { return data && data[key]; }; export const writeToStorage = (storage: IStorageWrapper, key: string, value: string) => { - storage.set(STORAGE_KEY, { [key]: value }); + storage.set(STORAGE_KEY, { ...storage.get(STORAGE_KEY), [key]: value }); }; diff --git a/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx b/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx index edecee61d7709c..f54b07905b94cf 100644 --- a/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx +++ b/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx @@ -49,10 +49,13 @@ export const AxisTitleSettings: React.FunctionComponent isAxisTitleVisible, toggleAxisTitleVisibility, }) => { - const { inputValue: title, handleInputChange: onTitleChange } = useDebouncedValue({ - value: axisTitle || '', - onChange: updateTitleState, - }); + const { inputValue: title, handleInputChange: onTitleChange } = useDebouncedValue( + { + value: axisTitle || '', + onChange: updateTitleState, + }, + { allowFalsyValue: true } + ); return ( <> 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/state_management/context_middleware/index.test.ts b/x-pack/plugins/lens/public/state_management/context_middleware/index.test.ts index f115cb59e6121d..d256fcf9b11e53 100644 --- a/x-pack/plugins/lens/public/state_management/context_middleware/index.test.ts +++ b/x-pack/plugins/lens/public/state_management/context_middleware/index.test.ts @@ -10,12 +10,12 @@ import moment from 'moment'; import { contextMiddleware } from '.'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; -import { initialState } from '../lens_slice'; +import { applyChanges, initialState } from '../lens_slice'; import { LensAppState } from '../types'; import { mockDataPlugin, mockStoreDeps } from '../../mocks'; const storeDeps = mockStoreDeps(); -const createMiddleware = (data: DataPublicPluginStart) => { +const createMiddleware = (data: DataPublicPluginStart, state?: Partial) => { const middleware = contextMiddleware({ ...storeDeps, lensServices: { @@ -24,12 +24,13 @@ const createMiddleware = (data: DataPublicPluginStart) => { }, }); const store = { - getState: jest.fn(() => ({ lens: initialState })), + getState: jest.fn(() => ({ lens: state || initialState })), dispatch: jest.fn(), }; const next = jest.fn(); - const invoke = (action: PayloadAction>) => middleware(store)(next)(action); + const invoke = (action: PayloadAction | void>) => + middleware(store)(next)(action); return { store, next, invoke }; }; @@ -70,6 +71,47 @@ describe('contextMiddleware', () => { }); expect(next).toHaveBeenCalledWith(action); }); + describe('when auto-apply is disabled', () => { + it('only updates searchSessionId when user applies changes', () => { + // setup + const data = mockDataPlugin(); + (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000)); + (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ + from: 'now-2m', + to: 'now', + }); + (data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ + min: moment(Date.now() - 100000), + max: moment(Date.now() - 30000), + }); + const { invoke, store } = createMiddleware(data, { + ...initialState, + autoApplyDisabled: true, + }); + + // setState shouldn't trigger + const setStateAction = { + type: 'lens/setState', + payload: { + visualization: { + state: {}, + activeId: 'id2', + }, + }, + }; + invoke(setStateAction); + expect(store.dispatch).not.toHaveBeenCalledWith( + expect.objectContaining({ type: 'lens/setState' }) + ); + + // applyChanges should trigger + const applyChangesAction = applyChanges(); + invoke(applyChangesAction); + expect(store.dispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'lens/setState' }) + ); + }); + }); it('does not update the searchSessionId when the state changes and too little time has passed', () => { const data = mockDataPlugin(); // time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update) diff --git a/x-pack/plugins/lens/public/state_management/context_middleware/index.ts b/x-pack/plugins/lens/public/state_management/context_middleware/index.ts index 25dea5527d0612..3ca806d17dcb78 100644 --- a/x-pack/plugins/lens/public/state_management/context_middleware/index.ts +++ b/x-pack/plugins/lens/public/state_management/context_middleware/index.ts @@ -8,7 +8,14 @@ import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit'; import moment from 'moment'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; -import { setState, LensDispatch, LensStoreDeps, navigateAway } from '..'; +import { + setState, + LensDispatch, + LensStoreDeps, + navigateAway, + applyChanges, + selectAutoApplyEnabled, +} from '..'; import { LensAppState } from '../types'; import { getResolvedDateRange, containsDynamicMath } from '../../utils'; import { subscribeToExternalContext } from './subscribe_to_external_context'; @@ -20,8 +27,12 @@ export const contextMiddleware = (storeDeps: LensStoreDeps) => (store: Middlewar store.getState, store.dispatch ); - return (next: Dispatch) => (action: PayloadAction>) => { - if (!action.payload?.searchSessionId && !onActiveDataChange.match(action)) { + return (next: Dispatch) => (action: PayloadAction) => { + if ( + !(action.payload as Partial)?.searchSessionId && + !onActiveDataChange.match(action) && + (selectAutoApplyEnabled(store.getState()) || applyChanges.match(action)) + ) { updateTimeRange(storeDeps.lensServices.data, store.dispatch); } if (navigateAway.match(action)) { diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts index bdd1bd8f39cc03..7b9c345ff89f63 100644 --- a/x-pack/plugins/lens/public/state_management/index.ts +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -20,6 +20,9 @@ export const { loadInitial, navigateAway, setState, + enableAutoApply, + disableAutoApply, + applyChanges, setSaveable, onActiveDataChange, updateState, diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts index 88a045ed0b506d..164941d5d5f89f 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts @@ -9,11 +9,18 @@ import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit'; import { LensStoreDeps } from '..'; import { loadInitial as loadInitialAction } from '..'; import { loadInitial } from './load_initial'; +import { readFromStorage } from '../../settings_storage'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { AUTO_APPLY_DISABLED_STORAGE_KEY } from '../../editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper'; + +const autoApplyDisabled = () => { + return readFromStorage(new Storage(localStorage), AUTO_APPLY_DISABLED_STORAGE_KEY) === 'true'; +}; export const initMiddleware = (storeDeps: LensStoreDeps) => (store: MiddlewareAPI) => { return (next: Dispatch) => (action: PayloadAction) => { if (loadInitialAction.match(action)) { - return loadInitial(store, storeDeps, action.payload); + return loadInitial(store, storeDeps, action.payload, autoApplyDisabled()); } next(action); }; diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 372d08017ee2a0..709577594ceae9 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -9,7 +9,7 @@ import { MiddlewareAPI } from '@reduxjs/toolkit'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; import { setState, initEmpty, LensStoreDeps } from '..'; -import { getPreloadedState } from '../lens_slice'; +import { disableAutoApply, getPreloadedState } from '../lens_slice'; import { SharingSavedObjectProps } from '../../types'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; import { getInitialDatasourceId } from '../../utils'; @@ -93,7 +93,8 @@ export function loadInitial( redirectCallback: (savedObjectId?: string) => void; initialInput?: LensEmbeddableInput; history?: History; - } + }, + autoApplyDisabled: boolean ) { const { lensServices, datasourceMap, embeddableEditorIncomingState, initialContext } = storeDeps; const { resolvedDateRange, searchSessionId, isLinkedToOriginatingApp, ...emptyState } = @@ -129,6 +130,9 @@ export function loadInitial( initialContext, }) ); + if (autoApplyDisabled) { + store.dispatch(disableAutoApply()); + } }) .catch((e: { message: string }) => { notifications.toasts.addDanger({ @@ -209,6 +213,10 @@ export function loadInitial( isLoading: false, }) ); + + if (autoApplyDisabled) { + store.dispatch(disableAutoApply()); + } }) .catch((e: { message: string }) => notifications.toasts.addDanger({ diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts index 85061f36ce35e7..4a183c11d896bb 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { EnhancedStore } from '@reduxjs/toolkit'; import { Query } from 'src/plugins/data/public'; import { switchDatasource, @@ -16,13 +17,20 @@ import { removeOrClearLayer, addLayer, LensRootStore, + selectTriggerApplyChanges, + selectChangesApplied, } from '.'; import { layerTypes } from '../../common'; import { makeLensStore, defaultState, mockStoreDeps } from '../mocks'; import { DatasourceMap, VisualizationMap } from '../types'; +import { applyChanges, disableAutoApply, enableAutoApply, setChangesApplied } from './lens_slice'; +import { LensAppState } from './types'; describe('lensSlice', () => { - const { store } = makeLensStore({}); + let store: EnhancedStore<{ lens: LensAppState }>; + beforeEach(() => { + store = makeLensStore({}).store; + }); const customQuery = { query: 'custom' } as Query; describe('state update', () => { @@ -34,6 +42,56 @@ describe('lensSlice', () => { expect(changedState).toEqual({ ...defaultState, query: customQuery }); }); + describe('auto-apply-related actions', () => { + it('should disable auto apply', () => { + expect(store.getState().lens.autoApplyDisabled).toBeUndefined(); + expect(store.getState().lens.changesApplied).toBeUndefined(); + + store.dispatch(disableAutoApply()); + + expect(store.getState().lens.autoApplyDisabled).toBe(true); + expect(store.getState().lens.changesApplied).toBe(true); + }); + + it('should enable auto-apply', () => { + store.dispatch(disableAutoApply()); + + expect(store.getState().lens.autoApplyDisabled).toBe(true); + + store.dispatch(enableAutoApply()); + + expect(store.getState().lens.autoApplyDisabled).toBe(false); + }); + + it('applies changes when auto-apply disabled', () => { + store.dispatch(disableAutoApply()); + + store.dispatch(applyChanges()); + + expect(selectTriggerApplyChanges(store.getState())).toBe(true); + }); + + it('does not apply changes if auto-apply enabled', () => { + expect(store.getState().lens.autoApplyDisabled).toBeUndefined(); + + store.dispatch(applyChanges()); + + expect(selectTriggerApplyChanges(store.getState())).toBe(false); + }); + + it('sets changes-applied flag', () => { + expect(store.getState().lens.changesApplied).toBeUndefined(); + + store.dispatch(setChangesApplied(true)); + + expect(selectChangesApplied(store.getState())).toBe(true); + + store.dispatch(setChangesApplied(false)); + + expect(selectChangesApplied(store.getState())).toBe(true); + }); + }); + it('updateState: updates state with updater', () => { const customUpdater = jest.fn((state) => ({ ...state, query: customQuery })); store.dispatch(updateState({ updater: customUpdater })); diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 099929cdf47962..56ff89f506c858 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -83,6 +83,10 @@ export const getPreloadedState = ({ export const setState = createAction>('lens/setState'); export const onActiveDataChange = createAction('lens/onActiveDataChange'); export const setSaveable = createAction('lens/setSaveable'); +export const enableAutoApply = createAction('lens/enableAutoApply'); +export const disableAutoApply = createAction('lens/disableAutoApply'); +export const applyChanges = createAction('lens/applyChanges'); +export const setChangesApplied = createAction('lens/setChangesApplied'); export const updateState = createAction<{ updater: (prevState: LensAppState) => LensAppState; }>('lens/updateState'); @@ -162,6 +166,10 @@ export const lensActions = { setState, onActiveDataChange, setSaveable, + enableAutoApply, + disableAutoApply, + applyChanges, + setChangesApplied, updateState, updateDatasourceState, updateVisualizationState, @@ -202,6 +210,22 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { isSaveable: payload, }; }, + [enableAutoApply.type]: (state) => { + state.autoApplyDisabled = false; + }, + [disableAutoApply.type]: (state) => { + state.autoApplyDisabled = true; + state.changesApplied = true; + }, + [applyChanges.type]: (state) => { + if (typeof state.applyChangesCounter === 'undefined') { + state.applyChangesCounter = 0; + } + state.applyChangesCounter!++; + }, + [setChangesApplied.type]: (state, { payload: applied }) => { + state.changesApplied = applied; + }, [updateState.type]: ( state, { diff --git a/x-pack/plugins/lens/public/state_management/selectors.test.ts b/x-pack/plugins/lens/public/state_management/selectors.test.ts new file mode 100644 index 00000000000000..2313d341b7e03e --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/selectors.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LensAppState, selectTriggerApplyChanges, selectChangesApplied } from '.'; + +describe('lens selectors', () => { + describe('selecting changes applied', () => { + it('should be true when auto-apply disabled and flag is set', () => { + const lensState = { + changesApplied: true, + autoApplyDisabled: true, + } as Partial; + + expect(selectChangesApplied({ lens: lensState as LensAppState })).toBeTruthy(); + }); + + it('should be false when auto-apply disabled and flag is false', () => { + const lensState = { + changesApplied: false, + autoApplyDisabled: true, + } as Partial; + + expect(selectChangesApplied({ lens: lensState as LensAppState })).toBeFalsy(); + }); + + it('should be true when auto-apply enabled no matter what', () => { + const lensState = { + changesApplied: false, + autoApplyDisabled: false, + } as Partial; + + expect(selectChangesApplied({ lens: lensState as LensAppState })).toBeTruthy(); + }); + }); + it('should select apply changes trigger', () => { + selectTriggerApplyChanges({ lens: { applyChangesCounter: 1 } as LensAppState }); // get the counters in sync + + expect( + selectTriggerApplyChanges({ lens: { applyChangesCounter: 2 } as LensAppState }) + ).toBeTruthy(); + expect( + selectTriggerApplyChanges({ lens: { applyChangesCounter: 2 } as LensAppState }) + ).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index 250e9dde31373d..26a0d70d068f5c 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -19,12 +19,22 @@ export const selectFilters = (state: LensState) => state.lens.filters; export const selectResolvedDateRange = (state: LensState) => state.lens.resolvedDateRange; export const selectVisualization = (state: LensState) => state.lens.visualization; export const selectStagedPreview = (state: LensState) => state.lens.stagedPreview; +export const selectAutoApplyEnabled = (state: LensState) => !state.lens.autoApplyDisabled; +export const selectChangesApplied = (state: LensState) => + !state.lens.autoApplyDisabled || Boolean(state.lens.changesApplied); export const selectDatasourceStates = (state: LensState) => state.lens.datasourceStates; export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId; export const selectActiveData = (state: LensState) => state.lens.activeData; export const selectIsFullscreenDatasource = (state: LensState) => Boolean(state.lens.isFullscreenDatasource); +let applyChangesCounter: number | undefined; +export const selectTriggerApplyChanges = (state: LensState) => { + const shouldApply = state.lens.applyChangesCounter !== applyChangesCounter; + applyChangesCounter = state.lens.applyChangesCounter; + return shouldApply; +}; + export const selectExecutionContext = createSelector( [selectQuery, selectFilters, selectResolvedDateRange], (query, filters, dateRange) => ({ diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index b0ff49862d9b83..0c902f944072d0 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -33,6 +33,9 @@ export interface PreviewState { export interface EditorFrameState extends PreviewState { activeDatasourceId: string | null; stagedPreview?: PreviewState; + autoApplyDisabled?: boolean; + applyChangesCounter?: number; + changesApplied?: boolean; isFullscreenDatasource?: boolean; } export interface LensAppState extends EditorFrameState { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 276c31328bb050..7047201c5dba38 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -160,7 +160,12 @@ export interface DatasourceSuggestion { keptLayerIds: string[]; } -export type StateSetter = (newState: T | ((prevState: T) => T)) => void; +type StateSetterArg = T | ((prevState: T) => T); + +export type StateSetter = ( + newState: StateSetterArg, + options?: OptionsShape +) => void; export interface InitializationOptions { isFullEditor?: boolean; @@ -361,7 +366,7 @@ export interface DatasourcePublicAPI { export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; - setState: StateSetter; + setState: StateSetter; showNoDataPopover: () => void; core: Pick; query: Query; @@ -400,13 +405,13 @@ export type ParamEditorCustomProps = Record & { label?: string // The only way a visualization has to restrict the query building export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { // Not a StateSetter because we have this unique use case of determining valid columns - setState: ( - newState: Parameters>[0], - publishToVisualization?: { + setState: StateSetter< + T, + { isDimensionComplete?: boolean; forceRender?: boolean; } - ) => void; + >; core: Pick; dateRange: DateRange; dimensionGroups: VisualizationDimensionGroupConfig[]; @@ -449,7 +454,13 @@ export type DatasourceDimensionDropProps = SharedDimensionProps & { groupId: string; columnId: string; state: T; - setState: StateSetter; + setState: StateSetter< + T, + { + isDimensionComplete?: boolean; + forceRender?: boolean; + } + >; dimensionGroups: VisualizationDimensionGroupConfig[]; }; @@ -651,6 +662,7 @@ export interface VisualizationSuggestion { export interface FramePublicAPI { datasourceLayers: Record; + appliedDatasourceLayers?: Record; // this is only set when auto-apply is turned off /** * Data of the chart currently rendered in the preview. * This data might be not available (e.g. if the chart can't be rendered) or outdated and belonging to another chart. diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx index 20d2bd31c7c648..3766e1f022c882 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx @@ -18,6 +18,7 @@ import { EuiFieldNumber, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; import { XYLayerConfig, AxesSettingsConfig, AxisExtentConfig } from '../../../common/expressions'; import { ToolbarPopover, @@ -241,7 +242,7 @@ export const AxisSettingsPopover: React.FunctionComponent = { type: 'long', _meta: { description: 'Number of times the user opened the in-product formula help popover.' }, }, + toggle_autoapply: { + type: 'long', + _meta: { + description: 'Number of times the user toggled auto-apply.', + }, + }, toggle_fullscreen_formula: { type: 'long', _meta: { 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 = ({ = {}): ExceptionListItemSchema { - return this.generate({ - name: `Blocklist ${this.randomString(5)}`, - list_id: ENDPOINT_BLOCKLISTS_LIST_ID, - item_id: `generator_endpoint_blocklist_${this.seededUUIDv4()}`, - os_types: ['windows'], - entries: [ - this.randomChoice([ + const os = this.randomOSFamily() as ExceptionListItemSchema['os_types'][number]; + const entriesList: CreateExceptionListItemSchema['entries'] = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: os === 'windows' ? 'C:\\Fol*\\file.*' : '/usr/*/*.dmg', + }, + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: os === 'windows' ? 'C:\\Fol*\\file.exe' : '/usr/*/app.dmg', + }, + { + field: 'process.executable.caseless', + value: + os === 'windows' + ? ['C:\\some\\path', 'C:\\some\\other\\path', 'C:\\yet\\another\\path'] + : ['/some/path', 'some/other/path', 'yet/another/path'], + type: 'match_any', + operator: 'included', + }, + { + field: 'process.hash.sha256', + value: [ + 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', + '2C26B46B68FFC68FF99B453C1D30413413422D706483BFA0F98A5E886266E7AE', + 'FCDE2B2EDBA56BF408601FB721FE9B5C338D10EE429EA04FAE5511B68FBF8FB9', + ], + type: 'match_any', + operator: 'included', + }, + { + field: 'process.Ext.code_signature', + entries: [ { - field: 'process.executable.caseless', - value: ['/some/path', 'some/other/path', 'yet/another/path'], - type: 'match_any', + field: 'trusted', + value: 'true', + type: 'match', operator: 'included', }, { - field: 'process.hash.sha256', - value: [ - 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', - '2C26B46B68FFC68FF99B453C1D30413413422D706483BFA0F98A5E886266E7AE', - 'FCDE2B2EDBA56BF408601FB721FE9B5C338D10EE429EA04FAE5511B68FBF8FB9', - ], + field: 'subject_name', + value: + os === 'windows' + ? ['notsus.app', 'verynotsus.app', 'superlegit.app'] + : ['notsus.exe', 'verynotsus.exe', 'superlegit.exe'], type: 'match_any', operator: 'included', }, - { - field: 'process.Ext.code_signature', - entries: [ - { - field: 'trusted', - value: 'true', - type: 'match', - operator: 'included', - }, - { - field: 'subject_name', - value: ['notsus.exe', 'verynotsus.exe', 'superlegit.exe'], - type: 'match_any', - operator: 'included', - }, - ], - type: 'nested', - }, - ]), - ], + ], + type: 'nested', + }, + ]; + + return this.generate({ + name: `Blocklist ${this.randomString(5)}`, + list_id: ENDPOINT_BLOCKLISTS_LIST_ID, + item_id: `generator_endpoint_blocklist_${this.seededUUIDv4()}`, + tags: [this.randomChoice([BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG])], + os_types: [os], + entries: [entriesList[this.randomN(5)]], ...overrides, }); } diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts index 90ab1d098aef50..d08f11a95b1945 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts @@ -50,7 +50,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { CASES_URL } from '../../urls/navigation'; -describe('Cases', () => { +// Flaky: https://github.com/elastic/kibana/issues/69847 +describe.skip('Cases', () => { beforeEach(() => { cleanKibana(); createTimeline(getCase1().timeline).then((response) => diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts index 0314c0c3a66b62..1e1abaa326bd47 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts @@ -13,7 +13,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -describe('Export rules', () => { +// Flaky https://github.com/elastic/kibana/issues/69849 +describe.skip('Export rules', () => { beforeEach(() => { cleanKibana(); cy.intercept( 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..d2578f91720335 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,18 @@ * 2.0. */ -import { - getException, - getExceptionList, - expectedExportedExceptionList, -} from '../../objects/exception'; +import { ROLES } from '../../../common/test'; +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, @@ -38,35 +26,53 @@ import { clearSearchSelection, } from '../../tasks/exceptions_table'; import { + EXCEPTIONS_TABLE_DELETE_BTN, EXCEPTIONS_TABLE_LIST_NAME, EXCEPTIONS_TABLE_SHOWING_LISTS, } 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 +93,99 @@ 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(); + // 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'); + }); +}); + +describe('Exceptions Table - read only', () => { + before(() => { + // First we login as a privileged user to create exception list + cleanKibana(); + loginAndWaitForPageWithoutDateRange(EXCEPTIONS_URL, ROLES.platform_engineer); + createExceptionList(getExceptionList(), getExceptionList().list_id); + + // Then we login as read-only user to test. + loginAndWaitForPageWithoutDateRange(EXCEPTIONS_URL, ROLES.reader); + waitForExceptionsTableToBeLoaded(); + cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); }); + + it('Delete icon is not shown', () => { + cy.get(EXCEPTIONS_TABLE_DELETE_BTN).should('not.exist'); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts index 048efd00d276b3..c28c55e0eb3f7f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts @@ -108,7 +108,6 @@ describe('Events Viewer', () => { it('resets all fields in the events viewer when `Reset Fields` is clicked', () => { const filterInput = 'host.geo.c'; - filterFieldsBrowser(filterInput); cy.get(HOST_GEO_COUNTRY_NAME_HEADER).should('not.exist'); addsHostGeoCountryNameToHeader(); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts index be726f0323d48c..07ea4078ce7c4b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts @@ -32,6 +32,7 @@ import { import { loginAndWaitForPage } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { openTimelineFieldsBrowser, populateTimeline } from '../../tasks/timeline'; +import { ecsFieldMap } from '../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { HOSTS_URL } from '../../urls/navigation'; @@ -109,7 +110,27 @@ describe('Fields Browser', () => { filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should('have.text', '5'); + const fieldsThatMatchFilterInput = Object.keys(ecsFieldMap).filter((fieldName) => { + const dotDelimitedFieldParts = fieldName.split('.'); + const fieldPartMatch = dotDelimitedFieldParts.filter((fieldPart) => { + const camelCasedStringsMatching = fieldPart + .split('_') + .some((part) => part.startsWith(filterInput)); + if (fieldPart.startsWith(filterInput)) { + return true; + } else if (camelCasedStringsMatching) { + return true; + } else { + return false; + } + }); + return fieldName.startsWith(filterInput) || fieldPartMatch.length > 0; + }).length; + + cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should( + 'have.text', + fieldsThatMatchFilterInput + ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts index 617f04697c9513..b3139d94aa6258 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/local_storage.spec.ts @@ -14,7 +14,7 @@ import { waitsForEventsToBeLoaded } from '../../tasks/hosts/events'; import { removeColumn } from '../../tasks/timeline'; // TODO: Fix bug in persisting the columns of timeline -describe('persistent timeline', () => { +describe.skip('persistent timeline', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPage(HOSTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts index 2219339d0577d4..ffb9e8b61c0b9a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts @@ -87,7 +87,9 @@ describe('Row renderers', () => { }); describe('Suricata', () => { - it('Signature tooltips do not overlap', () => { + // This test has become very flaky over time and was blocking a lot of PRs. + // A follw-up ticket to tackle this issue has been created. + it.skip('Signature tooltips do not overlap', () => { // Hover the signature to show the tooltips cy.get(TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE) .parents('.euiPopover__anchor') 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/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 8475ef7247c2c6..ab09aca83f575a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -233,7 +233,7 @@ export const changeRowsPerPageTo = (rowsCount: number) => { cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); cy.get(rowsPerPageSelector(rowsCount)) .pipe(($el) => $el.trigger('click')) - .should('not.be.visible'); + .should('not.exist'); }; export const changeRowsPerPageTo100 = () => { 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/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts index ee8bdb3b023dde..941a19669f2efb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts @@ -34,17 +34,24 @@ export const addsHostGeoContinentNameToTimeline = () => { }; export const clearFieldsBrowser = () => { + cy.clock(); cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}'); + cy.wait(0); + cy.tick(1000); }; export const closeFieldsBrowser = () => { cy.get(CLOSE_BTN).click({ force: true }); + cy.get(FIELDS_BROWSER_FILTER_INPUT).should('not.exist'); }; export const filterFieldsBrowser = (fieldName: string) => { - cy.get(FIELDS_BROWSER_FILTER_INPUT) - .type(fieldName) - .should('not.have.class', 'euiFieldSearch-isLoading'); + cy.clock(); + cy.get(FIELDS_BROWSER_FILTER_INPUT).type(fieldName, { delay: 50 }); + cy.wait(0); + cy.tick(1000); + // the text filter is debounced by 250 ms, wait 1s for changes to be applied + cy.get(FIELDS_BROWSER_FILTER_INPUT).should('not.have.class', 'euiFieldSearch-isLoading'); }; export const removesMessageField = () => { diff --git a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts index 3009ad8cdd0187..c6cad1e5b75a60 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts @@ -5,6 +5,10 @@ * 2.0. */ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY +// Please modify the 'extract_tactics_techniques_mitre.js' script directly and +// run 'yarn extract-mitre-attacks' from the root 'security_solution' plugin directory + import { i18n } from '@kbn/i18n'; import { MitreTacticsOptions, MitreTechniquesOptions, MitreSubtechniquesOptions } from './types'; @@ -268,6 +272,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1595', tactics: ['reconnaissance'], }, + { + name: 'Adversary-in-the-Middle', + id: 'T1557', + reference: 'https://attack.mitre.org/techniques/T1557', + tactics: ['credential-access', 'collection'], + }, { name: 'Application Layer Protocol', id: 'T1071', @@ -334,6 +344,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1176', tactics: ['persistence'], }, + { + name: 'Browser Session Hijacking', + id: 'T1185', + reference: 'https://attack.mitre.org/techniques/T1185', + tactics: ['collection'], + }, { name: 'Brute Force', id: 'T1110', @@ -370,6 +386,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1526', tactics: ['discovery'], }, + { + name: 'Cloud Storage Object Discovery', + id: 'T1619', + reference: 'https://attack.mitre.org/techniques/T1619', + tactics: ['discovery'], + }, { name: 'Command and Scripting Interpreter', id: 'T1059', @@ -760,6 +782,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1061', tactics: ['execution'], }, + { + name: 'Group Policy Discovery', + id: 'T1615', + reference: 'https://attack.mitre.org/techniques/T1615', + tactics: ['discovery'], + }, { name: 'Hardware Additions', id: 'T1200', @@ -850,18 +878,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1570', tactics: ['lateral-movement'], }, - { - name: 'Man in the Browser', - id: 'T1185', - reference: 'https://attack.mitre.org/techniques/T1185', - tactics: ['collection'], - }, - { - name: 'Man-in-the-Middle', - id: 'T1557', - reference: 'https://attack.mitre.org/techniques/T1557', - tactics: ['credential-access', 'collection'], - }, { name: 'Masquerading', id: 'T1036', @@ -1054,6 +1070,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1108', tactics: ['defense-evasion', 'persistence'], }, + { + name: 'Reflective Code Loading', + id: 'T1620', + reference: 'https://attack.mitre.org/techniques/T1620', + tactics: ['defense-evasion'], + }, { name: 'Remote Access Software', id: 'T1219', @@ -1482,6 +1504,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'reconnaissance', value: 'activeScanning', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.adversaryInTheMiddleDescription', + { defaultMessage: 'Adversary-in-the-Middle (T1557)' } + ), + id: 'T1557', + name: 'Adversary-in-the-Middle', + reference: 'https://attack.mitre.org/techniques/T1557', + tactics: 'credential-access,collection', + value: 'adversaryInTheMiddle', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationLayerProtocolDescription', @@ -1603,6 +1636,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'persistence', value: 'browserExtensions', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.browserSessionHijackingDescription', + { defaultMessage: 'Browser Session Hijacking (T1185)' } + ), + id: 'T1185', + name: 'Browser Session Hijacking', + reference: 'https://attack.mitre.org/techniques/T1185', + tactics: 'collection', + value: 'browserSessionHijacking', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bruteForceDescription', @@ -1669,6 +1713,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'cloudServiceDiscovery', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudStorageObjectDiscoveryDescription', + { defaultMessage: 'Cloud Storage Object Discovery (T1619)' } + ), + id: 'T1619', + name: 'Cloud Storage Object Discovery', + reference: 'https://attack.mitre.org/techniques/T1619', + tactics: 'discovery', + value: 'cloudStorageObjectDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.commandAndScriptingInterpreterDescription', @@ -2384,6 +2439,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'execution', value: 'graphicalUserInterface', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.groupPolicyDiscoveryDescription', + { defaultMessage: 'Group Policy Discovery (T1615)' } + ), + id: 'T1615', + name: 'Group Policy Discovery', + reference: 'https://attack.mitre.org/techniques/T1615', + tactics: 'discovery', + value: 'groupPolicyDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hardwareAdditionsDescription', @@ -2549,28 +2615,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'lateral-movement', value: 'lateralToolTransfer', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.manInTheBrowserDescription', - { defaultMessage: 'Man in the Browser (T1185)' } - ), - id: 'T1185', - name: 'Man in the Browser', - reference: 'https://attack.mitre.org/techniques/T1185', - tactics: 'collection', - value: 'manInTheBrowser', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.manInTheMiddleDescription', - { defaultMessage: 'Man-in-the-Middle (T1557)' } - ), - id: 'T1557', - name: 'Man-in-the-Middle', - reference: 'https://attack.mitre.org/techniques/T1557', - tactics: 'credential-access,collection', - value: 'manInTheMiddle', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.masqueradingDescription', @@ -2923,6 +2967,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion,persistence', value: 'redundantAccess', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.reflectiveCodeLoadingDescription', + { defaultMessage: 'Reflective Code Loading (T1620)' } + ), + id: 'T1620', + name: 'Reflective Code Loading', + reference: 'https://attack.mitre.org/techniques/T1620', + tactics: 'defense-evasion', + value: 'reflectiveCodeLoading', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteAccessSoftwareDescription', @@ -3879,6 +3934,13 @@ export const subtechniques = [ tactics: ['credential-access'], techniqueId: 'T1552', }, + { + name: 'Code Repositories', + id: 'T1213.003', + reference: 'https://attack.mitre.org/techniques/T1213/003', + tactics: ['collection'], + techniqueId: 'T1213', + }, { name: 'Code Signing', id: 'T1553.002', @@ -4320,6 +4382,20 @@ export const subtechniques = [ tactics: ['resource-development'], techniqueId: 'T1584', }, + { + name: 'Double File Extension', + id: 'T1036.007', + reference: 'https://attack.mitre.org/techniques/T1036/007', + tactics: ['defense-evasion'], + techniqueId: 'T1036', + }, + { + name: 'Downgrade Attack', + id: 'T1562.010', + reference: 'https://attack.mitre.org/techniques/T1562/010', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, { name: 'Downgrade System Image', id: 'T1601.002', @@ -4404,6 +4480,13 @@ export const subtechniques = [ tactics: ['collection'], techniqueId: 'T1114', }, + { + name: 'Email Hiding Rules', + id: 'T1564.008', + reference: 'https://attack.mitre.org/techniques/T1564/008', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, { name: 'Emond', id: 'T1546.014', @@ -4586,6 +4669,13 @@ export const subtechniques = [ tactics: ['credential-access'], techniqueId: 'T1552', }, + { + name: 'HTML Smuggling', + id: 'T1027.006', + reference: 'https://attack.mitre.org/techniques/T1027/006', + tactics: ['defense-evasion'], + techniqueId: 'T1027', + }, { name: 'Hardware', id: 'T1592.001', @@ -4621,6 +4711,13 @@ export const subtechniques = [ tactics: ['defense-evasion'], techniqueId: 'T1564', }, + { + name: 'IIS Components', + id: 'T1505.004', + reference: 'https://attack.mitre.org/techniques/T1505/004', + tactics: ['persistence'], + techniqueId: 'T1505', + }, { name: 'IP Addresses', id: 'T1590.005', @@ -4880,6 +4977,13 @@ export const subtechniques = [ tactics: ['discovery'], techniqueId: 'T1069', }, + { + name: 'Login Items', + id: 'T1547.015', + reference: 'https://attack.mitre.org/techniques/T1547/015', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, { name: 'Logon Script (Mac)', id: 'T1037.002', @@ -4894,6 +4998,13 @@ export const subtechniques = [ tactics: ['persistence', 'privilege-escalation'], techniqueId: 'T1037', }, + { + name: 'MMC', + id: 'T1218.014', + reference: 'https://attack.mitre.org/techniques/T1218/014', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, { name: 'MSBuild', id: 'T1127.001', @@ -4971,6 +5082,13 @@ export const subtechniques = [ tactics: ['defense-evasion'], techniqueId: 'T1036', }, + { + name: 'Mavinject', + id: 'T1218.013', + reference: 'https://attack.mitre.org/techniques/T1218/013', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, { name: 'Mshta', id: 'T1218.005', @@ -5440,6 +5558,13 @@ export const subtechniques = [ tactics: ['defense-evasion'], techniqueId: 'T1036', }, + { + name: 'Resource Forking', + id: 'T1564.009', + reference: 'https://attack.mitre.org/techniques/T1564/009', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, { name: 'Revert Cloud Instance', id: 'T1578.004', @@ -5538,6 +5663,13 @@ export const subtechniques = [ tactics: ['lateral-movement'], techniqueId: 'T1563', }, + { + name: 'Safe Mode Boot', + id: 'T1562.009', + reference: 'https://attack.mitre.org/techniques/T1562/009', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, { name: 'Scan Databases', id: 'T1596.005', @@ -5818,6 +5950,13 @@ export const subtechniques = [ tactics: ['persistence', 'defense-evasion'], techniqueId: 'T1542', }, + { + name: 'System Language Discovery', + id: 'T1614.001', + reference: 'https://attack.mitre.org/techniques/T1614/001', + tactics: ['discovery'], + techniqueId: 'T1614', + }, { name: 'Systemd Service', id: 'T1543.002', @@ -6676,6 +6815,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1552', value: 'cloudInstanceMetadataApi', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.codeRepositoriesT1213Description', + { defaultMessage: 'Code Repositories (T1213.003)' } + ), + id: 'T1213.003', + name: 'Code Repositories', + reference: 'https://attack.mitre.org/techniques/T1213/003', + tactics: 'collection', + techniqueId: 'T1213', + value: 'codeRepositories', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.codeSigningT1553Description', @@ -7432,6 +7583,30 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1584', value: 'domains', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.doubleFileExtensionT1036Description', + { defaultMessage: 'Double File Extension (T1036.007)' } + ), + id: 'T1036.007', + name: 'Double File Extension', + reference: 'https://attack.mitre.org/techniques/T1036/007', + tactics: 'defense-evasion', + techniqueId: 'T1036', + value: 'doubleFileExtension', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.downgradeAttackT1562Description', + { defaultMessage: 'Downgrade Attack (T1562.010)' } + ), + id: 'T1562.010', + name: 'Downgrade Attack', + reference: 'https://attack.mitre.org/techniques/T1562/010', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'downgradeAttack', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.downgradeSystemImageT1601Description', @@ -7576,6 +7751,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1114', value: 'emailForwardingRule', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.emailHidingRulesT1564Description', + { defaultMessage: 'Email Hiding Rules (T1564.008)' } + ), + id: 'T1564.008', + name: 'Email Hiding Rules', + reference: 'https://attack.mitre.org/techniques/T1564/008', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'emailHidingRules', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.emondT1546Description', @@ -7888,6 +8075,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1552', value: 'groupPolicyPreferences', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.htmlSmugglingT1027Description', + { defaultMessage: 'HTML Smuggling (T1027.006)' } + ), + id: 'T1027.006', + name: 'HTML Smuggling', + reference: 'https://attack.mitre.org/techniques/T1027/006', + tactics: 'defense-evasion', + techniqueId: 'T1027', + value: 'htmlSmuggling', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.hardwareT1592Description', @@ -7948,6 +8147,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1564', value: 'hiddenWindow', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.iisComponentsT1505Description', + { defaultMessage: 'IIS Components (T1505.004)' } + ), + id: 'T1505.004', + name: 'IIS Components', + reference: 'https://attack.mitre.org/techniques/T1505/004', + tactics: 'persistence', + techniqueId: 'T1505', + value: 'iisComponents', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ipAddressesT1590Description', @@ -8392,6 +8603,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1069', value: 'localGroups', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.loginItemsT1547Description', + { defaultMessage: 'Login Items (T1547.015)' } + ), + id: 'T1547.015', + name: 'Login Items', + reference: 'https://attack.mitre.org/techniques/T1547/015', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'loginItems', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.logonScriptMacT1037Description', @@ -8416,6 +8639,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1037', value: 'logonScriptWindows', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.mmcT1218Description', + { defaultMessage: 'MMC (T1218.014)' } + ), + id: 'T1218.014', + name: 'MMC', + reference: 'https://attack.mitre.org/techniques/T1218/014', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'mmc', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.msBuildT1127Description', @@ -8548,6 +8783,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1036', value: 'matchLegitimateNameOrLocation', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.mavinjectT1218Description', + { defaultMessage: 'Mavinject (T1218.013)' } + ), + id: 'T1218.013', + name: 'Mavinject', + reference: 'https://attack.mitre.org/techniques/T1218/013', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'mavinject', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.mshtaT1218Description', @@ -9352,6 +9599,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1036', value: 'renameSystemUtilities', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.resourceForkingT1564Description', + { defaultMessage: 'Resource Forking (T1564.009)' } + ), + id: 'T1564.009', + name: 'Resource Forking', + reference: 'https://attack.mitre.org/techniques/T1564/009', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'resourceForking', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.revertCloudInstanceT1578Description', @@ -9520,6 +9779,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1563', value: 'sshHijacking', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.safeModeBootT1562Description', + { defaultMessage: 'Safe Mode Boot (T1562.009)' } + ), + id: 'T1562.009', + name: 'Safe Mode Boot', + reference: 'https://attack.mitre.org/techniques/T1562/009', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'safeModeBoot', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.scanDatabasesT1596Description', @@ -10000,6 +10271,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1542', value: 'systemFirmware', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.systemLanguageDiscoveryT1614Description', + { defaultMessage: 'System Language Discovery (T1614.001)' } + ), + id: 'T1614.001', + name: 'System Language Discovery', + reference: 'https://attack.mitre.org/techniques/T1614/001', + tactics: 'discovery', + techniqueId: 'T1614', + value: 'systemLanguageDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.systemdServiceT1543Description', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 78feb911ee082f..33dff406734c99 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -27,7 +27,8 @@ export const getAllExceptionListsColumns = ( onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void, onDelete: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void, formatUrl: FormatUrl, - navigateToUrl: (url: string) => Promise + navigateToUrl: (url: string) => Promise, + isKibanaReadOnly: boolean ): AllExceptionListsColumns[] => [ { align: 'left', @@ -155,7 +156,7 @@ export const getAllExceptionListsColumns = ( }, { render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => { - return listId === 'endpoint_list' ? ( + return listId === 'endpoint_list' || isKibanaReadOnly ? ( <> ) : ( ({ - useUserData: jest.fn().mockReturnValue([ - { - loading: false, - canUserCRUD: false, - }, - ]), -})); - describe('ExceptionListsTable', () => { const exceptionList1 = getExceptionListSchemaMock(); const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' }; @@ -86,9 +79,17 @@ describe('ExceptionListsTable', () => { endpoint_list: exceptionList1, }, ]); + + (useUserData as jest.Mock).mockReturnValue([ + { + loading: false, + canUserCRUD: false, + canUserREAD: false, + }, + ]); }); - it('does not render delete option disabled if list is "endpoint_list"', async () => { + it('does not render delete option if list is "endpoint_list"', async () => { const wrapper = mount( @@ -106,4 +107,25 @@ describe('ExceptionListsTable', () => { wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(0).prop('disabled') ).toBeFalsy(); }); + + it('does not render delete option if user is read only', async () => { + (useUserData as jest.Mock).mockReturnValue([ + { + loading: false, + canUserCRUD: false, + canUserREAD: true, + }, + ]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsTableListId"]').at(1).text()).toEqual( + 'not_endpoint_list' + ); + expect(wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 4a7c71a1084a7b..c40b6b95717241 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -60,7 +60,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = { export const ExceptionListsTable = React.memo(() => { const { formatUrl } = useFormatUrl(SecurityPageName.rules); - const [{ loading: userInfoLoading, canUserCRUD }] = useUserData(); + const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData(); const hasPermissions = userHasPermissions(canUserCRUD); const { loading: listsConfigLoading } = useListsConfig(); @@ -193,8 +193,16 @@ export const ExceptionListsTable = React.memo(() => { ); const exceptionsColumns = useMemo((): AllExceptionListsColumns[] => { - return getAllExceptionListsColumns(handleExport, handleDelete, formatUrl, navigateToUrl); - }, [handleExport, handleDelete, formatUrl, navigateToUrl]); + // Defaulting to true to default to the lower privilege first + const isKibanaReadOnly = (canUserREAD && !canUserCRUD) ?? true; + return getAllExceptionListsColumns( + handleExport, + handleDelete, + formatUrl, + navigateToUrl, + isKibanaReadOnly + ); + }, [handleExport, handleDelete, formatUrl, navigateToUrl, canUserREAD, canUserCRUD]); const handleRefresh = useCallback((): void => { if (refreshExceptions != null) { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index f241a3df873274..37882030082384 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -196,7 +196,7 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[] {value} ), - sortable: !!isInMemorySorting, + sortable: true, truncateText: true, width: '85px', }, @@ -204,7 +204,7 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[] field: 'severity', name: i18n.COLUMN_SEVERITY, render: (value: Rule['severity']) => , - sortable: !!isInMemorySorting, + sortable: true, truncateText: true, width: '12%', }, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx index 6b3cc7478079a4..4107c971cc3b2f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx @@ -47,10 +47,13 @@ export interface EventFiltersFlyoutProps { id?: string; data?: Ecs; onCancel(): void; + maskProps?: { + style?: string; + }; } export const EventFiltersFlyout: React.FC = memo( - ({ onCancel, id, type = 'create', data }) => { + ({ onCancel, id, type = 'create', data, ...flyoutProps }) => { useEventFiltersNotification(); const [enrichedData, setEnrichedData] = useState(); const toasts = useToasts(); @@ -210,7 +213,12 @@ export const EventFiltersFlyout: React.FC = memo( ); return ( - +

diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 7324213975a742..5108fa86b0f56e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -58,187 +58,264 @@ exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should tabType="query" timelineId="test" > - - - -
- -
- - -
+
+ + - - - -
- -
- - - - -
- - +
+
+
+ + + + +
+ + - - - - + className="euiLoadingContent__singleLine" + key="0" + > + + - - + className="euiLoadingContent__singleLine" + key="1" + > + + - - + className="euiLoadingContent__singleLine" + key="2" + > + + - - + className="euiLoadingContent__singleLine" + key="3" + > + + - - + className="euiLoadingContent__singleLine" + key="4" + > + + - - + className="euiLoadingContent__singleLine" + key="5" + > + + - - + className="euiLoadingContent__singleLine" + key="6" + > + + - - + className="euiLoadingContent__singleLine" + key="7" + > + + - - + className="euiLoadingContent__singleLine" + key="8" + > + + + className="euiLoadingContent__singleLine" + key="9" + > + + - - - + + + + + +
+ +
+ +
+ +
+ +
+ + + + `; @@ -508,9 +585,12 @@ Array [ } } > - +
- +
- + {detailsEcsData && ( @@ -145,7 +145,11 @@ export const EventDetailsFooterComponent = React.memo( /> )} {isAddEventFilterModalOpen && detailsEcsData != null && ( - + )} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx new file mode 100644 index 00000000000000..dc8cf627b4656b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.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 from 'react'; +import { render } from '@testing-library/react'; +import { EventDetailsPanel } from './'; +import '../../../../common/mock/match_media'; +import { TestProviders } from '../../../../common/mock'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +import { Ecs } from '../../../../../common/ecs'; +import { mockAlertDetailsData } from '../../../../common/components/event_details/__mocks__'; +import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; +import { + KibanaServices, + useKibana, + useGetUserCasesPermissions, +} from '../../../../common/lib/kibana'; +import { + mockBrowserFields, + mockDocValueFields, + mockRuntimeMappings, +} from '../../../../common/containers/source/mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { mockCasesContext } from '../../../../../../cases/public/mocks/mock_cases_context'; + +const ecsData: Ecs = { + _id: '1', + agent: { type: ['blah'] }, + kibana: { + alert: { + workflow_status: ['open'], + rule: { + parameters: {}, + uuid: ['testId'], + }, + }, + }, +}; + +const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => { + return { + ...detail, + isObjectArray: false, + }; +}) as TimelineEventsDetailsItem[]; + +jest.mock('../../../../../common/endpoint/service/host_isolation/utils', () => { + return { + isIsolationSupported: jest.fn().mockReturnValue(true), + }; +}); + +jest.mock( + '../../../../detections/containers/detection_engine/alerts/use_host_isolation_status', + () => { + return { + useHostIsolationStatus: jest.fn().mockReturnValue({ + loading: false, + isIsolated: false, + agentStatus: 'healthy', + }), + }; + } +); + +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), +})); + +jest.mock('../../../../detections/components/user_info', () => ({ + useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), +})); +jest.mock('../../../../common/lib/kibana'); +jest.mock( + '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges', + () => ({ + useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), + }) +); +jest.mock('../../../../cases/components/use_insert_timeline'); + +jest.mock('../../../../common/utils/endpoint_alert_check', () => { + return { + isAlertFromEndpointAlert: jest.fn().mockReturnValue(true), + isAlertFromEndpointEvent: jest.fn().mockReturnValue(true), + }; +}); +jest.mock( + '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline', + () => { + return { + useInvestigateInTimeline: jest.fn().mockReturnValue({ + investigateInTimelineActionItems: [
], + investigateInTimelineAlertClick: () => {}, + }), + }; + } +); +jest.mock('../../../../detections/components/alerts_table/actions'); +const mockSearchStrategy = jest.fn(); + +const defaultProps = { + timelineId: TimelineId.test, + loadingEventDetails: false, + detailsEcsData: ecsData, + isHostIsolationPanelOpen: false, + handleOnEventClosed: jest.fn(), + onAddIsolationStatusClick: jest.fn(), + expandedEvent: { eventId: ecsData._id, indexName: '' }, + detailsData: mockAlertDetailsDataWithIsObject, + tabType: TimelineTabs.query, + browserFields: mockBrowserFields, + docValueFields: mockDocValueFields, + runtimeMappings: mockRuntimeMappings, +}; + +describe('event details footer component', () => { + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + search: mockSearchStrategy.mockReturnValue({ + unsubscribe: jest.fn(), + subscribe: jest.fn(), + }), + }, + query: jest.fn(), + }, + uiSettings: { + get: jest.fn().mockReturnValue([]), + }, + cases: { + getCasesContext: () => mockCasesContext, + }, + }, + }); + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: true, + read: true, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + test('it renders the take action dropdown in the timeline version', () => { + const wrapper = render( + + + + ); + expect(wrapper.getByTestId('side-panel-flyout-footer')).toBeTruthy(); + }); + test('it renders the take action dropdown in the flyout version', () => { + const wrapper = render( + + + + ); + expect(wrapper.getByTestId('side-panel-flyout-footer')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 057f18899e8fd1..112b3aaab8687f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -19,6 +19,8 @@ import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; +import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; +import { APP_ID } from '../../../../../common/constants'; import { ExpandableEvent, ExpandableEventTitle } from './expandable_event'; import { useTimelineEventsDetails } from '../../../containers/details'; import { TimelineTabs } from '../../../../../common/types/timeline'; @@ -94,6 +96,13 @@ const EventDetailsPanelComponent: React.FC = ({ 'isolateHost' ); + const { + services: { cases }, + } = useKibana(); + + const CasesContext = cases.getCasesContext(); + const casesPermissions = useGetUserCasesPermissions(); + const [isIsolateActionSuccessBannerVisible, setIsIsolateActionSuccessBannerVisible] = useState(false); @@ -239,7 +248,7 @@ const EventDetailsPanelComponent: React.FC = ({ /> ) : ( - <> + = ({ hostRisk={hostRisk} handleOnEventClosed={handleOnEventClosed} /> - + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx index 48aa0853be49ea..2b75f57b43d20f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx @@ -25,6 +25,8 @@ import { } from '../../../../common/types/timeline'; import { FlowTarget } from '../../../../common/search_strategy/security_solution/network'; import { EventDetailsPanel } from './event_details'; +import { useKibana } from '../../../common/lib/kibana'; +import { mockCasesContext } from '../../../../../cases/public/mocks/mock_cases_context'; jest.mock('../../../common/lib/kibana'); @@ -99,9 +101,34 @@ describe('Details Panel Component', () => { timelineId: 'test', }; + const mockSearchStrategy = jest.fn(); + describe('DetailsPanel: rendering', () => { beforeEach(() => { store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + search: mockSearchStrategy.mockReturnValue({ + unsubscribe: jest.fn(), + subscribe: jest.fn(), + }), + }, + query: jest.fn(), + }, + uiSettings: { + get: jest.fn().mockReturnValue([]), + }, + application: { + navigateToApp: jest.fn(), + }, + cases: { + getCasesContext: () => mockCasesContext, + }, + }, + }); }); test('it should not render the DetailsPanel if no expanded detail has been set in the reducer', () => { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/blocklists/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/blocklists/index.ts index 81a18bb89c3547..3ed2ea6e7f1b3d 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/blocklists/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/blocklists/index.ts @@ -80,7 +80,7 @@ const createBlocklists: RunFn = async ({ flags, log }) => { const body = eventGenerator.generateBlocklistForCreate(); if (isArtifactByPolicy(body)) { - const nmExceptions = Math.floor(Math.random() * 3) || 1; + const nmExceptions = eventGenerator.randomN(3) || 1; body.tags = Array.from({ length: nmExceptions }, () => { return `policy:${randomPolicyId()}`; }); diff --git a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js index 89835e27d3fa75..b0b963872585a6 100644 --- a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js @@ -167,6 +167,10 @@ async function main() { * 2.0. */ + // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY + // Please modify the 'extract_tactics_techniques_mitre.js' script directly and + // run 'yarn extract-mitre-attacks' from the root 'security_solution' plugin directory + import { i18n } from '@kbn/i18n'; import { MitreTacticsOptions, MitreTechniquesOptions, MitreSubtechniquesOptions } from './types'; @@ -197,13 +201,13 @@ async function main() { * * Is built alongside and sampled from the data in the file so to always be valid with the most up to date MITRE ATT&CK data */ - export const mockThreatData = ${JSON.stringify( + export const getMockThreatData = () => (${JSON.stringify( buildMockThreatData(tactics, techniques, subtechniques), null, 2 ) .replace(/}"/g, '}') - .replace(/"{/g, '{')}; + .replace(/"{/g, '{')}); `; fs.writeFileSync(`${OUTPUT_DIRECTORY}/mitre_tactics_techniques.ts`, body, 'utf-8'); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 60f91330d4558c..20da1e212f40f0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -28,6 +28,9 @@ export const ArtifactConstants = { SUPPORTED_HOST_ISOLATION_EXCEPTIONS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], GLOBAL_HOST_ISOLATION_EXCEPTIONS_NAME: 'endpoint-hostisolationexceptionlist', + + SUPPORTED_BLOCKLISTS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], + GLOBAL_BLOCKLISTS_NAME: 'endpoint-blocklist', }; export const ManifestConstants = { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index 16cbe618c5076a..83dbcf1ca6f6de 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -10,15 +10,13 @@ import { listMock } from '../../../../../lists/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import type { EntriesArray, EntryList } from '@kbn/securitysolution-io-ts-list-types'; -import { - buildArtifact, - getEndpointExceptionList, - getEndpointTrustedAppsList, - getFilteredEndpointExceptionList, -} from './lists'; +import { buildArtifact, getEndpointExceptionList, getFilteredEndpointExceptionList } from './lists'; import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; import { ArtifactConstants } from './common'; import { + ENDPOINT_BLOCKLISTS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, } from '@kbn/securitysolution-list-constants'; @@ -61,12 +59,12 @@ describe('artifacts lists', () => { const first = getFoundExceptionListItemSchemaMock(); mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -107,12 +105,12 @@ describe('artifacts lists', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -158,12 +156,12 @@ describe('artifacts lists', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -211,12 +209,12 @@ describe('artifacts lists', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -263,12 +261,12 @@ describe('artifacts lists', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -306,12 +304,12 @@ describe('artifacts lists', () => { first.data[1].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -349,12 +347,12 @@ describe('artifacts lists', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -378,12 +376,12 @@ describe('artifacts lists', () => { .mockReturnValueOnce(first) .mockReturnValueOnce(second); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); // Expect 2 exceptions, the first two calls returned the same exception list items expect(resp.entries.length).toEqual(2); @@ -394,12 +392,12 @@ describe('artifacts lists', () => { exceptionsResponse.data = []; exceptionsResponse.total = 0; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse); - const resp = await getFilteredEndpointExceptionList( - mockExceptionClient, - 'v1', - TEST_FILTER, - ENDPOINT_LIST_ID - ); + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: TEST_FILTER, + listId: ENDPOINT_LIST_ID, + }); expect(resp.entries.length).toEqual(0); }); @@ -543,13 +541,17 @@ describe('artifacts lists', () => { ], }; - describe('getEndpointExceptionList', () => { - test('it should build proper kuery', async () => { + describe('Builds proper kuery without policy', () => { + test('for Endpoint List', async () => { mockExceptionClient.findExceptionListItem = jest .fn() .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); - const resp = await getEndpointExceptionList(mockExceptionClient, 'v1', 'windows'); + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'windows', + }); expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); @@ -563,15 +565,18 @@ describe('artifacts lists', () => { sortOrder: 'desc', }); }); - }); - describe('getEndpointTrustedAppsList', () => { - test('it should build proper kuery without policy', async () => { + test('for Trusted Apps', async () => { mockExceptionClient.findExceptionListItem = jest .fn() .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); - const resp = await getEndpointTrustedAppsList(mockExceptionClient, 'v1', 'macos'); + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'macos', + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); @@ -587,17 +592,98 @@ describe('artifacts lists', () => { }); }); - test('it should build proper kuery with policy', async () => { + test('for Event Filters', async () => { mockExceptionClient.findExceptionListItem = jest .fn() .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); - const resp = await getEndpointTrustedAppsList( - mockExceptionClient, - 'v1', - 'macos', - 'c6d16e42-c32d-4dce-8a88-113cfe276ad1' - ); + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'macos', + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + }); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + namespaceType: 'agnostic', + filter: + 'exception-list-agnostic.attributes.os_types:"macos" and (exception-list-agnostic.attributes.tags:"policy:all")', + perPage: 100, + page: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + }); + + test('for Host Isolation Exceptions', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'macos', + listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + }); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + namespaceType: 'agnostic', + filter: + 'exception-list-agnostic.attributes.os_types:"macos" and (exception-list-agnostic.attributes.tags:"policy:all")', + perPage: 100, + page: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + }); + + test('for Blocklists', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'macos', + listId: ENDPOINT_BLOCKLISTS_LIST_ID, + }); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_BLOCKLISTS_LIST_ID, + namespaceType: 'agnostic', + filter: + 'exception-list-agnostic.attributes.os_types:"macos" and (exception-list-agnostic.attributes.tags:"policy:all")', + perPage: 100, + page: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + }); + }); + + describe('Build proper kuery with policy', () => { + test('for Trusted Apps', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'macos', + policyId: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); @@ -614,5 +700,91 @@ describe('artifacts lists', () => { sortOrder: 'desc', }); }); + + test('for Event Filters', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'macos', + policyId: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + }); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + namespaceType: 'agnostic', + filter: + 'exception-list-agnostic.attributes.os_types:"macos" and ' + + '(exception-list-agnostic.attributes.tags:"policy:all" or ' + + 'exception-list-agnostic.attributes.tags:"policy:c6d16e42-c32d-4dce-8a88-113cfe276ad1")', + perPage: 100, + page: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + }); + + test('for Host Isolation Exceptions', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'macos', + policyId: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', + listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + }); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + namespaceType: 'agnostic', + filter: + 'exception-list-agnostic.attributes.os_types:"macos" and ' + + '(exception-list-agnostic.attributes.tags:"policy:all" or ' + + 'exception-list-agnostic.attributes.tags:"policy:c6d16e42-c32d-4dce-8a88-113cfe276ad1")', + perPage: 100, + page: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + }); + test('for Blocklists', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + os: 'macos', + policyId: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', + listId: ENDPOINT_BLOCKLISTS_LIST_ID, + }); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_BLOCKLISTS_LIST_ID, + namespaceType: 'agnostic', + filter: + 'exception-list-agnostic.attributes.os_types:"macos" and ' + + '(exception-list-agnostic.attributes.tags:"policy:all" or ' + + 'exception-list-agnostic.attributes.tags:"policy:c6d16e42-c32d-4dce-8a88-113cfe276ad1")', + perPage: 100, + page: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + }); }); }); 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 b23c2fe08bf103..7a36e2ef940e5f 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 @@ -16,6 +16,7 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { hasSimpleExecutableName, OperatingSystem } from '@kbn/securitysolution-utils'; import { + ENDPOINT_BLOCKLISTS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, ENDPOINT_LIST_ID, @@ -63,22 +64,30 @@ export async function buildArtifact( }; } -export async function getFilteredEndpointExceptionList( - eClient: ExceptionListClient, - schemaVersion: string, - filter: string, - listId: - | typeof ENDPOINT_LIST_ID - | typeof ENDPOINT_TRUSTED_APPS_LIST_ID - | typeof ENDPOINT_EVENT_FILTERS_LIST_ID - | typeof ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID -): Promise { +export type ArtifactListId = + | typeof ENDPOINT_LIST_ID + | typeof ENDPOINT_TRUSTED_APPS_LIST_ID + | typeof ENDPOINT_EVENT_FILTERS_LIST_ID + | typeof ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID + | typeof ENDPOINT_BLOCKLISTS_LIST_ID; + +export async function getFilteredEndpointExceptionList({ + elClient, + filter, + listId, + schemaVersion, +}: { + elClient: ExceptionListClient; + filter: string; + listId: ArtifactListId; + schemaVersion: string; +}): Promise { const exceptions: WrappedTranslatedExceptionList = { entries: [] }; let page = 1; let paging = true; while (paging) { - const response = await eClient.findExceptionListItem({ + const response = await elClient.findExceptionListItem({ listId, namespaceType: 'agnostic', filter, @@ -107,72 +116,42 @@ export async function getFilteredEndpointExceptionList( return validated as WrappedTranslatedExceptionList; } -export async function getEndpointExceptionList( - eClient: ExceptionListClient, - schemaVersion: string, - os: string -): Promise { - const filter = `exception-list-agnostic.attributes.os_types:\"${os}\"`; - - return getFilteredEndpointExceptionList(eClient, schemaVersion, filter, ENDPOINT_LIST_ID); -} - -export async function getEndpointTrustedAppsList( - eClient: ExceptionListClient, - schemaVersion: string, - os: string, - policyId?: string -): Promise { - const osFilter = `exception-list-agnostic.attributes.os_types:\"${os}\"`; - const policyFilter = `(exception-list-agnostic.attributes.tags:\"policy:all\"${ - policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : '' - })`; - - return getFilteredEndpointExceptionList( - eClient, - schemaVersion, - `${osFilter} and ${policyFilter}`, - ENDPOINT_TRUSTED_APPS_LIST_ID - ); -} - -export async function getEndpointEventFiltersList( - eClient: ExceptionListClient, - schemaVersion: string, - os: string, - policyId?: string -): Promise { +export async function getEndpointExceptionList({ + elClient, + listId, + os, + policyId, + schemaVersion, +}: { + elClient: ExceptionListClient; + listId?: ArtifactListId; + os: string; + policyId?: string; + schemaVersion: string; +}): Promise { const osFilter = `exception-list-agnostic.attributes.os_types:\"${os}\"`; const policyFilter = `(exception-list-agnostic.attributes.tags:\"policy:all\"${ policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : '' })`; - return getFilteredEndpointExceptionList( - eClient, + // for endpoint list + if (!listId || listId === ENDPOINT_LIST_ID) { + return getFilteredEndpointExceptionList({ + elClient, + schemaVersion, + filter: `${osFilter}`, + listId: ENDPOINT_LIST_ID, + }); + } + // for TAs, EFs, Host IEs and Blocklists + return getFilteredEndpointExceptionList({ + elClient, schemaVersion, - `${osFilter} and ${policyFilter}`, - ENDPOINT_EVENT_FILTERS_LIST_ID - ); + filter: `${osFilter} and ${policyFilter}`, + listId, + }); } -export async function getHostIsolationExceptionsList( - eClient: ExceptionListClient, - schemaVersion: string, - os: string, - policyId?: string -): Promise { - const osFilter = `exception-list-agnostic.attributes.os_types:\"${os}\"`; - const policyFilter = `(exception-list-agnostic.attributes.tags:\"policy:all\"${ - policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : '' - })`; - - return getFilteredEndpointExceptionList( - eClient, - schemaVersion, - `${osFilter} and ${policyFilter}`, - ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID - ); -} /** * Translates Exception list items to Exceptions the endpoint can understand * @param exceptions 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 c878c02df2a081..717eadc1363315 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 @@ -10,6 +10,8 @@ import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_BLOCKLISTS_LIST_ID, } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { PackagePolicy } from '../../../../../../fleet/common/types/models'; @@ -73,6 +75,9 @@ describe('ManifestManager', () => { 'endpoint-hostisolationexceptionlist-windows-v1'; const ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_LINUX = 'endpoint-hostisolationexceptionlist-linux-v1'; + const ARTIFACT_NAME_BLOCKLISTS_MACOS = 'endpoint-blocklist-macos-v1'; + const ARTIFACT_NAME_BLOCKLISTS_WINDOWS = 'endpoint-blocklist-windows-v1'; + const ARTIFACT_NAME_BLOCKLISTS_LINUX = 'endpoint-blocklist-linux-v1'; let ARTIFACTS: InternalArtifactCompleteSchema[] = []; let ARTIFACTS_BY_ID: { [K: string]: InternalArtifactCompleteSchema } = {}; @@ -284,6 +289,9 @@ describe('ManifestManager', () => { ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_MACOS, ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_WINDOWS, ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_LINUX, + ARTIFACT_NAME_BLOCKLISTS_MACOS, + ARTIFACT_NAME_BLOCKLISTS_WINDOWS, + ARTIFACT_NAME_BLOCKLISTS_LINUX, ]; const getArtifactIds = (artifacts: InternalArtifactSchema[]) => [ @@ -327,7 +335,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(12); + expect(artifacts.length).toBe(15); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); for (const artifact of artifacts) { @@ -342,14 +350,18 @@ describe('ManifestManager', () => { test('Builds fully new manifest if no baseline parameter passed and present exception list items', async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); + const eventFiltersListItem = getExceptionListItemSchemaMock({ os_types: ['windows'] }); const hostIsolationExceptionsItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); + const blocklistsListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const context = buildManifestManagerContextMock({}); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, [ENDPOINT_TRUSTED_APPS_LIST_ID]: { linux: [trustedAppListItem] }, + [ENDPOINT_EVENT_FILTERS_LIST_ID]: { linux: [eventFiltersListItem] }, [ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID]: { linux: [hostIsolationExceptionsItem] }, + [ENDPOINT_BLOCKLISTS_LIST_ID]: { linux: [blocklistsListItem] }, }); context.savedObjectsClient.create = jest .fn() @@ -366,7 +378,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(12); + expect(artifacts.length).toBe(15); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(getArtifactObject(artifacts[0])).toStrictEqual({ @@ -381,12 +393,19 @@ describe('ManifestManager', () => { }); expect(getArtifactObject(artifacts[6])).toStrictEqual({ entries: [] }); expect(getArtifactObject(artifacts[7])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[8])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[8])).toStrictEqual({ + entries: translateToEndpointExceptions([eventFiltersListItem], 'v1'), + }); expect(getArtifactObject(artifacts[9])).toStrictEqual({ entries: [] }); expect(getArtifactObject(artifacts[10])).toStrictEqual({ entries: [] }); expect(getArtifactObject(artifacts[11])).toStrictEqual({ entries: translateToEndpointExceptions([hostIsolationExceptionsItem], 'v1'), }); + expect(getArtifactObject(artifacts[12])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[13])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[14])).toStrictEqual({ + entries: translateToEndpointExceptions([blocklistsListItem], 'v1'), + }); for (const artifact of artifacts) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); @@ -399,6 +418,9 @@ describe('ManifestManager', () => { test('Reuses artifacts when baseline parameter passed and present exception list items', async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); + const eventFiltersListItem = getExceptionListItemSchemaMock({ os_types: ['windows'] }); + const hostIsolationExceptionsItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); + const blocklistsListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const context = buildManifestManagerContextMock({}); const manifestManager = new ManifestManager(context); @@ -416,6 +438,9 @@ describe('ManifestManager', () => { context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, [ENDPOINT_TRUSTED_APPS_LIST_ID]: { linux: [trustedAppListItem] }, + [ENDPOINT_EVENT_FILTERS_LIST_ID]: { linux: [eventFiltersListItem] }, + [ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID]: { linux: [hostIsolationExceptionsItem] }, + [ENDPOINT_BLOCKLISTS_LIST_ID]: { linux: [blocklistsListItem] }, }); const manifest = await manifestManager.buildNewManifest(oldManifest); @@ -426,7 +451,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(12); + expect(artifacts.length).toBe(15); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts[0]).toStrictEqual(oldManifest.getAllArtifacts()[0]); @@ -439,7 +464,19 @@ describe('ManifestManager', () => { }); expect(getArtifactObject(artifacts[6])).toStrictEqual({ entries: [] }); expect(getArtifactObject(artifacts[7])).toStrictEqual({ entries: [] }); - expect(getArtifactObject(artifacts[8])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[8])).toStrictEqual({ + entries: translateToEndpointExceptions([eventFiltersListItem], 'v1'), + }); + expect(getArtifactObject(artifacts[9])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[10])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[11])).toStrictEqual({ + entries: translateToEndpointExceptions([hostIsolationExceptionsItem], 'v1'), + }); + expect(getArtifactObject(artifacts[12])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[13])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[14])).toStrictEqual({ + entries: translateToEndpointExceptions([blocklistsListItem], 'v1'), + }); for (const artifact of artifacts) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); @@ -488,7 +525,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(13); + expect(artifacts.length).toBe(16); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(getArtifactObject(artifacts[0])).toStrictEqual({ diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index af985bf2301730..7be2a36396a715 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -10,6 +10,12 @@ import semver from 'semver'; import LRU from 'lru-cache'; import { isEqual, isEmpty } from 'lodash'; import { Logger, SavedObjectsClientContract } from 'src/core/server'; +import { + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, + ENDPOINT_BLOCKLISTS_LIST_ID, + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, +} from '@kbn/securitysolution-list-constants'; import { ListResult } from '../../../../../../fleet/common'; import { PackagePolicyServiceInterface } from '../../../../../../fleet/server'; import { ExceptionListClient } from '../../../../../../lists/server'; @@ -23,10 +29,7 @@ import { ArtifactConstants, buildArtifact, getArtifactId, - getEndpointEventFiltersList, getEndpointExceptionList, - getEndpointTrustedAppsList, - getHostIsolationExceptionsList, Manifest, } from '../../../lib/artifacts'; import { @@ -133,7 +136,11 @@ export class ManifestManager { */ protected async buildExceptionListArtifact(os: string): Promise { return buildArtifact( - await getEndpointExceptionList(this.exceptionListClient, this.schemaVersion, os), + await getEndpointExceptionList({ + elClient: this.exceptionListClient, + schemaVersion: this.schemaVersion, + os, + }), this.schemaVersion, os, ArtifactConstants.GLOBAL_ALLOWLIST_NAME @@ -171,7 +178,13 @@ export class ManifestManager { */ protected async buildTrustedAppsArtifact(os: string, policyId?: string) { return buildArtifact( - await getEndpointTrustedAppsList(this.exceptionListClient, this.schemaVersion, os, policyId), + await getEndpointExceptionList({ + elClient: this.exceptionListClient, + schemaVersion: this.schemaVersion, + os, + policyId, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }), this.schemaVersion, os, ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME @@ -231,13 +244,66 @@ export class ManifestManager { protected async buildEventFiltersForOs(os: string, policyId?: string) { return buildArtifact( - await getEndpointEventFiltersList(this.exceptionListClient, this.schemaVersion, os, policyId), + await getEndpointExceptionList({ + elClient: this.exceptionListClient, + schemaVersion: this.schemaVersion, + os, + policyId, + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + }), this.schemaVersion, os, ArtifactConstants.GLOBAL_EVENT_FILTERS_NAME ); } + /** + * Builds an array of Blocklist entries (one per supported OS) based on the current state of the + * Blocklist list + * @protected + */ + protected async buildBlocklistArtifacts(): Promise { + const defaultArtifacts: InternalArtifactCompleteSchema[] = []; + const policySpecificArtifacts: Record = {}; + + for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { + defaultArtifacts.push(await this.buildBlocklistForOs(os)); + } + + await iterateAllListItems( + (page) => this.listEndpointPolicyIds(page), + async (policyId) => { + for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { + policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; + policySpecificArtifacts[policyId].push(await this.buildBlocklistForOs(os, policyId)); + } + } + ); + + return { defaultArtifacts, policySpecificArtifacts }; + } + + protected async buildBlocklistForOs(os: string, policyId?: string) { + return buildArtifact( + await getEndpointExceptionList({ + elClient: this.exceptionListClient, + schemaVersion: this.schemaVersion, + os, + policyId, + listId: ENDPOINT_BLOCKLISTS_LIST_ID, + }), + this.schemaVersion, + os, + ArtifactConstants.GLOBAL_BLOCKLISTS_NAME + ); + } + + /** + * Builds an array of endpoint host isolation exception (one per supported OS) based on the current state of the + * Host Isolation Exception List + * @returns + */ + protected async buildHostIsolationExceptionsArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; @@ -266,12 +332,13 @@ export class ManifestManager { policyId?: string ): Promise { return buildArtifact( - await getHostIsolationExceptionsList( - this.exceptionListClient, - this.schemaVersion, + await getEndpointExceptionList({ + elClient: this.exceptionListClient, + schemaVersion: this.schemaVersion, os, - policyId - ), + policyId, + listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + }), this.schemaVersion, os, ArtifactConstants.GLOBAL_HOST_ISOLATION_EXCEPTIONS_NAME @@ -413,7 +480,7 @@ export class ManifestManager { * Builds a new manifest based on the current user exception list. * * @param baselineManifest A baseline manifest to use for initializing pre-existing artifacts. - * @returns {Promise} A new Manifest object reprenting the current exception list. + * @returns {Promise} A new Manifest object representing the current exception list. */ public async buildNewManifest( baselineManifest: Manifest = ManifestManager.createDefaultManifest(this.schemaVersion) @@ -423,6 +490,7 @@ export class ManifestManager { this.buildTrustedAppsArtifacts(), this.buildEventFiltersArtifacts(), this.buildHostIsolationExceptionsArtifacts(), + this.buildBlocklistArtifacts(), ]); const manifest = new Manifest({ 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/session_view/.eslintrc.json b/x-pack/plugins/session_view/.eslintrc.json new file mode 100644 index 00000000000000..2aab6c2d9093b6 --- /dev/null +++ b/x-pack/plugins/session_view/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/consistent-type-definitions": 0 + } +} diff --git a/x-pack/plugins/session_view/README.md b/x-pack/plugins/session_view/README.md new file mode 100644 index 00000000000000..384be8bcc292b5 --- /dev/null +++ b/x-pack/plugins/session_view/README.md @@ -0,0 +1,36 @@ +# Session View + +Session View is meant to provide a visualization into what is going on in a particular Linux environment where the agent is running. It looks likes a terminal emulator; however, it is a tool for introspecting process activity and understanding user and service behaviour in your Linux servers and infrastructure. It is a time-ordered series of process executions displayed in a tree over time. + +It provides an audit trail of: + +- Interactive processes being entered by a user into the terminal - User Input +- Processes and services which do not have a controlling tty (ie are not interactive) +- Output which is generated as a result of process activity - Output +- Nested sessions inside the entry session - Nested session (Note: For now nested sessions will display as they did at Cmd with no special handling for TMUX) +- Full telemetry about the process initiated event. This will include the information specified in the Linux logical event model +- Who executed the session or process, even if the user changes. + +## Development + +## Tests + +### Unit tests + +From kibana path in your terminal go to this plugin root: + +```bash +cd x-pack/plugins/session_view +``` + +Then run jest with: + +```bash +yarn test:jest +``` + +Or if running from kibana root, you can specify the `-i` to specify the path: + +```bash +yarn test:jest -i x-pack/plugins/session_view/ +``` diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts new file mode 100644 index 00000000000000..5baf690dc44a53 --- /dev/null +++ b/x-pack/plugins/session_view/common/constants.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. + */ + +export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route'; +export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route'; +export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default'; +export const ALERTS_INDEX = '.siem-signals-default'; +export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id'; + +// We fetch a large number of events per page to mitigate a few design caveats in session viewer +// 1. Due to the hierarchical nature of the data (e.g we are rendering a time ordered pid tree) there are common scenarios where there +// are few top level processes, but many nested children. For example, a build script is run on a remote host via ssh. If for example our page +// size is 10 and the build script has 500 nested children, the user would see a load more button that they could continously click without seeing +// anychange since the next 10 events would be for processes nested under a top level process that might not be expanded. That being said, it's quite +// possible there are build scripts with many thousands of events, in which case this initial large page will have the same issue. A technique used +// in previous incarnations of session view included auto expanding the node which is receiving the new page of events so as to not confuse the user. +// We may need to include this trick as part of this implementation as well. +// 2. The plain text search that comes with Session view is currently limited in that it only searches through data that has been loaded into the browser. +// The large page size allows the user to get a broader set of results per page. That being said, this feature is kind of flawed since sessions could be many thousands +// if not 100s of thousands of events, and to be required to page through these sessions to find more search matches is not a great experience. Future iterations of the +// search functionality will instead use a separate ES backend search to avoid this. +// 3. Fewer round trips to the backend! +export const PROCESS_EVENTS_PER_PAGE = 1000; diff --git a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts new file mode 100644 index 00000000000000..b7b0bbb91b5ec2 --- /dev/null +++ b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts @@ -0,0 +1,951 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + Process, + ProcessEvent, + ProcessEventsPage, + ProcessFields, + EventAction, + EventKind, + ProcessMap, +} from '../../types/process_tree'; + +export const mockEvents: ProcessEvent[] = [ + { + '@timestamp': '2021-11-23T15:25:04.210Z', + user: { + name: '', + id: '1000', + }, + process: { + pid: 3535, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: false, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + parent: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.210Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + session_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.210Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + entry_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.210Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + group_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.210Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + name: '', + args_count: 0, + args: [], + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.210Z', + }, + event: { + action: EventAction.fork, + category: 'process', + kind: EventKind.event, + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + }, + { + '@timestamp': '2021-11-23T15:25:04.218Z', + user: { + name: '', + id: '1000', + }, + process: { + pid: 3535, + executable: '/usr/bin/vi', + command_line: 'bash', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3727', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + parent: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.218Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + session_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.218Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + entry_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.218Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + group_leader: { + pid: 3535, + executable: '/usr/bin/vi', + command_line: 'bash', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3727', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.218Z', + }, + event: { + action: EventAction.exec, + category: 'process', + kind: EventKind.event, + }, + }, + { + '@timestamp': '2021-11-23T15:25:05.202Z', + user: { + name: '', + id: '1000', + }, + process: { + pid: 3535, + exit_code: 137, + executable: '/usr/bin/vi', + command_line: 'bash', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3728', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + parent: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:05.202Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + session_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:05.202Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + entry_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:05.202Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + group_leader: { + pid: 3535, + exit_code: 137, + executable: '/usr/bin/vi', + command_line: 'bash', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3728', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + start: '2021-11-23T15:25:05.202Z', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + }, + event: { + action: EventAction.end, + category: 'process', + kind: EventKind.event, + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + }, +] as ProcessEvent[]; + +export const mockAlerts: ProcessEvent[] = [ + { + kibana: { + alert: { + rule: { + category: 'Custom Query Rule', + consumer: 'siem', + name: 'cmd test alert', + uuid: '709d3890-4c71-11ec-8c67-01ccde9db9bf', + enabled: true, + description: 'cmd test alert', + risk_score: 21, + severity: 'low', + query: "process.executable: '/usr/bin/vi'", + }, + status: 'active', + workflow_status: 'open', + reason: 'process event created low alert cmd test alert.', + original_time: new Date('2021-11-23T15:25:04.218Z'), + original_event: { + action: 'exec', + }, + uuid: '6bb22512e0e588d1a2449b61f164b216e366fba2de39e65d002ae734d71a6c38', + }, + }, + '@timestamp': '2021-11-23T15:26:34.859Z', + user: { + name: 'vagrant', + id: '1000', + }, + process: { + pid: 3535, + executable: '/usr/bin/vi', + command_line: 'bash', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + parent: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.859Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + session_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.859Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + entry_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.859Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + group_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args: [], + args_count: 0, + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.859Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.859Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + event: { + action: EventAction.exec, + category: 'process', + kind: EventKind.signal, + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + }, + { + kibana: { + alert: { + rule: { + category: 'Custom Query Rule', + consumer: 'siem', + name: 'cmd test alert', + uuid: '709d3890-4c71-11ec-8c67-01ccde9db9bf', + enabled: true, + description: 'cmd test alert', + risk_score: 21, + severity: 'low', + query: "process.executable: '/usr/bin/vi'", + }, + status: 'active', + workflow_status: 'open', + reason: 'process event created low alert cmd test alert.', + original_time: new Date('2021-11-23T15:25:05.202Z'), + original_event: { + action: 'exit', + }, + uuid: '2873463965b70d37ab9b2b3a90ac5a03b88e76e94ad33568285cadcefc38ed75', + }, + }, + '@timestamp': '2021-11-23T15:26:34.860Z', + user: { + name: 'vagrant', + id: '1000', + }, + process: { + pid: 3535, + exit_code: 137, + executable: '/usr/bin/vi', + command_line: 'bash', + interactive: true, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + parent: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.860Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + session_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.860Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + entry_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.860Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + group_leader: { + pid: 2442, + user: { + name: '', + id: '1000', + }, + executable: '/usr/bin/bash', + command_line: 'bash', + interactive: true, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.860Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + name: '', + args_count: 2, + args: ['vi', 'cmd/config.ini'], + working_directory: '/home/vagrant', + start: '2021-11-23T15:26:34.860Z', + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + event: { + action: EventAction.end, + category: 'process', + kind: EventKind.signal, + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + }, +]; + +export const mockData: ProcessEventsPage[] = [ + { + events: mockEvents, + cursor: '2021-11-23T15:25:04.210Z', + }, +]; + +export const childProcessMock: Process = { + id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bd', + events: [], + children: [], + autoExpand: false, + searchMatched: null, + parent: undefined, + orphans: [], + addEvent: (_) => undefined, + clearSearch: () => undefined, + getChildren: () => [], + hasOutput: () => false, + hasAlerts: () => false, + getAlerts: () => [], + hasExec: () => false, + getOutput: () => '', + getDetails: () => + ({ + '@timestamp': '2021-11-23T15:25:05.210Z', + event: { + kind: EventKind.event, + category: 'process', + action: EventAction.exec, + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + user: { + id: '1', + name: 'vagrant', + }, + process: { + args: ['ls', '-l'], + args_count: 2, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bd', + executable: '/bin/ls', + interactive: true, + name: 'ls', + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:05.210Z', + pid: 2, + parent: { + args: ['bash'], + args_count: 1, + entity_id: '3d0192c6-7c54-5ee6-a110-3539a7cf42bc', + executable: '/bin/bash', + interactive: false, + name: '', + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.210Z', + pid: 1, + user: { + id: '1', + name: 'vagrant', + }, + }, + session_leader: {} as ProcessFields, + entry_leader: {} as ProcessFields, + group_leader: {} as ProcessFields, + }, + } as ProcessEvent), + isUserEntered: () => false, + getMaxAlertLevel: () => null, +}; + +export const processMock: Process = { + id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + events: [], + children: [], + autoExpand: false, + searchMatched: null, + parent: undefined, + orphans: [], + addEvent: (_) => undefined, + clearSearch: () => undefined, + getChildren: () => [], + hasOutput: () => false, + hasAlerts: () => false, + getAlerts: () => [], + hasExec: () => false, + getOutput: () => '', + getDetails: () => + ({ + '@timestamp': '2021-11-23T15:25:04.210Z', + event: { + kind: EventKind.event, + category: 'process', + action: EventAction.exec, + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + user: { + id: '1', + name: 'vagrant', + }, + process: { + args: ['bash'], + args_count: 1, + entity_id: '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726', + executable: '/bin/bash', + exit_code: 137, + interactive: false, + name: '', + working_directory: '/home/vagrant', + start: '2021-11-23T15:25:04.210Z', + pid: 1, + parent: {} as ProcessFields, + session_leader: {} as ProcessFields, + entry_leader: {} as ProcessFields, + group_leader: {} as ProcessFields, + }, + } as ProcessEvent), + isUserEntered: () => false, + getMaxAlertLevel: () => null, +}; + +export const sessionViewBasicProcessMock: Process = { + ...processMock, + events: mockEvents, + hasExec: () => true, + isUserEntered: () => true, +}; + +export const sessionViewAlertProcessMock: Process = { + ...processMock, + events: [...mockEvents, ...mockAlerts], + hasAlerts: () => true, + getAlerts: () => mockEvents, + hasExec: () => true, + isUserEntered: () => true, +}; + +export const mockProcessMap = mockEvents.reduce( + (processMap, event) => { + processMap[event.process.entity_id] = { + id: event.process.entity_id, + events: [event], + children: [], + parent: undefined, + autoExpand: false, + searchMatched: null, + orphans: [], + addEvent: (_) => undefined, + clearSearch: () => undefined, + getChildren: () => [], + hasOutput: () => false, + hasAlerts: () => false, + getAlerts: () => [], + hasExec: () => false, + getOutput: () => '', + getDetails: () => event, + isUserEntered: () => false, + getMaxAlertLevel: () => null, + }; + return processMap; + }, + { + [sessionViewBasicProcessMock.id]: sessionViewBasicProcessMock, + } as ProcessMap +); diff --git a/x-pack/plugins/session_view/common/mocks/responses/session_view_process_events.mock.ts b/x-pack/plugins/session_view/common/mocks/responses/session_view_process_events.mock.ts new file mode 100644 index 00000000000000..47849f859ba9c1 --- /dev/null +++ b/x-pack/plugins/session_view/common/mocks/responses/session_view_process_events.mock.ts @@ -0,0 +1,1236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ProcessEventResults } from '../../types/process_tree'; + +export const sessionViewProcessEventsMock: ProcessEventResults = { + events: [ + { + _index: 'cmd', + _id: 'FMUGTX0BGGlsPv9flMF7', + _score: null, + _source: { + '@timestamp': '2021-11-23T13:40:16.528Z', + event: { + kind: 'event', + category: 'process', + action: 'fork', + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + Ext: { + variant: 'CentOS', + }, + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + user: { + // To keep backwards compat and avoid data duplication. We keep user/group info for top level process at the top level + id: '0', // the effective user aka euid + name: 'root', + real: { + // ruid + id: '2', + name: 'kg', + }, + saved: { + // suid + id: '2', + name: 'kg', + }, + }, + group: { + id: '1', // the effective group aka egid + name: 'groupA', + real: { + // rgid + id: '1', + name: 'groupA', + }, + saved: { + // sgid + id: '1', + name: 'groupA', + }, + }, + process: { + entity_id: '4321', + args: ['/bin/sshd'], + args_count: 1, + command_line: 'sshd', + executable: '/bin/sshd', + name: 'sshd', + interactive: false, + working_directory: '/', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + parent: { + entity_id: '4322', + args: ['/bin/sshd'], + args_count: 1, + command_line: 'sshd', + executable: '/bin/sshd', + name: 'sshd', + interactive: true, + working_directory: '/', + pid: 2, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '2', + name: 'kg', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + group_leader: { + entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + pid: 1234, // this directly replaces parent.pgid + start: '2021-10-14T08:05:34.853Z', + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + group_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + session_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + // parent: { + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 2, + // start: '2021-10-14T08:05:34.853Z', + // session_leader: { + // // used as a foreign key to the parent session of the session_leader + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 4321, + // start: '2021-10-14T08:05:34.853Z', + // }, + // }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + entry_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + // parent: { + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 2, + // start: '2021-10-14T08:05:34.853Z', + // session_leader: { + // // used as a foreign key to the parent session of the entry_leader + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 4321, + // start: '2021-10-14T08:05:34.853Z', + // }, + // }, + entry_meta: { + type: 'sshd', + source: { + ip: '10.132.0.50', + geo: { + city_name: 'Vancouver', + continent_code: 'NA', + continent_name: 'North America', + country_iso_code: 'CA', + country_name: 'Canada', + location: { + lon: -73.61483, + lat: 45.505918, + }, + postal_code: 'V9J1E3', + region_iso_code: 'BC', + region_name: 'British Columbia', + timezone: 'America/Los_Angeles', + }, + }, + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + { + descriptor: 1, + type: 'pipe', + pipe: { + inode: '6183207', + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + }, + sort: [1637674816528], + }, + { + _index: 'cmd', + _id: 'FsUGTX0BGGlsPv9flMGF', + _score: null, + _source: { + '@timestamp': '2021-11-23T13:40:16.541Z', + event: { + kind: 'event', + category: 'process', + action: 'exec', + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + Ext: { + variant: 'CentOS', + }, + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + user: { + id: '2', + name: 'kg', + real: { + id: '2', + name: 'kg', + }, + saved: { + id: '2', + name: 'kg', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + process: { + entity_id: '4321', + args: ['/bin/bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + previous: [{ args: ['/bin/sshd'], args_count: 1, executable: '/bin/sshd' }], + parent: { + entity_id: '4322', + args: ['/bin/sshd'], + args_count: 1, + command_line: 'sshd', + executable: '/bin/sshd', + name: 'sshd', + interactive: true, + working_directory: '/', + pid: 2, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + group_leader: { + entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + pid: 1234, // this directly replaces parent.pgid + start: '2021-10-14T08:05:34.853Z', + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + group_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + session_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + // parent: { + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 2, + // start: '2021-10-14T08:05:34.853Z', + // session_leader: { + // // used as a foreign key to the parent session of the session_leader + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 4321, + // start: '2021-10-14T08:05:34.853Z', + // }, + // }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + entry_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + // parent: { + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 2, + // start: '2021-10-14T08:05:34.853Z', + // session_leader: { + // // used as a foreign key to the parent session of the entry_leader + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 4321, + // start: '2021-10-14T08:05:34.853Z', + // }, + // }, + entry_meta: { + type: 'sshd', + source: { + ip: '10.132.0.50', + geo: { + city_name: 'Vancouver', + continent_code: 'NA', + continent_name: 'North America', + country_iso_code: 'CA', + country_name: 'Canada', + location: { + lon: -73.61483, + lat: 45.505918, + }, + postal_code: 'V9J1E3', + region_iso_code: 'BC', + region_name: 'British Columbia', + timezone: 'America/Los_Angeles', + }, + }, + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + { + descriptor: 1, + type: 'pipe', + pipe: { + inode: '6183207', + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + }, + sort: [1637674816541], + }, + { + _index: 'cmd', + _id: 'H8UGTX0BGGlsPv9fp8F_', + _score: null, + _source: { + '@timestamp': '2021-11-23T13:40:21.392Z', + event: { + kind: 'event', + category: 'process', + action: 'end', + }, + host: { + architecture: 'x86_64', + hostname: 'james-fleet-714-2', + id: '48c1b3f1ac5da4e0057fc9f60f4d1d5d', + ip: '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809', + mac: '42:01:0a:84:00:32', + name: 'james-fleet-714-2', + os: { + Ext: { + variant: 'CentOS', + }, + family: 'centos', + full: 'CentOS 7.9.2009', + kernel: '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021', + name: 'Linux', + platform: 'centos', + version: '7.9.2009', + }, + }, + user: { + id: '2', + name: 'kg', + real: { + id: '2', + name: 'kg', + }, + saved: { + id: '2', + name: 'kg', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + process: { + entity_id: '4321', + args: ['/bin/bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + end: '2021-10-14T10:05:34.853Z', + exit_code: 137, + previous: [{ args: ['/bin/sshd'], args_count: 1, executable: '/bin/sshd' }], + parent: { + entity_id: '4322', + args: ['/bin/sshd'], + args_count: 1, + command_line: 'sshd', + executable: '/bin/sshd', + name: 'sshd', + interactive: true, + working_directory: '/', + pid: 2, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + group_leader: { + entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + pid: 1234, // this directly replaces parent.pgid + start: '2021-10-14T08:05:34.853Z', + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + group_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + session_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + // parent: { + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 2, + // start: '2021-10-14T08:05:34.853Z', + // session_leader: { + // // used as a foreign key to the parent session of the session_leader + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 4321, + // start: '2021-10-14T08:05:34.853Z', + // }, + // }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + entry_leader: { + entity_id: '4321', + args: ['bash'], + args_count: 1, + command_line: 'bash', + executable: '/bin/bash', + name: 'bash', + interactive: true, + working_directory: '/home/kg', + pid: 3, + start: '2021-10-14T08:05:34.853Z', + user: { + id: '0', + name: 'root', + real: { + id: '0', + name: 'root', + }, + saved: { + id: '0', + name: 'root', + }, + }, + group: { + id: '1', + name: 'groupA', + real: { + id: '1', + name: 'groupA', + }, + saved: { + id: '1', + name: 'groupA', + }, + supplemental: [ + { + id: '2', + name: 'groupB', + }, + { + id: '3', + name: 'groupC', + }, + ], + }, + // parent: { + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 2, + // start: '2021-10-14T08:05:34.853Z', + // session_leader: { + // // used as a foreign key to the parent session of the entry_leader + // entity_id: '0fe5f6a0-6f04-49a5-8faf-768445b38d16', + // pid: 4321, + // start: '2021-10-14T08:05:34.853Z', + // }, + // }, + entry_meta: { + type: 'sshd', + source: { + ip: '10.132.0.50', + geo: { + city_name: 'Vancouver', + continent_code: 'NA', + continent_name: 'North America', + country_iso_code: 'CA', + country_name: 'Canada', + location: { + lon: -73.61483, + lat: 45.505918, + }, + postal_code: 'V9J1E3', + region_iso_code: 'BC', + region_name: 'British Columbia', + timezone: 'America/Los_Angeles', + }, + }, + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + file_descriptions: [ + { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + { + descriptor: 1, + type: 'pipe', + pipe: { + inode: '6183207', + }, + }, + ], + tty: { + descriptor: 0, + type: 'char_device', + char_device: { + major: 8, + minor: 1, + }, + }, + }, + }, + sort: [1637674821392], + }, + ], +}; diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts new file mode 100644 index 00000000000000..746c1b2093661b --- /dev/null +++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const enum EventKind { + event = 'event', + signal = 'signal', +} + +export const enum EventAction { + fork = 'fork', + exec = 'exec', + end = 'end', + output = 'output', +} + +export interface User { + id: string; + name: string; +} + +export interface ProcessEventResults { + events: any[]; +} + +export type EntryMetaType = + | 'init' + | 'sshd' + | 'ssm' + | 'kubelet' + | 'teleport' + | 'terminal' + | 'console'; + +export interface EntryMeta { + type: EntryMetaType; + source: { + ip: string; + }; +} + +export interface Teletype { + descriptor: number; + type: string; + char_device: { + major: number; + minor: number; + }; +} + +export interface ProcessFields { + entity_id: string; + args: string[]; + args_count: number; + command_line: string; + executable: string; + name: string; + interactive: boolean; + working_directory: string; + pid: number; + start: string; + end?: string; + user: User; + exit_code?: number; + entry_meta?: EntryMeta; + tty: Teletype; +} + +export interface ProcessSelf extends Omit { + parent: ProcessFields; + session_leader: ProcessFields; + entry_leader: ProcessFields; + group_leader: ProcessFields; +} + +export interface ProcessEventHost { + architecture: string; + hostname: string; + id: string; + ip: string; + mac: string; + name: string; + os: { + family: string; + full: string; + kernel: string; + name: string; + platform: string; + version: string; + }; +} + +export interface ProcessEventAlertRule { + category: string; + consumer: string; + description: string; + enabled: boolean; + name: string; + query: string; + risk_score: number; + severity: string; + uuid: string; +} + +export interface ProcessEventAlert { + uuid: string; + reason: string; + workflow_status: string; + status: string; + original_time: Date; + original_event: { + action: string; + }; + rule: ProcessEventAlertRule; +} + +export interface ProcessEvent { + '@timestamp': string; + event: { + kind: EventKind; + category: string; + action: EventAction; + }; + user: User; + host: ProcessEventHost; + process: ProcessSelf; + kibana?: { + alert: ProcessEventAlert; + }; +} + +export interface ProcessEventsPage { + events: ProcessEvent[]; + cursor: string; +} + +export interface Process { + id: string; // the process entity_id + events: ProcessEvent[]; + children: Process[]; + orphans: Process[]; // currently, orphans are rendered inline with the entry session leaders children + parent: Process | undefined; + autoExpand: boolean; + searchMatched: string | null; // either false, or set to searchQuery + addEvent(event: ProcessEvent): void; + clearSearch(): void; + hasOutput(): boolean; + hasAlerts(): boolean; + getAlerts(): ProcessEvent[]; + hasExec(): boolean; + getOutput(): string; + getDetails(): ProcessEvent; + isUserEntered(): boolean; + getMaxAlertLevel(): number | null; + getChildren(verboseMode: boolean): Process[]; +} + +export type ProcessMap = { + [key: string]: Process; +}; diff --git a/x-pack/plugins/session_view/common/utils/expand_dotted_object.test.ts b/x-pack/plugins/session_view/common/utils/expand_dotted_object.test.ts new file mode 100644 index 00000000000000..a4a4845e759e7a --- /dev/null +++ b/x-pack/plugins/session_view/common/utils/expand_dotted_object.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expandDottedObject } from './expand_dotted_object'; + +const testFlattenedObj = { + 'flattened.property.a': 'valueA', + 'flattened.property.b': 'valueB', + regularProp: { + nestedProp: 'nestedValue', + }, + 'nested.array': [ + { + arrayProp: 'arrayValue', + }, + ], + emptyArray: [], +}; +describe('expandDottedObject(obj)', () => { + it('retrieves values from flattened keys', () => { + const expanded: any = expandDottedObject(testFlattenedObj); + + expect(expanded.flattened.property.a).toEqual('valueA'); + expect(expanded.flattened.property.b).toEqual('valueB'); + }); + it('retrieves values from nested keys', () => { + const expanded: any = expandDottedObject(testFlattenedObj); + + expect(Array.isArray(expanded.nested.array)).toBeTruthy(); + expect(expanded.nested.array[0].arrayProp).toEqual('arrayValue'); + }); + it("doesn't break regular value access", () => { + const expanded: any = expandDottedObject(testFlattenedObj); + + expect(expanded.regularProp.nestedProp).toEqual('nestedValue'); + }); +}); diff --git a/x-pack/plugins/session_view/common/utils/expand_dotted_object.ts b/x-pack/plugins/session_view/common/utils/expand_dotted_object.ts new file mode 100644 index 00000000000000..69a9cb8236cbce --- /dev/null +++ b/x-pack/plugins/session_view/common/utils/expand_dotted_object.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { merge } from '@kbn/std'; + +const expandDottedField = (dottedFieldName: string, val: unknown): object => { + const parts = dottedFieldName.split('.'); + if (parts.length === 1) { + return { [parts[0]]: val }; + } else { + return { [parts[0]]: expandDottedField(parts.slice(1).join('.'), val) }; + } +}; + +/* + * Expands an object with "dotted" fields to a nested object with unflattened fields. + * + * Example: + * expandDottedObject({ + * "kibana.alert.depth": 1, + * "kibana.alert.ancestors": [{ + * id: "d5e8eb51-a6a0-456d-8a15-4b79bfec3d71", + * type: "event", + * index: "signal_index", + * depth: 0, + * }], + * }) + * + * => { + * kibana: { + * alert: { + * ancestors: [ + * id: "d5e8eb51-a6a0-456d-8a15-4b79bfec3d71", + * type: "event", + * index: "signal_index", + * depth: 0, + * ], + * depth: 1, + * }, + * }, + * } + */ +export const expandDottedObject = (dottedObj: object) => { + return Object.entries(dottedObj).reduce( + (acc, [key, val]) => merge(acc, expandDottedField(key, val)), + {} + ); +}; diff --git a/x-pack/plugins/session_view/common/utils/sort_processes.test.ts b/x-pack/plugins/session_view/common/utils/sort_processes.test.ts new file mode 100644 index 00000000000000..b1db5381954dcb --- /dev/null +++ b/x-pack/plugins/session_view/common/utils/sort_processes.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { sortProcesses } from './sort_processes'; +import { mockProcessMap } from '../mocks/constants/session_view_process.mock'; + +describe('sortProcesses(a, b)', () => { + it('sorts processes in ascending order by start time', () => { + const processes = Object.values(mockProcessMap); + + // shuffle some things to ensure all sort lines are hit + const c = processes[0]; + processes[0] = processes[processes.length - 1]; + processes[processes.length - 1] = c; + + processes.sort(sortProcesses); + + for (let i = 0; i < processes.length - 1; i++) { + const current = processes[i]; + const next = processes[i + 1]; + expect( + new Date(next.getDetails().process.start) >= new Date(current.getDetails().process.start) + ).toBeTruthy(); + } + }); +}); diff --git a/x-pack/plugins/session_view/common/utils/sort_processes.ts b/x-pack/plugins/session_view/common/utils/sort_processes.ts new file mode 100644 index 00000000000000..a0a42590e457e6 --- /dev/null +++ b/x-pack/plugins/session_view/common/utils/sort_processes.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Process } from '../types/process_tree'; + +export const sortProcesses = (a: Process, b: Process) => { + const eventAStartTime = new Date(a.getDetails().process.start); + const eventBStartTime = new Date(b.getDetails().process.start); + + if (eventAStartTime < eventBStartTime) { + return -1; + } + + if (eventAStartTime > eventBStartTime) { + return 1; + } + + return 0; +}; diff --git a/x-pack/plugins/session_view/jest.config.js b/x-pack/plugins/session_view/jest.config.js new file mode 100644 index 00000000000000..d35db0d369468d --- /dev/null +++ b/x-pack/plugins/session_view/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/session_view'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/session_view', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/session_view/{common,public,server}/**/*.{ts,tsx}', + ], + setupFiles: ['jest-canvas-mock'], +}; diff --git a/x-pack/plugins/session_view/kibana.json b/x-pack/plugins/session_view/kibana.json new file mode 100644 index 00000000000000..ff9d849016c555 --- /dev/null +++ b/x-pack/plugins/session_view/kibana.json @@ -0,0 +1,19 @@ +{ + "id": "sessionView", + "version": "8.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Security Team", + "githubTeam": "security-team" + }, + "requiredPlugins": [ + "data", + "timelines" + ], + "requiredBundles": [ + "kibanaReact", + "esUiShared" + ], + "server": true, + "ui": true +} diff --git a/x-pack/plugins/session_view/package.json b/x-pack/plugins/session_view/package.json new file mode 100644 index 00000000000000..2cb3dc882ed711 --- /dev/null +++ b/x-pack/plugins/session_view/package.json @@ -0,0 +1,11 @@ +{ + "author": "Elastic", + "name": "session_view", + "version": "8.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "test:jest": "node ../../scripts/jest", + "test:coverage": "node ../../scripts/jest --coverage" + } +} diff --git a/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.test.tsx new file mode 100644 index 00000000000000..80ad3ce0c46302 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelAccordion } from './index'; + +const TEST_ID = 'test'; +const TEST_LIST_ITEM = [ + { + title: 'item title', + description: 'item description', + }, +]; +const TEST_TITLE = 'accordion title'; +const ACTION_TEXT = 'extra action'; + +describe('DetailPanelAccordion component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelAccordion is mounted', () => { + it('should render basic acoordion', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId('sessionView:detail-panel-accordion')).toBeVisible(); + }); + + it('should render acoordion with tooltip', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId('sessionView:detail-panel-accordion')).toBeVisible(); + expect( + renderResult.queryByTestId('sessionView:detail-panel-accordion-tooltip') + ).toBeVisible(); + }); + + it('should render acoordion with extra action', async () => { + const mockFn = jest.fn(); + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId('sessionView:detail-panel-accordion')).toBeVisible(); + const extraActionButton = renderResult.getByTestId( + 'sessionView:detail-panel-accordion-action' + ); + expect(extraActionButton).toHaveTextContent(ACTION_TEXT); + extraActionButton.click(); + expect(mockFn).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.tsx new file mode 100644 index 00000000000000..4e03931e4fcd97 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_accordion/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { ReactNode } from 'react'; +import { EuiAccordion, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; +import { useStyles } from './styles'; +import { DetailPanelDescriptionList } from '../detail_panel_description_list'; + +interface DetailPanelAccordionDeps { + id: string; + listItems: Array<{ + title: NonNullable; + description: NonNullable; + }>; + title: string; + tooltipContent?: string; + extraActionTitle?: string; + onExtraActionClick?: () => void; +} + +/** + * An accordion section in session view detail panel. + */ +export const DetailPanelAccordion = ({ + id, + listItems, + title, + tooltipContent, + extraActionTitle, + onExtraActionClick, +}: DetailPanelAccordionDeps) => { + const styles = useStyles(); + + return ( + + + {title} + + {tooltipContent && ( + + + + )} + + } + extraAction={ + extraActionTitle ? ( + + {extraActionTitle} + + ) : null + } + css={styles.accordion} + data-test-subj="sessionView:detail-panel-accordion" + > + + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts new file mode 100644 index 00000000000000..c44e069c05c004 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_accordion/styles.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const tabSection: CSSObject = { + padding: euiTheme.size.base, + }; + + const accordion: CSSObject = { + borderTop: euiTheme.border.thin, + '&:last-child': { + borderBottom: euiTheme.border.thin, + }, + }; + + const accordionButton: CSSObject = { + padding: euiTheme.size.base, + fontWeight: euiTheme.font.weight.bold, + }; + + return { + accordion, + accordionButton, + tabSection, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_copy/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_copy/index.test.tsx new file mode 100644 index 00000000000000..bb1dd243621bd5 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_copy/index.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelCopy } from './index'; + +const TEST_TEXT_COPY = 'copy component test'; +const TEST_CHILD = {TEST_TEXT_COPY}; + +describe('DetailPanelCopy component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelCopy is mounted', () => { + it('renders DetailPanelCopy correctly', async () => { + renderResult = mockedContext.render( + {TEST_CHILD} + ); + + expect(renderResult.queryByText(TEST_TEXT_COPY)).toBeVisible(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_copy/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_copy/index.tsx new file mode 100644 index 00000000000000..a5ce77894949b1 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_copy/index.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { ReactNode } from 'react'; +import { EuiButtonIcon, EuiCopy } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DetailPanelListItem } from '../detail_panel_list_item'; +import { dataOrDash } from '../../utils/data_or_dash'; +import { useStyles } from './styles'; + +interface DetailPanelCopyDeps { + children: ReactNode; + textToCopy: string | number; + display?: 'inlineBlock' | 'block' | undefined; +} + +interface DetailPanelListItemProps { + copy: ReactNode; + display?: string; +} + +/** + * Copy to clipboard component in Session view detail panel. + */ +export const DetailPanelCopy = ({ + children, + textToCopy, + display = 'inlineBlock', +}: DetailPanelCopyDeps) => { + const styles = useStyles(); + + const props: DetailPanelListItemProps = { + copy: ( + + {(copy) => ( + + )} + + ), + }; + + if (display === 'block') { + props.display = display; + } + + return {children}; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_copy/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_copy/styles.ts new file mode 100644 index 00000000000000..0bfc67dddb8859 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_copy/styles.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const copyButton: CSSObject = { + position: 'absolute', + right: euiTheme.size.s, + top: 0, + bottom: 0, + margin: 'auto', + }; + + return { + copyButton, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.test.tsx new file mode 100644 index 00000000000000..aaf3086aabf5ee --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelDescriptionList } from './index'; + +const TEST_FIRST_TITLE = 'item title'; +const TEST_FIRST_DESCRIPTION = 'item description'; +const TEST_SECOND_TITLE = 'second title'; +const TEST_SECOND_DESCRIPTION = 'second description'; +const TEST_LIST_ITEM = [ + { + title: TEST_FIRST_TITLE, + description: TEST_FIRST_DESCRIPTION, + }, + { + title: TEST_SECOND_TITLE, + description: TEST_SECOND_DESCRIPTION, + }, +]; + +describe('DetailPanelDescriptionList component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelDescriptionList is mounted', () => { + it('renders DetailPanelDescriptionList correctly', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId('sessionView:detail-panel-description-list')).toBeVisible(); + + // check list items are rendered + expect(renderResult.queryByText(TEST_FIRST_TITLE)).toBeVisible(); + expect(renderResult.queryByText(TEST_FIRST_DESCRIPTION)).toBeVisible(); + expect(renderResult.queryByText(TEST_SECOND_TITLE)).toBeVisible(); + expect(renderResult.queryByText(TEST_SECOND_DESCRIPTION)).toBeVisible(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.tsx new file mode 100644 index 00000000000000..3d942fc42326e5 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_description_list/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { ReactNode } from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import { useStyles } from './styles'; + +interface DetailPanelDescriptionListDeps { + listItems: Array<{ + title: NonNullable; + description: NonNullable; + }>; +} + +/** + * Description list in session view detail panel. + */ +export const DetailPanelDescriptionList = ({ listItems }: DetailPanelDescriptionListDeps) => { + const styles = useStyles(); + return ( + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts new file mode 100644 index 00000000000000..d815cb2a48283b --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_description_list/styles.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { CSSObject } from '@emotion/react'; +import { useEuiTheme } from '@elastic/eui'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const descriptionList: CSSObject = { + padding: euiTheme.size.s, + }; + + const tabListTitle = { + width: '40%', + display: 'flex', + alignItems: 'center', + }; + + const tabListDescription = { + width: '60%', + display: 'flex', + alignItems: 'center', + }; + + return { + descriptionList, + tabListTitle, + tabListDescription, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.test.tsx new file mode 100644 index 00000000000000..2df9f47e5a4163 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { ProcessEventHost } from '../../../common/types/process_tree'; +import { DetailPanelHostTab } from './index'; + +const TEST_ARCHITECTURE = 'x86_64'; +const TEST_HOSTNAME = 'host-james-fleet-714-2'; +const TEST_ID = '48c1b3f1ac5da4e0057fc9f60f4d1d5d'; +const TEST_IP = '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809'; +const TEST_MAC = '42:01:0a:84:00:32'; +const TEST_NAME = 'name-james-fleet-714-2'; +const TEST_OS_FAMILY = 'family-centos'; +const TEST_OS_FULL = 'full-CentOS 7.9.2009'; +const TEST_OS_KERNEL = '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021'; +const TEST_OS_NAME = 'os-Linux'; +const TEST_OS_PLATFORM = 'platform-centos'; +const TEST_OS_VERSION = 'version-7.9.2009'; + +const TEST_HOST: ProcessEventHost = { + architecture: TEST_ARCHITECTURE, + hostname: TEST_HOSTNAME, + id: TEST_ID, + ip: TEST_IP, + mac: TEST_MAC, + name: TEST_NAME, + os: { + family: TEST_OS_FAMILY, + full: TEST_OS_FULL, + kernel: TEST_OS_KERNEL, + name: TEST_OS_NAME, + platform: TEST_OS_PLATFORM, + version: TEST_OS_VERSION, + }, +}; + +describe('DetailPanelHostTab component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelHostTab is mounted', () => { + it('renders DetailPanelHostTab correctly', async () => { + renderResult = mockedContext.render(); + + expect(renderResult.queryByText('architecture')).toBeVisible(); + expect(renderResult.queryByText('hostname')).toBeVisible(); + expect(renderResult.queryByText('id')).toBeVisible(); + expect(renderResult.queryByText('ip')).toBeVisible(); + expect(renderResult.queryByText('mac')).toBeVisible(); + expect(renderResult.queryByText('name')).toBeVisible(); + expect(renderResult.queryByText(TEST_ARCHITECTURE)).toBeVisible(); + expect(renderResult.queryByText(TEST_HOSTNAME)).toBeVisible(); + expect(renderResult.queryByText(TEST_ID)).toBeVisible(); + expect(renderResult.queryByText(TEST_IP)).toBeVisible(); + expect(renderResult.queryByText(TEST_MAC)).toBeVisible(); + expect(renderResult.queryByText(TEST_NAME)).toBeVisible(); + + // expand host os accordion + renderResult + .queryByTestId('sessionView:detail-panel-accordion') + ?.querySelector('button') + ?.click(); + expect(renderResult.queryByText('os.family')).toBeVisible(); + expect(renderResult.queryByText('os.full')).toBeVisible(); + expect(renderResult.queryByText('os.kernel')).toBeVisible(); + expect(renderResult.queryByText('os.name')).toBeVisible(); + expect(renderResult.queryByText('os.platform')).toBeVisible(); + expect(renderResult.queryByText('os.version')).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_FAMILY)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_FULL)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_KERNEL)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_NAME)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_PLATFORM)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_VERSION)).toBeVisible(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.tsx new file mode 100644 index 00000000000000..e46e0e2751872d --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_host_tab/index.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { EuiTextColor } from '@elastic/eui'; +import { ProcessEventHost } from '../../../common/types/process_tree'; +import { DetailPanelAccordion } from '../detail_panel_accordion'; +import { DetailPanelCopy } from '../detail_panel_copy'; +import { DetailPanelDescriptionList } from '../detail_panel_description_list'; +import { DetailPanelListItem } from '../detail_panel_list_item'; +import { dataOrDash } from '../../utils/data_or_dash'; +import { useStyles } from '../detail_panel_process_tab/styles'; + +interface DetailPanelHostTabDeps { + processHost: ProcessEventHost; +} + +/** + * Host Panel of session view detail panel. + */ +export const DetailPanelHostTab = ({ processHost }: DetailPanelHostTabDeps) => { + const styles = useStyles(); + + return ( + <> + hostname, + description: ( + + + {dataOrDash(processHost.hostname)} + + + ), + }, + { + title: id, + description: ( + + + {dataOrDash(processHost.id)} + + + ), + }, + { + title: ip, + description: ( + + + {dataOrDash(processHost.ip)} + + + ), + }, + { + title: mac, + description: ( + + + {dataOrDash(processHost.mac)} + + + ), + }, + { + title: name, + description: ( + + + {dataOrDash(processHost.name)} + + + ), + }, + ]} + /> + architecture, + description: ( + + + {dataOrDash(processHost.architecture)} + + + ), + }, + { + title: os.family, + description: ( + + + {dataOrDash(processHost.os.family)} + + + ), + }, + { + title: os.full, + description: ( + + + {dataOrDash(processHost.os.full)} + + + ), + }, + { + title: os.kernel, + description: ( + + + {dataOrDash(processHost.os.kernel)} + + + ), + }, + { + title: os.name, + description: ( + + + {dataOrDash(processHost.os.name)} + + + ), + }, + { + title: os.platform, + description: ( + + + {dataOrDash(processHost.os.platform)} + + + ), + }, + { + title: os.version, + description: ( + + + {dataOrDash(processHost.os.version)} + + + ), + }, + ]} + /> + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.test.tsx new file mode 100644 index 00000000000000..e6572a097d85a3 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelListItem } from './index'; + +const TEST_STRING = 'item title'; +const TEST_CHILD = {TEST_STRING}; +const TEST_COPY_STRING = 'test copy button'; +const BUTTON_TEST_ID = 'sessionView:test-copy-button'; +const TEST_COPY = ; +const LIST_ITEM_TEST_ID = 'sessionView:detail-panel-list-item'; +const WAIT_TIMEOUT = 500; + +describe('DetailPanelListItem component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelListItem is mounted', () => { + it('renders DetailPanelListItem correctly', async () => { + renderResult = mockedContext.render({TEST_CHILD}); + + expect(renderResult.queryByTestId(LIST_ITEM_TEST_ID)).toBeVisible(); + expect(renderResult.queryByText(TEST_STRING)).toBeVisible(); + }); + + it('renders copy element correctly', async () => { + renderResult = mockedContext.render( + {TEST_CHILD} + ); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID)); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeVisible(); + + fireEvent.mouseLeave(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + }); + + it('does not have mouse events when copy prop is not present', async () => { + renderResult = mockedContext.render({TEST_CHILD}); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID), { timeout: WAIT_TIMEOUT }); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.tsx new file mode 100644 index 00000000000000..93a6554bbe54aa --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_list_item/index.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, ReactNode } from 'react'; +import { EuiText, EuiTextProps } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; +import { useStyles } from './styles'; + +interface DetailPanelListItemDeps { + children: ReactNode; + copy?: ReactNode; + display?: string; +} + +interface EuiTextPropsCss extends EuiTextProps { + css: CSSObject; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} + +/** + * Detail panel description list item. + */ +export const DetailPanelListItem = ({ + children, + copy, + display = 'flex', +}: DetailPanelListItemDeps) => { + const [isHovered, setIsHovered] = useState(false); + const styles = useStyles({ display }); + + const props: EuiTextPropsCss = { + size: 's', + css: !!copy ? styles.copiableItem : styles.item, + }; + + if (!!copy) { + props.onMouseEnter = () => setIsHovered(true); + props.onMouseLeave = () => setIsHovered(false); + } + + return ( + + {children} + {isHovered && copy} + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.ts new file mode 100644 index 00000000000000..c370bd8adb6e2d --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_list_item/styles.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 { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +interface StylesDeps { + display: string | undefined; +} + +export const useStyles = ({ display }: StylesDeps) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const item: CSSObject = { + display, + alignItems: 'center', + padding: euiTheme.size.s, + width: '100%', + fontSize: 'inherit', + fontWeight: 'inherit', + minHeight: '36px', + }; + + const copiableItem: CSSObject = { + ...item, + position: 'relative', + borderRadius: euiTheme.border.radius.medium, + '&:hover': { + background: transparentize(euiTheme.colors.primary, 0.1), + }, + }; + + return { + item, + copiableItem, + }; + }, [display, euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.test.ts b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.test.ts new file mode 100644 index 00000000000000..d458ee3a1d6669 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.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 { getProcessExecutableCopyText } from './helpers'; + +describe('detail panel process tab helpers tests', () => { + it('getProcessExecutableCopyText works with empty array', () => { + const result = getProcessExecutableCopyText([]); + expect(result).toEqual(''); + }); + + it('getProcessExecutableCopyText works with array of tuples', () => { + const result = getProcessExecutableCopyText([ + ['echo', 'exec'], + ['echo', 'exit'], + ]); + expect(result).toEqual('echo exec, echo exit'); + }); + + it('getProcessExecutableCopyText returns empty string with an invalid array of tuples', () => { + // when some sub arrays only have 1 item + let result = getProcessExecutableCopyText([['echo', 'exec'], ['echo']]); + expect(result).toEqual(''); + + // when some sub arrays have more than two item + result = getProcessExecutableCopyText([ + ['echo', 'exec'], + ['echo', 'exec', 'random'], + ['echo', 'exit'], + ]); + expect(result).toEqual(''); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.ts b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.ts new file mode 100644 index 00000000000000..632e0bc5fd2e3e --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/helpers.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Serialize an array of executable tuples to a copyable text. + * + * @param {String[][]} executable + * @return {String} serialized string with data of each executable + */ +export const getProcessExecutableCopyText = (executable: string[][]) => { + try { + return executable + .map((execTuple) => { + const [execCommand, eventAction] = execTuple; + if (!execCommand || !eventAction || execTuple.length !== 2) { + throw new Error(); + } + return `${execCommand} ${eventAction}`; + }) + .join(', '); + } catch (_) { + return ''; + } +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.test.tsx new file mode 100644 index 00000000000000..074c69de7e8992 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelProcess, DetailPanelProcessLeader } from '../../types'; +import { DetailPanelProcessTab } from './index'; + +const getLeaderDetail = (leader: string): DetailPanelProcessLeader => ({ + id: `${leader}-id`, + name: `${leader}-name`, + start: new Date('2022-02-24').toISOString(), + entryMetaType: 'sshd', + userName: `${leader}-jack`, + interactive: true, + pid: 1234, + entryMetaSourceIp: '10.132.0.50', + executable: '/usr/bin/bash', +}); + +const TEST_PROCESS_DETAIL: DetailPanelProcess = { + id: 'process-id', + start: new Date('2022-02-22').toISOString(), + end: new Date('2022-02-23').toISOString(), + exit_code: 137, + user: 'process-jack', + args: ['vi', 'test.txt'], + executable: [ + ['test-executable-cmd', '(fork)'], + ['test-executable-cmd', '(exec)'], + ['test-executable-cmd', '(end)'], + ], + pid: 1233, + entryLeader: getLeaderDetail('entryLeader'), + sessionLeader: getLeaderDetail('sessionLeader'), + groupLeader: getLeaderDetail('groupLeader'), + parent: getLeaderDetail('parent'), +}; + +describe('DetailPanelProcessTab component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelProcessTab is mounted', () => { + it('renders DetailPanelProcessTab correctly', async () => { + renderResult = mockedContext.render( + + ); + + // Process detail rendered correctly + expect(renderResult.queryByText(TEST_PROCESS_DETAIL.id)).toBeVisible(); + expect(renderResult.queryByText(TEST_PROCESS_DETAIL.start)).toBeVisible(); + expect(renderResult.queryByText(TEST_PROCESS_DETAIL.end)).toBeVisible(); + expect(renderResult.queryByText(TEST_PROCESS_DETAIL.exit_code)).toBeVisible(); + expect(renderResult.queryByText(TEST_PROCESS_DETAIL.user)).toBeVisible(); + expect(renderResult.queryByText(`['vi','test.txt']`)).toBeVisible(); + expect(renderResult.queryAllByText('test-executable-cmd')).toHaveLength(3); + expect(renderResult.queryByText('(fork)')).toBeVisible(); + expect(renderResult.queryByText('(exec)')).toBeVisible(); + expect(renderResult.queryByText('(end)')).toBeVisible(); + expect(renderResult.queryByText(TEST_PROCESS_DETAIL.pid)).toBeVisible(); + + // Process tab accordions rendered correctly + expect(renderResult.queryByText('entryLeader-name')).toBeVisible(); + expect(renderResult.queryByText('sessionLeader-name')).toBeVisible(); + expect(renderResult.queryByText('groupLeader-name')).toBeVisible(); + expect(renderResult.queryByText('parent-name')).toBeVisible(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/index.tsx new file mode 100644 index 00000000000000..97e2cdc806c0f0 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { ReactNode } from 'react'; +import { EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DetailPanelProcess } from '../../types'; +import { DetailPanelAccordion } from '../detail_panel_accordion'; +import { DetailPanelCopy } from '../detail_panel_copy'; +import { DetailPanelDescriptionList } from '../detail_panel_description_list'; +import { DetailPanelListItem } from '../detail_panel_list_item'; +import { dataOrDash } from '../../utils/data_or_dash'; +import { getProcessExecutableCopyText } from './helpers'; +import { useStyles } from './styles'; + +interface DetailPanelProcessTabDeps { + processDetail: DetailPanelProcess; +} + +type ListItems = Array<{ + title: NonNullable; + description: NonNullable; +}>; + +// TODO: Update placeholder descriptions for these tootips once UX Writer Team Defines them +const leaderDescriptionListInfo = [ + { + id: 'processEntryLeader', + title: 'Entry Leader', + tooltipContent: i18n.translate('xpack.sessionView.detailPanel.entryLeaderTooltip', { + defaultMessage: 'A entry leader placeholder description', + }), + }, + { + id: 'processSessionLeader', + title: 'Session Leader', + tooltipContent: i18n.translate('xpack.sessionView.detailPanel.sessionLeaderTooltip', { + defaultMessage: 'A session leader placeholder description', + }), + }, + { + id: 'processGroupLeader', + title: 'Group Leader', + tooltipContent: i18n.translate('xpack.sessionView.detailPanel.processGroupLeaderTooltip', { + defaultMessage: 'a group leader placeholder description', + }), + }, + { + id: 'processParent', + title: 'Parent', + tooltipContent: i18n.translate('xpack.sessionView.detailPanel.processParentTooltip', { + defaultMessage: 'a parent placeholder description', + }), + }, +]; + +/** + * Detail panel in the session view. + */ +export const DetailPanelProcessTab = ({ processDetail }: DetailPanelProcessTabDeps) => { + const styles = useStyles(); + const leaderListItems = [ + processDetail.entryLeader, + processDetail.sessionLeader, + processDetail.groupLeader, + processDetail.parent, + ].map((leader, idx) => { + const listItems: ListItems = [ + { + title: id, + description: ( + + + {dataOrDash(leader.id)} + + + ), + }, + { + title: start, + description: ( + + {leader.start} + + ), + }, + ]; + // Only include entry_meta.type for entry leader + if (idx === 0) { + listItems.push({ + title: entry_meta.type, + description: ( + + + {dataOrDash(leader.entryMetaType)} + + + ), + }); + } + listItems.push( + { + title: user.name, + description: ( + + {dataOrDash(leader.userName)} + + ), + }, + { + title: interactive, + description: ( + + {leader.interactive ? 'True' : 'False'} + + ), + }, + { + title: pid, + description: ( + + {dataOrDash(leader.pid)} + + ), + } + ); + // Only include entry_meta.source.ip for entry leader + if (idx === 0) { + listItems.push({ + title: entry_meta.source.ip, + description: ( + + {dataOrDash(leader.entryMetaSourceIp)} + + ), + }); + } + return { + ...leaderDescriptionListInfo[idx], + name: leader.name, + listItems, + }; + }); + + const processArgs = processDetail.args.length + ? `[${processDetail.args.map((arg) => `'${arg}'`)}]` + : '-'; + + return ( + <> + id, + description: ( + + + {dataOrDash(processDetail.id)} + + + ), + }, + { + title: start, + description: ( + + {processDetail.start} + + ), + }, + { + title: end, + description: ( + + {processDetail.end} + + ), + }, + { + title: exit_code, + description: ( + + + {dataOrDash(processDetail.exit_code)} + + + ), + }, + { + title: user, + description: ( + + {dataOrDash(processDetail.user)} + + ), + }, + { + title: args, + description: ( + + {processArgs} + + ), + }, + { + title: executable, + description: ( + + {processDetail.executable.map((execTuple, idx) => { + const [executable, eventAction] = execTuple; + return ( +
+ + {executable} + + + {eventAction} + +
+ ); + })} +
+ ), + }, + { + title: process.pid, + description: ( + + + {dataOrDash(processDetail.pid)} + + + ), + }, + ]} + /> + {leaderListItems.map((leader) => ( + + ))} + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_process_tab/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/styles.ts new file mode 100644 index 00000000000000..8c1154f0c0076f --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_process_tab/styles.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const description: CSSObject = { + width: `calc(100% - ${euiTheme.size.xl})`, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }; + + const descriptionSemibold: CSSObject = { + ...description, + fontWeight: euiTheme.font.weight.medium, + }; + + const executableAction: CSSObject = { + fontWeight: euiTheme.font.weight.semiBold, + paddingLeft: euiTheme.size.xs, + }; + + return { + description, + descriptionSemibold, + executableAction, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts new file mode 100644 index 00000000000000..9092009a7d291c --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + mockData, + mockProcessMap, +} from '../../../common/mocks/constants/session_view_process.mock'; +import { Process, ProcessMap } from '../../../common/types/process_tree'; +import { + updateProcessMap, + buildProcessTree, + searchProcessTree, + autoExpandProcessTree, +} from './helpers'; + +const SESSION_ENTITY_ID = '3d0192c6-7c54-5ee6-a110-3539a7cf42bc'; +const SEARCH_QUERY = 'vi'; +const SEARCH_RESULT_PROCESS_ID = '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3727'; + +const mockEvents = mockData[0].events; + +describe('process tree hook helpers tests', () => { + let processMap: ProcessMap; + + beforeEach(() => { + processMap = {}; + }); + + it('updateProcessMap works', () => { + processMap = updateProcessMap(processMap, mockEvents); + + // processes are added to processMap + mockEvents.forEach((event) => { + expect(processMap[event.process.entity_id]).toBeTruthy(); + }); + }); + + it('buildProcessTree works', () => { + const newOrphans = buildProcessTree(mockProcessMap, mockEvents, [], SESSION_ENTITY_ID); + + const sessionLeaderChildrenIds = new Set( + mockProcessMap[SESSION_ENTITY_ID].children.map((child: Process) => child.id) + ); + + // processes are added under their parent's childrean array in processMap + mockEvents.forEach((event) => { + expect(sessionLeaderChildrenIds.has(event.process.entity_id)); + }); + + expect(newOrphans.length).toBe(0); + }); + + it('searchProcessTree works', () => { + const searchResults = searchProcessTree(mockProcessMap, SEARCH_QUERY); + + // search returns the process with search query in its event args + expect(searchResults[0].id).toBe(SEARCH_RESULT_PROCESS_ID); + }); + + it('autoExpandProcessTree works', () => { + processMap = mockProcessMap; + // mock what buildProcessTree does + const childProcesses = Object.values(processMap).filter( + (process) => process.id !== SESSION_ENTITY_ID + ); + processMap[SESSION_ENTITY_ID].children = childProcesses; + + expect(processMap[SESSION_ENTITY_ID].autoExpand).toBeFalsy(); + processMap = autoExpandProcessTree(processMap); + // session leader should have autoExpand to be true + expect(processMap[SESSION_ENTITY_ID].autoExpand).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts new file mode 100644 index 00000000000000..d3d7af1c62eda9 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Process, ProcessEvent, ProcessMap } from '../../../common/types/process_tree'; +import { ProcessImpl } from './hooks'; + +// given a page of new events, add these events to the appropriate process class model +// create a new process if none are created and return the mutated processMap +export const updateProcessMap = (processMap: ProcessMap, events: ProcessEvent[]) => { + events.forEach((event) => { + const { entity_id: id } = event.process; + let process = processMap[id]; + + if (!process) { + process = new ProcessImpl(id); + processMap[id] = process; + } + + process.addEvent(event); + }); + + return processMap; +}; + +// given a page of events, update process model parent child relationships +// if we cannot find a parent for a process include said process +// in the array of orphans. We track orphans in their own array, so +// we can attempt to re-parent the orphans when new pages of events are +// processed. This is especially important when paginating backwards +// (e.g in the case where the SessionView jumpToEvent prop is used, potentially skipping over ancestor processes) +export const buildProcessTree = ( + processMap: ProcessMap, + events: ProcessEvent[], + orphans: Process[], + sessionEntityId: string, + backwardDirection: boolean = false +) => { + // we process events in reverse order when paginating backwards. + if (backwardDirection) { + events = events.slice().reverse(); + } + + events.forEach((event) => { + const process = processMap[event.process.entity_id]; + const parentProcess = processMap[event.process.parent?.entity_id]; + + // if session leader, or process already has a parent, return + if (process.id === sessionEntityId || process.parent) { + return; + } + + if (parentProcess) { + process.parent = parentProcess; // handy for recursive operations (like auto expand) + + if (backwardDirection) { + parentProcess.children.unshift(process); + } else { + parentProcess.children.push(process); + } + } else if (!orphans?.includes(process)) { + // if no parent process, process is probably orphaned + if (backwardDirection) { + orphans?.unshift(process); + } else { + orphans?.push(process); + } + } + }); + + const newOrphans: Process[] = []; + + // with this new page of events processed, lets try re-parent any orphans + orphans?.forEach((process) => { + const parentProcess = processMap[process.getDetails().process.parent.entity_id]; + + if (parentProcess) { + process.parent = parentProcess; // handy for recursive operations (like auto expand) + + parentProcess.children.push(process); + } else { + newOrphans.push(process); + } + }); + + return newOrphans; +}; + +// given a plain text searchQuery, iterates over all processes in processMap +// and marks ones which match the below text (currently what is rendered in the process line item) +// process.searchMatched is used by process_tree_node to highlight the text which matched the search +// this funtion also returns a list of process results which is used by session_view_search_bar to drive +// result navigation UX +// FYI: this function mutates properties of models contained in processMap +export const searchProcessTree = (processMap: ProcessMap, searchQuery: string | undefined) => { + const results = []; + + for (const processId of Object.keys(processMap)) { + const process = processMap[processId]; + + if (searchQuery) { + const event = process.getDetails(); + const { working_directory: workingDirectory, args } = event.process; + + // TODO: the text we search is the same as what we render. + // in future we may support KQL searches to match against any property + // for now plain text search is limited to searching process.working_directory + process.args + const text = `${workingDirectory} ${args?.join(' ')}`; + + process.searchMatched = text.includes(searchQuery) ? searchQuery : null; + + if (process.searchMatched) { + results.push(process); + } + } else { + process.clearSearch(); + } + } + + return results; +}; + +// Iterate over all processes in processMap, and mark each process (and it's ancestors) for auto expansion if: +// a) the process was "user entered" (aka an interactive group leader) +// b) matches the plain text search above +// Returns the processMap with it's processes autoExpand bool set to true or false +// process.autoExpand is read by process_tree_node to determine whether to auto expand it's child processes. +export const autoExpandProcessTree = (processMap: ProcessMap) => { + for (const processId of Object.keys(processMap)) { + const process = processMap[processId]; + + if (process.searchMatched || process.isUserEntered()) { + let { parent } = process; + const parentIdSet = new Set(); + + while (parent && !parentIdSet.has(parent.id)) { + parentIdSet.add(parent.id); + parent.autoExpand = true; + parent = parent.parent; + } + } + } + + return processMap; +}; + +export const processNewEvents = ( + eventsProcessMap: ProcessMap, + events: ProcessEvent[] | undefined, + orphans: Process[], + sessionEntityId: string, + backwardDirection: boolean = false +): [ProcessMap, Process[]] => { + if (!events || events.length === 0) { + return [eventsProcessMap, orphans]; + } + + const updatedProcessMap = updateProcessMap(eventsProcessMap, events); + const newOrphans = buildProcessTree( + updatedProcessMap, + events, + orphans, + sessionEntityId, + backwardDirection + ); + + return [autoExpandProcessTree(updatedProcessMap), newOrphans]; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/hooks.test.tsx new file mode 100644 index 00000000000000..9cece96fe84670 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EventAction } from '../../../common/types/process_tree'; +import { mockEvents } from '../../../common/mocks/constants/session_view_process.mock'; +import { ProcessImpl } from './hooks'; + +describe('ProcessTree hooks', () => { + describe('ProcessImpl.getDetails memoize will cache bust on new events', () => { + it('should return the exec event details when this.events changes', () => { + const process = new ProcessImpl(mockEvents[0].process.entity_id); + + process.addEvent(mockEvents[0]); + + let result = process.getDetails(); + + // push exec event + process.addEvent(mockEvents[1]); + + result = process.getDetails(); + + expect(result.event.action).toEqual(EventAction.exec); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts new file mode 100644 index 00000000000000..a8c6ffe8e75d3a --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import _ from 'lodash'; +import memoizeOne from 'memoize-one'; +import { useState, useEffect } from 'react'; +import { + EventAction, + EventKind, + Process, + ProcessEvent, + ProcessMap, + ProcessEventsPage, +} from '../../../common/types/process_tree'; +import { processNewEvents, searchProcessTree, autoExpandProcessTree } from './helpers'; +import { sortProcesses } from '../../../common/utils/sort_processes'; + +interface UseProcessTreeDeps { + sessionEntityId: string; + data: ProcessEventsPage[]; + searchQuery?: string; +} + +export class ProcessImpl implements Process { + id: string; + events: ProcessEvent[]; + children: Process[]; + parent: Process | undefined; + autoExpand: boolean; + searchMatched: string | null; + orphans: Process[]; + + constructor(id: string) { + this.id = id; + this.events = []; + this.children = []; + this.orphans = []; + this.autoExpand = false; + this.searchMatched = null; + } + + addEvent(event: ProcessEvent) { + // rather than push new events on the array, we return a new one + // this helps the below memoizeOne functions to behave correctly. + this.events = this.events.concat(event); + } + + clearSearch() { + this.searchMatched = null; + this.autoExpand = false; + } + + getChildren(verboseMode: boolean) { + let children = this.children; + + // if there are orphans, we just render them inline with the other child processes (currently only session leader does this) + if (this.orphans.length) { + children = [...children, ...this.orphans].sort(sortProcesses); + } + + // When verboseMode is false, we filter out noise via a few techniques. + // This option is driven by the "verbose mode" toggle in SessionView/index.tsx + if (!verboseMode) { + return children.filter((child) => { + const { group_leader: groupLeader, session_leader: sessionLeader } = + child.getDetails().process; + + // search matches will never be filtered out + if (child.searchMatched) { + return true; + } + + // Hide processes that have their session leader as their process group leader. + // This accounts for a lot of noise from bash and other shells forking, running auto completion processes and + // other shell startup activities (e.g bashrc .profile etc) + if (groupLeader.pid === sessionLeader.pid) { + return false; + } + + // If the process has no children and has not exec'd (fork only), we hide it. + if (child.children.length === 0 && !child.hasExec()) { + return false; + } + + return true; + }); + } + + return children; + } + + hasOutput() { + return !!this.findEventByAction(this.events, EventAction.output); + } + + hasAlerts() { + return !!this.findEventByKind(this.events, EventKind.signal); + } + + getAlerts() { + return this.filterEventsByKind(this.events, EventKind.signal); + } + + hasExec() { + return !!this.findEventByAction(this.events, EventAction.exec); + } + + hasExited() { + return !!this.findEventByAction(this.events, EventAction.end); + } + + getDetails() { + return this.getDetailsMemo(this.events); + } + + getOutput() { + // not implemented, output ECS schema not defined (for a future release) + return ''; + } + + // isUserEntered is a best guess at which processes were initiated by a real person + // In most situations a user entered command in a shell such as bash, will cause bash + // to fork, create a new process group, and exec the command (e.g ls). If the session + // has a controlling tty (aka an interactive session), we assume process group leaders + // with a session leader for a parent are "user entered". + // Because of the presence of false positives in this calculation, it is currently + // only used to auto expand parts of the tree that could be of interest. + isUserEntered() { + const event = this.getDetails(); + const { + pid, + tty, + parent, + session_leader: sessionLeader, + group_leader: groupLeader, + } = event.process; + + const parentIsASessionLeader = parent.pid === sessionLeader.pid; // possibly bash, zsh or some other shell + const processIsAGroupLeader = pid === groupLeader.pid; + const sessionIsInteractive = !!tty; + + return sessionIsInteractive && parentIsASessionLeader && processIsAGroupLeader; + } + + getMaxAlertLevel() { + // TODO: as part of alerts details work + tie in with the new alert flyout + return null; + } + + findEventByAction = memoizeOne((events: ProcessEvent[], action: EventAction) => { + return events.find(({ event }) => event.action === action); + }); + + findEventByKind = memoizeOne((events: ProcessEvent[], kind: EventKind) => { + return events.find(({ event }) => event.kind === kind); + }); + + filterEventsByAction = memoizeOne((events: ProcessEvent[], action: EventAction) => { + return events.filter(({ event }) => event.action === action); + }); + + filterEventsByKind = memoizeOne((events: ProcessEvent[], kind: EventKind) => { + return events.filter(({ event }) => event.kind === kind); + }); + + // returns the most recent fork, exec, or end event + // to be used as a source for the most up to date details + // on the processes lifecycle. + getDetailsMemo = memoizeOne((events: ProcessEvent[]) => { + const actionsToFind = [EventAction.fork, EventAction.exec, EventAction.end]; + const filtered = events.filter((processEvent) => { + return actionsToFind.includes(processEvent.event.action); + }); + + // because events is already ordered by @timestamp we take the last event + // which could be a fork (w no exec or exit), most recent exec event (there can be multiple), or end event. + // If a process has an 'end' event will always be returned (since it is last and includes details like exit_code and end time) + return filtered[filtered.length - 1] || ({} as ProcessEvent); + }); +} + +export const useProcessTree = ({ sessionEntityId, data, searchQuery }: UseProcessTreeDeps) => { + // initialize map, as well as a placeholder for session leader process + // we add a fake session leader event, sourced from wide event data. + // this is because we might not always have a session leader event + // especially if we are paging in reverse from deep within a large session + const fakeLeaderEvent = data[0].events.find((event) => event.event.kind === EventKind.event); + const sessionLeaderProcess = new ProcessImpl(sessionEntityId); + + if (fakeLeaderEvent) { + fakeLeaderEvent.process = { + ...fakeLeaderEvent.process, + ...fakeLeaderEvent.process.entry_leader, + parent: fakeLeaderEvent.process.parent, + }; + sessionLeaderProcess.events.push(fakeLeaderEvent); + } + + const initializedProcessMap: ProcessMap = { + [sessionEntityId]: sessionLeaderProcess, + }; + + const [processMap, setProcessMap] = useState(initializedProcessMap); + const [processedPages, setProcessedPages] = useState([]); + const [searchResults, setSearchResults] = useState([]); + const [orphans, setOrphans] = useState([]); + + useEffect(() => { + let updatedProcessMap: ProcessMap = processMap; + let newOrphans: Process[] = orphans; + const newProcessedPages: ProcessEventsPage[] = []; + + data.forEach((page, i) => { + const processed = processedPages.find((p) => p.cursor === page.cursor); + + if (!processed) { + const backwards = i < processedPages.length; + + const result = processNewEvents( + updatedProcessMap, + page.events, + orphans, + sessionEntityId, + backwards + ); + + updatedProcessMap = result[0]; + newOrphans = result[1]; + + newProcessedPages.push(page); + } + }); + + if (newProcessedPages.length > 0) { + setProcessMap({ ...updatedProcessMap }); + setProcessedPages([...processedPages, ...newProcessedPages]); + setOrphans(newOrphans); + } + }, [data, processMap, orphans, processedPages, sessionEntityId]); + + useEffect(() => { + setSearchResults(searchProcessTree(processMap, searchQuery)); + autoExpandProcessTree(processMap); + }, [searchQuery, processMap]); + + // set new orphans array on the session leader + const sessionLeader = processMap[sessionEntityId]; + + sessionLeader.orphans = orphans; + + return { sessionLeader: processMap[sessionEntityId], processMap, searchResults }; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx new file mode 100644 index 00000000000000..ac6807984ba831 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mockData } from '../../../common/mocks/constants/session_view_process.mock'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { ProcessImpl } from './hooks'; +import { ProcessTree } from './index'; + +describe('ProcessTree component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When ProcessTree is mounted', () => { + it('should render given a valid sessionEntityId and data', () => { + renderResult = mockedContext.render( + true} + hasNextPage={false} + fetchPreviousPage={() => true} + hasPreviousPage={false} + onProcessSelected={jest.fn()} + /> + ); + expect(renderResult.queryByTestId('sessionView:sessionViewProcessTree')).toBeTruthy(); + expect(renderResult.queryAllByTestId('sessionView:processTreeNode')).toBeTruthy(); + }); + + it('should insert a DOM element used to highlight a process when selectedProcess is set', () => { + const mockSelectedProcess = new ProcessImpl(mockData[0].events[0].process.entity_id); + + renderResult = mockedContext.render( + true} + hasNextPage={false} + fetchPreviousPage={() => true} + hasPreviousPage={false} + selectedProcess={mockSelectedProcess} + onProcessSelected={jest.fn()} + /> + ); + + // click on view more button + renderResult.getByTestId('sessionView:processTreeNodeChildProcessesButton').click(); + + expect( + renderResult + .queryByTestId('sessionView:processTreeSelectionArea') + ?.parentElement?.getAttribute('data-id') + ).toEqual(mockSelectedProcess.id); + + // change the selected process + const mockSelectedProcess2 = new ProcessImpl(mockData[0].events[1].process.entity_id); + + renderResult.rerender( + true} + hasNextPage={false} + fetchPreviousPage={() => true} + hasPreviousPage={false} + selectedProcess={mockSelectedProcess2} + onProcessSelected={jest.fn()} + /> + ); + + expect( + renderResult + .queryByTestId('sessionView:processTreeSelectionArea') + ?.parentElement?.getAttribute('data-id') + ).toEqual(mockSelectedProcess2.id); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx new file mode 100644 index 00000000000000..6b3061a0d77bb3 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useRef, useEffect, useLayoutEffect, useCallback } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { ProcessTreeNode } from '../process_tree_node'; +import { useProcessTree } from './hooks'; +import { Process, ProcessEventsPage, ProcessEvent } from '../../../common/types/process_tree'; +import { useScroll } from '../../hooks/use_scroll'; +import { useStyles } from './styles'; + +type FetchFunction = () => void; + +interface ProcessTreeDeps { + // process.entity_id to act as root node (typically a session (or entry session) leader). + sessionEntityId: string; + + data: ProcessEventsPage[]; + + jumpToEvent?: ProcessEvent; + isFetching: boolean; + hasNextPage: boolean | undefined; + hasPreviousPage: boolean | undefined; + fetchNextPage: FetchFunction; + fetchPreviousPage: FetchFunction; + + // plain text search query (only searches "process.working_directory process.args.join(' ')" + searchQuery?: string; + + // currently selected process + selectedProcess?: Process | null; + onProcessSelected: (process: Process) => void; + setSearchResults?: (results: Process[]) => void; +} + +export const ProcessTree = ({ + sessionEntityId, + data, + jumpToEvent, + isFetching, + hasNextPage, + hasPreviousPage, + fetchNextPage, + fetchPreviousPage, + searchQuery, + selectedProcess, + onProcessSelected, + setSearchResults, +}: ProcessTreeDeps) => { + const styles = useStyles(); + + const { sessionLeader, processMap, searchResults } = useProcessTree({ + sessionEntityId, + data, + searchQuery, + }); + + const scrollerRef = useRef(null); + const selectionAreaRef = useRef(null); + + useEffect(() => { + if (setSearchResults) { + setSearchResults(searchResults); + } + }, [searchResults, setSearchResults]); + + useScroll({ + div: scrollerRef.current, + handler: (pos: number, endReached: boolean) => { + if (!isFetching && endReached) { + fetchNextPage(); + } + }, + }); + + /** + * highlights a process in the tree + * we do it this way to avoid state changes on potentially thousands of components + */ + const selectProcess = useCallback( + (process: Process) => { + if (!selectionAreaRef?.current || !scrollerRef?.current) { + return; + } + + const selectionAreaEl = selectionAreaRef.current; + selectionAreaEl.style.display = 'block'; + + // TODO: concept of alert level unknown wrt to elastic security + const alertLevel = process.getMaxAlertLevel(); + + if (alertLevel && alertLevel >= 0) { + selectionAreaEl.style.backgroundColor = + alertLevel > 0 ? styles.alertSelected : styles.defaultSelected; + } else { + selectionAreaEl.style.backgroundColor = ''; + } + + // find the DOM element for the command which is selected by id + const processEl = scrollerRef.current.querySelector(`[data-id="${process.id}"]`); + + if (processEl) { + processEl.prepend(selectionAreaEl); + + const cTop = scrollerRef.current.scrollTop; + const cBottom = cTop + scrollerRef.current.clientHeight; + + const eTop = processEl.offsetTop; + const eBottom = eTop + processEl.clientHeight; + const isVisible = eTop >= cTop && eBottom <= cBottom; + + if (!isVisible) { + processEl.scrollIntoView({ block: 'center' }); + } + } + }, + [styles.alertSelected, styles.defaultSelected] + ); + + useLayoutEffect(() => { + if (selectedProcess) { + selectProcess(selectedProcess); + } + }, [selectedProcess, selectProcess]); + + useEffect(() => { + // after 2 pages are loaded (due to bi-directional jump to), auto select the process + // for the jumpToEvent + if (jumpToEvent && data.length === 2) { + const process = processMap[jumpToEvent.process.entity_id]; + + if (process) { + onProcessSelected(process); + } + } + }, [jumpToEvent, processMap, onProcessSelected, data]); + + // auto selects the session leader process if no selection is made yet + useEffect(() => { + if (!selectedProcess) { + onProcessSelected(sessionLeader); + } + }, [sessionLeader, onProcessSelected, selectedProcess]); + + return ( +
+ {hasPreviousPage && ( + + + + )} + {sessionLeader && ( + + )} +
+ {hasNextPage && ( + + + + )} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree/styles.ts b/x-pack/plugins/session_view/public/components/process_tree/styles.ts new file mode 100644 index 00000000000000..65fb66ad90aa7c --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree/styles.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { transparentize, useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const defaultSelectionColor = euiTheme.colors.accent; + + const scroller: CSSObject = { + position: 'relative', + fontFamily: euiTheme.font.familyCode, + overflow: 'auto', + height: '100%', + backgroundColor: euiTheme.colors.lightestShade, + }; + + const selectionArea: CSSObject = { + position: 'absolute', + display: 'none', + marginLeft: '-50%', + width: '150%', + height: '100%', + backgroundColor: defaultSelectionColor, + pointerEvents: 'none', + opacity: 0.1, + }; + + const defaultSelected = transparentize(euiTheme.colors.primary, 0.008); + const alertSelected = transparentize(euiTheme.colors.danger, 0.008); + + return { + scroller, + selectionArea, + defaultSelected, + alertSelected, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx new file mode 100644 index 00000000000000..618b36578d7dae --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { ProcessTreeAlerts } from './index'; + +describe('ProcessTreeAlerts component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When ProcessTreeAlerts is mounted', () => { + it('should return null if no alerts', async () => { + renderResult = mockedContext.render(); + + expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeNull(); + }); + + it('should return an array of alert details', async () => { + renderResult = mockedContext.render(); + + expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy(); + mockAlerts.forEach((alert) => { + if (!alert.kibana) { + return; + } + const { uuid, rule, original_event: event, workflow_status: status } = alert.kibana.alert; + const { name, query, severity } = rule; + + expect( + renderResult.queryByTestId(`sessionView:sessionViewAlertDetail-${uuid}`) + ).toBeTruthy(); + expect( + renderResult.queryByTestId(`sessionView:sessionViewAlertDetailViewRule-${uuid}`) + ).toBeTruthy(); + expect(renderResult.queryAllByText(new RegExp(event.action, 'i')).length).toBeTruthy(); + expect(renderResult.queryAllByText(new RegExp(status, 'i')).length).toBeTruthy(); + expect(renderResult.queryAllByText(new RegExp(name, 'i')).length).toBeTruthy(); + expect(renderResult.queryAllByText(new RegExp(query, 'i')).length).toBeTruthy(); + expect(renderResult.queryAllByText(new RegExp(severity, 'i')).length).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx new file mode 100644 index 00000000000000..5312c09867b96e --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useStyles } from './styles'; +import { ProcessEvent } from '../../../common/types/process_tree'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { CoreStart } from '../../../../../../src/core/public'; + +interface ProcessTreeAlertsDeps { + alerts: ProcessEvent[]; +} + +const getRuleUrl = (alert: ProcessEvent, http: CoreStart['http']) => { + return http.basePath.prepend(`/app/security/rules/id/${alert.kibana?.alert.rule.uuid}`); +}; + +const ProcessTreeAlert = ({ alert }: { alert: ProcessEvent }) => { + const { http } = useKibana().services; + + if (!alert.kibana) { + return null; + } + + const { uuid, rule, original_event: event, workflow_status: status } = alert.kibana.alert; + const { name, query, severity } = rule; + + return ( + + + +
+ +
+ {name} +
+ +
+ {query} +
+ +
+ +
+ {severity} +
+ +
+ {status} +
+ +
+ +
+ {event.action} + +
+ + + +
+
+
+
+ ); +}; + +export function ProcessTreeAlerts({ alerts }: ProcessTreeAlertsDeps) { + const styles = useStyles(); + + if (alerts.length === 0) { + return null; + } + + return ( +
+ {alerts.map((alert: ProcessEvent) => ( + + ))} +
+ ); +} diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts new file mode 100644 index 00000000000000..d601891591305b --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { size, colors, border } = euiTheme; + + const container: CSSObject = { + marginTop: size.s, + marginRight: size.s, + color: colors.text, + padding: size.m, + borderStyle: 'solid', + borderColor: colors.lightShade, + borderWidth: border.width.thin, + borderRadius: border.radius.medium, + maxWidth: 800, + backgroundColor: 'white', + '&>div': { + borderTop: border.thin, + marginTop: size.m, + paddingTop: size.m, + '&:first-child': { + borderTop: 'none', + }, + }, + }; + + return { + container, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx new file mode 100644 index 00000000000000..16cb9461746916 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiButton, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { Process } from '../../../common/types/process_tree'; +import { useButtonStyles } from './use_button_styles'; + +export const ChildrenProcessesButton = ({ + onToggle, + isExpanded, +}: { + onToggle: () => void; + isExpanded: boolean; +}) => { + const { button, buttonArrow, getExpandedIcon } = useButtonStyles(); + + return ( + + + + + ); +}; + +export const SessionLeaderButton = ({ + process, + onClick, + showGroupLeadersOnly, + childCount, +}: { + process: Process; + onClick: () => void; + showGroupLeadersOnly: boolean; + childCount: number; +}) => { + const groupLeaderCount = process.getChildren(false).length; + const sameGroupCount = childCount - groupLeaderCount; + const { button, buttonArrow, getExpandedIcon } = useButtonStyles(); + + if (sameGroupCount > 0) { + return ( + + +

+ } + > + + + + +
+ ); + } + return null; +}; + +export const AlertButton = ({ + isExpanded, + onToggle, +}: { + isExpanded: boolean; + onToggle: () => void; +}) => { + const { alertButton, buttonArrow, getExpandedIcon } = useButtonStyles(); + + return ( + + + + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx new file mode 100644 index 00000000000000..2a3bf94086021b --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { + processMock, + childProcessMock, + sessionViewAlertProcessMock, +} from '../../../common/mocks/constants/session_view_process.mock'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { ProcessTreeNode } from './index'; + +describe('ProcessTreeNode component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When ProcessTreeNode is mounted', () => { + it('should render given a valid process', async () => { + renderResult = mockedContext.render(); + + expect(renderResult.queryByTestId('sessionView:processTreeNode')).toBeTruthy(); + }); + + it('should have an alternate rendering for a session leader', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.container.textContent).toEqual(' bash started by vagrant'); + }); + + // commented out until we get new UX for orphans treatment aka disjointed tree + // it('renders orphaned node', async () => { + // renderResult = mockedContext.render(); + // expect(renderResult.queryByText(/orphaned/i)).toBeTruthy(); + // }); + + it('renders Exec icon and exit code for executed process', async () => { + const executedProcessMock: typeof processMock = { + ...processMock, + hasExec: () => true, + }; + + renderResult = mockedContext.render(); + + expect(renderResult.queryByTestId('sessionView:processTreeNodeExecIcon')).toBeTruthy(); + expect(renderResult.queryByTestId('sessionView:processTreeNodeExitCode')).toBeTruthy(); + }); + + it('does not render exit code if it does not exist', async () => { + const processWithoutExitCode: typeof processMock = { + ...processMock, + hasExec: () => true, + getDetails: () => ({ + ...processMock.getDetails(), + process: { + ...processMock.getDetails().process, + exit_code: undefined, + }, + }), + }; + + renderResult = mockedContext.render(); + expect(renderResult.queryByTestId('sessionView:processTreeNodeExitCode')).toBeFalsy(); + }); + + it('renders Root Escalation flag properly', async () => { + const rootEscalationProcessMock: typeof processMock = { + ...processMock, + getDetails: () => ({ + ...processMock.getDetails(), + user: { + id: '-1', + name: 'root', + }, + process: { + ...processMock.getDetails().process, + parent: { + ...processMock.getDetails().process.parent, + user: { + name: 'test', + id: '1000', + }, + }, + }, + }), + }; + + renderResult = mockedContext.render(); + + expect( + renderResult.queryByTestId('sessionView:processTreeNodeRootEscalationFlag') + ).toBeTruthy(); + }); + + it('executes callback function when user Clicks', async () => { + const onProcessSelected = jest.fn(); + + renderResult = mockedContext.render( + + ); + + userEvent.click(renderResult.getByTestId('sessionView:processTreeNodeRow')); + expect(onProcessSelected).toHaveBeenCalled(); + }); + + it('does not executes callback function when user is Clicking to copy text', async () => { + const windowGetSelectionSpy = jest.spyOn(window, 'getSelection'); + + const onProcessSelected = jest.fn(); + + renderResult = mockedContext.render( + + ); + + // @ts-ignore + windowGetSelectionSpy.mockImplementation(() => ({ type: 'Range' })); + + userEvent.click(renderResult.getByTestId('sessionView:processTreeNodeRow')); + expect(onProcessSelected).not.toHaveBeenCalled(); + + // cleanup + windowGetSelectionSpy.mockRestore(); + }); + describe('Alerts', () => { + it('renders Alert button when process has alerts', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId('processTreeNodeAlertButton')).toBeTruthy(); + }); + it('toggle Alert Details button when Alert button is clicked', async () => { + renderResult = mockedContext.render( + + ); + userEvent.click(renderResult.getByTestId('processTreeNodeAlertButton')); + expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy(); + userEvent.click(renderResult.getByTestId('processTreeNodeAlertButton')); + expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeFalsy(); + }); + }); + describe('Child processes', () => { + it('renders Child processes button when process has Child processes', async () => { + const processMockWithChildren: typeof processMock = { + ...processMock, + getChildren: () => [childProcessMock], + }; + + renderResult = mockedContext.render(); + + expect( + renderResult.queryByTestId('sessionView:processTreeNodeChildProcessesButton') + ).toBeTruthy(); + }); + it('toggle Child processes nodes when Child processes button is clicked', async () => { + const processMockWithChildren: typeof processMock = { + ...processMock, + getChildren: () => [childProcessMock], + }; + + renderResult = mockedContext.render(); + + expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toHaveLength(1); + + userEvent.click( + renderResult.getByTestId('sessionView:processTreeNodeChildProcessesButton') + ); + expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toHaveLength(2); + + userEvent.click( + renderResult.getByTestId('sessionView:processTreeNodeChildProcessesButton') + ); + expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toHaveLength(1); + }); + }); + describe('Search', () => { + it('highlights text within the process node line item if it matches the searchQuery', () => { + // set a mock search matched indicator for the process (typically done by ProcessTree/helpers.ts) + processMock.searchMatched = '/vagrant'; + + renderResult = mockedContext.render(); + + expect( + renderResult.getByTestId('sessionView:processNodeSearchHighlight').textContent + ).toEqual('/vagrant'); + }); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx new file mode 100644 index 00000000000000..9db83f58f77382 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { + useRef, + useLayoutEffect, + useState, + useEffect, + MouseEvent, + useCallback, +} from 'react'; +import { EuiButton, EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { Process } from '../../../common/types/process_tree'; +import { useStyles } from './styles'; +import { ProcessTreeAlerts } from '../process_tree_alerts'; +import { SessionLeaderButton, AlertButton, ChildrenProcessesButton } from './buttons'; +import { useButtonStyles } from './use_button_styles'; +interface ProcessDeps { + process: Process; + isSessionLeader?: boolean; + depth?: number; + onProcessSelected?: (process: Process) => void; +} + +/** + * Renders a node on the process tree + */ +export function ProcessTreeNode({ + process, + isSessionLeader = false, + depth = 0, + onProcessSelected, +}: ProcessDeps) { + const textRef = useRef(null); + + const [childrenExpanded, setChildrenExpanded] = useState(isSessionLeader || process.autoExpand); + const [alertsExpanded, setAlertsExpanded] = useState(false); + const [showGroupLeadersOnly, setShowGroupLeadersOnly] = useState(isSessionLeader); + const { searchMatched } = process; + + useEffect(() => { + setChildrenExpanded(isSessionLeader || process.autoExpand); + }, [isSessionLeader, process.autoExpand]); + + const alerts = process.getAlerts(); + const styles = useStyles({ depth, hasAlerts: !!alerts.length }); + const buttonStyles = useButtonStyles(); + + useLayoutEffect(() => { + if (searchMatched !== null && textRef.current) { + const regex = new RegExp(searchMatched); + const text = textRef.current.textContent; + + if (text) { + const html = text.replace(regex, (match) => { + return `${match}`; + }); + + // eslint-disable-next-line no-unsanitized/property + textRef.current.innerHTML = html; + } + } + }, [searchMatched, styles.searchHighlight]); + + const onShowGroupLeaderOnlyClick = useCallback(() => { + setShowGroupLeadersOnly(!showGroupLeadersOnly); + }, [showGroupLeadersOnly]); + + const onChildrenToggle = useCallback(() => { + setChildrenExpanded(!childrenExpanded); + }, [childrenExpanded]); + + const onAlertsToggle = useCallback(() => { + setAlertsExpanded(!alertsExpanded); + }, [alertsExpanded]); + + const onProcessClicked = (e: MouseEvent) => { + e.stopPropagation(); + + const selection = window.getSelection(); + + // do not select the command if the user was just selecting text for copy. + if (selection && selection.type === 'Range') { + return; + } + + onProcessSelected?.(process); + }; + + const processDetails = process.getDetails(); + + if (!processDetails) { + return null; + } + + const id = process.id; + const { user } = processDetails; + const { + args, + name, + tty, + parent, + working_directory: workingDirectory, + exit_code: exitCode, + } = processDetails.process; + + const children = process.getChildren(!showGroupLeadersOnly); + const childCount = process.getChildren(true).length; + const shouldRenderChildren = childrenExpanded && children && children.length > 0; + const childrenTreeDepth = depth + 1; + + const showRootEscalation = user.name === 'root' && user.id !== parent.user.id; + const interactiveSession = !!tty; + const sessionIcon = interactiveSession ? 'consoleApp' : 'compute'; + const hasExec = process.hasExec(); + const iconTestSubj = hasExec + ? 'sessionView:processTreeNodeExecIcon' + : 'sessionView:processTreeNodeForkIcon'; + const processIcon = hasExec ? 'console' : 'branch'; + + return ( +
+
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
+ {isSessionLeader ? ( + <> + {name || args[0]}{' '} + {' '} + {user.name} + + + ) : ( + + + + {workingDirectory}  + {args[0]}  + {args.slice(1).join(' ')} + {exitCode !== undefined && ( + + {' '} + [exit_code: {exitCode}] + + )} + + + )} + + {showRootEscalation && ( + + + + )} + {!isSessionLeader && childCount > 0 && ( + + )} + {alerts.length > 0 && ( + + )} +
+
+ + {alertsExpanded && } + + {shouldRenderChildren && ( +
+ {children.map((child) => { + return ( + + ); + })} +
+ )} +
+ ); +} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts new file mode 100644 index 00000000000000..07092d6de28ead --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +interface StylesDeps { + depth: number; + hasAlerts: boolean; +} + +export const useStyles = ({ depth, hasAlerts }: StylesDeps) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, border, size } = euiTheme; + + const TREE_INDENT = euiTheme.base * 2; + + const darkText: CSSObject = { + color: colors.text, + }; + + const searchHighlight = ` + background-color: ${colors.highlight}; + color: ${colors.fullShade}; + border-radius: ${border.radius.medium}; + `; + + const children: CSSObject = { + position: 'relative', + color: colors.ghost, + marginLeft: size.base, + paddingLeft: size.s, + borderLeft: border.editable, + marginTop: size.s, + }; + + /** + * gets border, bg and hover colors for a process + */ + const getHighlightColors = () => { + let bgColor = 'none'; + const hoverColor = transparentize(colors.primary, 0.04); + let borderColor = 'transparent'; + + // TODO: alerts highlight colors + if (hasAlerts) { + bgColor = transparentize(colors.danger, 0.04); + borderColor = transparentize(colors.danger, 0.48); + } + + return { bgColor, borderColor, hoverColor }; + }; + + const { bgColor, borderColor, hoverColor } = getHighlightColors(); + + const processNode: CSSObject = { + display: 'block', + cursor: 'pointer', + position: 'relative', + margin: `${size.s} 0px`, + '&:not(:first-child)': { + marginTop: size.s, + }, + '&:hover:before': { + backgroundColor: hoverColor, + }, + '&:before': { + position: 'absolute', + height: '100%', + pointerEvents: 'none', + content: `''`, + marginLeft: `-${depth * TREE_INDENT}px`, + borderLeft: `${size.xs} solid ${borderColor}`, + backgroundColor: bgColor, + width: `calc(100% + ${depth * TREE_INDENT}px)`, + }, + }; + + const wrapper: CSSObject = { + paddingLeft: size.s, + position: 'relative', + verticalAlign: 'middle', + color: colors.mediumShade, + wordBreak: 'break-all', + minHeight: size.l, + lineHeight: size.l, + }; + + const workingDir: CSSObject = { + color: colors.successText, + }; + + const alertDetails: CSSObject = { + padding: size.s, + border: border.editable, + borderRadius: border.radius.medium, + }; + + return { + darkText, + searchHighlight, + children, + processNode, + wrapper, + workingDir, + alertDetails, + }; + }, [depth, euiTheme, hasAlerts]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts new file mode 100644 index 00000000000000..d208fa8f079af3 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { euiLightVars as theme } from '@kbn/ui-theme'; +import { CSSObject } from '@emotion/react'; + +export const useButtonStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, border, font, size } = euiTheme; + + const button: CSSObject = { + background: transparentize(theme.euiColorVis6, 0.04), + border: `${border.width.thin} solid ${transparentize(theme.euiColorVis6, 0.48)}`, + lineHeight: '18px', + height: '20px', + fontSize: '11px', + fontFamily: font.familyCode, + borderRadius: border.radius.medium, + color: colors.text, + marginLeft: size.s, + minWidth: 0, + }; + + const buttonArrow: CSSObject = { + marginLeft: size.s, + }; + + const alertButton: CSSObject = { + ...button, + background: transparentize(colors.dangerText, 0.04), + border: `${border.width.thin} solid ${transparentize(colors.dangerText, 0.48)}`, + }; + + const userChangedButton: CSSObject = { + ...button, + background: transparentize(theme.euiColorVis1, 0.04), + border: `${border.width.thin} solid ${transparentize(theme.euiColorVis1, 0.48)}`, + }; + + const getExpandedIcon = (expanded: boolean) => { + return expanded ? 'arrowUp' : 'arrowDown'; + }; + + return { + buttonArrow, + button, + alertButton, + userChangedButton, + getExpandedIcon, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts new file mode 100644 index 00000000000000..b93e5b43ddf884 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useEffect, useState } from 'react'; +import { useInfiniteQuery } from 'react-query'; +import { EuiSearchBarOnChangeArgs } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { ProcessEvent, ProcessEventResults } from '../../../common/types/process_tree'; +import { PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE } from '../../../common/constants'; + +export const useFetchSessionViewProcessEvents = ( + sessionEntityId: string, + jumpToEvent: ProcessEvent | undefined +) => { + const { http } = useKibana().services; + + const jumpToCursor = jumpToEvent && jumpToEvent['@timestamp']; + + const query = useInfiniteQuery( + 'sessionViewProcessEvents', + async ({ pageParam = {} }) => { + let { cursor } = pageParam; + const { forward } = pageParam; + + if (!cursor && jumpToCursor) { + cursor = jumpToCursor; + } + + const res = await http.get(PROCESS_EVENTS_ROUTE, { + query: { + sessionEntityId, + cursor, + forward, + }, + }); + + const events = res.events.map((event: any) => event._source as ProcessEvent); + + return { events, cursor }; + }, + { + getNextPageParam: (lastPage, pages) => { + if (lastPage.events.length === PROCESS_EVENTS_PER_PAGE) { + return { + cursor: lastPage.events[lastPage.events.length - 1]['@timestamp'], + forward: true, + }; + } + }, + getPreviousPageParam: (firstPage, pages) => { + if (jumpToEvent && firstPage.events.length === PROCESS_EVENTS_PER_PAGE) { + return { + cursor: firstPage.events[0]['@timestamp'], + forward: false, + }; + } + }, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + } + ); + + useEffect(() => { + if (jumpToEvent && query.data?.pages.length === 1) { + query.fetchPreviousPage(); + } + }, [jumpToEvent, query]); + + return query; +}; + +export const useSearchQuery = () => { + const [searchQuery, setSearchQuery] = useState(''); + const onSearch = ({ query }: EuiSearchBarOnChangeArgs) => { + if (query) { + setSearchQuery(query.text); + } else { + setSearchQuery(''); + } + }; + + return { + searchQuery, + onSearch, + }; +}; diff --git a/x-pack/plugins/session_view/public/components/session_view/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view/index.test.tsx new file mode 100644 index 00000000000000..41336977cf78a3 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view/index.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import React from 'react'; +import { sessionViewProcessEventsMock } from '../../../common/mocks/responses/session_view_process_events.mock'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { SessionView } from './index'; +import userEvent from '@testing-library/user-event'; + +describe('SessionView component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + let mockedApi: AppContextTestRender['coreStart']['http']['get']; + + const waitForApiCall = () => waitFor(() => expect(mockedApi).toHaveBeenCalled()); + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + mockedApi = mockedContext.coreStart.http.get; + render = () => + (renderResult = mockedContext.render()); + }); + + describe('When SessionView is mounted', () => { + describe('And no data exists', () => { + beforeEach(async () => { + mockedApi.mockResolvedValue({ + events: [], + }); + }); + + it('should show the Empty message', async () => { + render(); + await waitForApiCall(); + expect(renderResult.getByTestId('sessionView:sessionViewProcessEventsEmpty')).toBeTruthy(); + }); + + it('should not display the search bar', async () => { + render(); + await waitForApiCall(); + expect( + renderResult.queryByTestId('sessionView:sessionViewProcessEventsSearch') + ).toBeFalsy(); + }); + }); + + describe('And data exists', () => { + beforeEach(async () => { + mockedApi.mockResolvedValue(sessionViewProcessEventsMock); + }); + + it('should show loading indicator while retrieving data and hide it when it gets it', async () => { + let releaseApiResponse: (value?: unknown) => void; + + // make the request wait + mockedApi.mockReturnValue(new Promise((resolve) => (releaseApiResponse = resolve))); + render(); + await waitForApiCall(); + + // see if loader is present + expect(renderResult.getByText('Loading session…')).toBeTruthy(); + + // release the request + releaseApiResponse!(mockedApi); + + // check the loader is gone + await waitForElementToBeRemoved(renderResult.getByText('Loading session…')); + }); + + it('should display the search bar', async () => { + render(); + await waitForApiCall(); + expect(renderResult.getByTestId('sessionView:sessionViewProcessEventsSearch')).toBeTruthy(); + }); + + it('should show items on the list, and auto selects session leader', async () => { + render(); + await waitForApiCall(); + + expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toBeTruthy(); + + const selectionArea = renderResult.queryByTestId('sessionView:processTreeSelectionArea'); + + expect(selectionArea?.parentElement?.getAttribute('data-id')).toEqual('test-entity-id'); + }); + + it('should toggle detail panel visibilty when detail button clicked', async () => { + render(); + await waitForApiCall(); + + userEvent.click(renderResult.getByTestId('sessionViewDetailPanelToggle')); + expect(renderResult.getByText('Process')).toBeTruthy(); + expect(renderResult.getByText('Host')).toBeTruthy(); + expect(renderResult.getByText('Alerts')).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx new file mode 100644 index 00000000000000..7a82edc94ff1b1 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, useCallback } from 'react'; +import { + EuiEmptyPrompt, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiResizableContainer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { SectionLoading } from '../../shared_imports'; +import { ProcessTree } from '../process_tree'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { SessionViewDetailPanel } from '../session_view_detail_panel'; +import { SessionViewSearchBar } from '../session_view_search_bar'; +import { useStyles } from './styles'; +import { useFetchSessionViewProcessEvents } from './hooks'; + +interface SessionViewDeps { + // the root node of the process tree to render. e.g process.entry.entity_id or process.session_leader.entity_id + sessionEntityId: string; + height?: number; + jumpToEvent?: ProcessEvent; +} + +/** + * The main wrapper component for the session view. + */ +export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionViewDeps) => { + const [isDetailOpen, setIsDetailOpen] = useState(false); + const [selectedProcess, setSelectedProcess] = useState(null); + + const styles = useStyles({ height }); + + const onProcessSelected = useCallback((process: Process) => { + setSelectedProcess(process); + }, []); + + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState(null); + + const { + data, + error, + fetchNextPage, + hasNextPage, + isFetching, + fetchPreviousPage, + hasPreviousPage, + } = useFetchSessionViewProcessEvents(sessionEntityId, jumpToEvent); + + const hasData = data && data.pages.length > 0 && data.pages[0].events.length > 0; + const renderIsLoading = isFetching && !data; + const renderDetails = isDetailOpen && selectedProcess; + const toggleDetailPanel = () => { + setIsDetailOpen(!isDetailOpen); + }; + + if (!isFetching && !hasData) { + return ( + + +

+ } + body={ +

+ +

+ } + /> + ); + } + + return ( + <> + + + + + + + + + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + {renderIsLoading && ( + + + + )} + + {error && ( + + + + } + body={ +

+ +

+ } + /> + )} + + {hasData && ( +
+ +
+ )} +
+ + {renderDetails ? ( + <> + + + + + + ) : ( + <> + {/* Returning an empty element here (instead of false) to avoid a bug in EuiResizableContainer */} + + )} + + )} +
+ + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SessionView as default }; diff --git a/x-pack/plugins/session_view/public/components/session_view/styles.ts b/x-pack/plugins/session_view/public/components/session_view/styles.ts new file mode 100644 index 00000000000000..d7159ec5b1b399 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view/styles.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 { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +interface StylesDeps { + height: number | undefined; +} + +export const useStyles = ({ height = 500 }: StylesDeps) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const processTree: CSSObject = { + height: `${height}px`, + paddingTop: euiTheme.size.s, + }; + + const detailPanel: CSSObject = { + height: `${height}px`, + }; + + return { + processTree, + detailPanel, + }; + }, [height, euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts b/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts new file mode 100644 index 00000000000000..295371fbff96cf --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/helpers.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Process, ProcessFields } from '../../../common/types/process_tree'; +import { DetailPanelProcess, EuiTabProps } from '../../types'; + +const getDetailPanelProcessLeader = (leader: ProcessFields) => ({ + ...leader, + id: leader.entity_id, + entryMetaType: leader.entry_meta?.type || '', + userName: leader.user.name, + entryMetaSourceIp: leader.entry_meta?.source.ip || '', +}); + +export const getDetailPanelProcess = (process: Process) => { + const processData = {} as DetailPanelProcess; + + processData.id = process.id; + processData.start = process.events[0]['@timestamp']; + processData.end = process.events[process.events.length - 1]['@timestamp']; + const args = new Set(); + processData.executable = []; + + process.events.forEach((event) => { + if (!processData.user) { + processData.user = event.user.name; + } + if (!processData.pid) { + processData.pid = event.process.pid; + } + + if (event.process.args.length > 0) { + args.add(event.process.args.join(' ')); + } + if (event.process.executable) { + processData.executable.push([event.process.executable, `(${event.event.action})`]); + } + if (event.process.exit_code) { + processData.exit_code = event.process.exit_code; + } + }); + + processData.args = [...args]; + processData.entryLeader = getDetailPanelProcessLeader(process.events[0].process.entry_leader); + processData.sessionLeader = getDetailPanelProcessLeader(process.events[0].process.session_leader); + processData.groupLeader = getDetailPanelProcessLeader(process.events[0].process.group_leader); + processData.parent = getDetailPanelProcessLeader(process.events[0].process.parent); + + return processData; +}; + +export const getSelectedTabContent = (tabs: EuiTabProps[], selectedTabId: string) => { + const selectedTab = tabs.find((tab) => tab.id === selectedTabId); + + if (selectedTab) { + return selectedTab.content; + } + + return null; +}; diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx new file mode 100644 index 00000000000000..f754086fe5fab7 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { sessionViewBasicProcessMock } from '../../../common/mocks/constants/session_view_process.mock'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { SessionViewDetailPanel } from './index'; + +describe('SessionView component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When SessionViewDetailPanel is mounted', () => { + it('shows process detail by default', async () => { + renderResult = mockedContext.render( + + ); + expect(renderResult.queryByText('8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726')).toBeVisible(); + }); + + it('can switch tabs to show host details', async () => { + renderResult = mockedContext.render( + + ); + + renderResult.queryByText('Host')?.click(); + expect(renderResult.queryByText('hostname')).toBeVisible(); + expect(renderResult.queryAllByText('james-fleet-714-2')).toHaveLength(2); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx new file mode 100644 index 00000000000000..a47ce1d91ac973 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.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 React, { useState, useMemo, useCallback } from 'react'; +import { EuiTabs, EuiTab, EuiNotificationBadge } from '@elastic/eui'; +import { EuiTabProps } from '../../types'; +import { Process } from '../../../common/types/process_tree'; +import { getDetailPanelProcess, getSelectedTabContent } from './helpers'; +import { DetailPanelProcessTab } from '../detail_panel_process_tab'; +import { DetailPanelHostTab } from '../detail_panel_host_tab'; + +interface SessionViewDetailPanelDeps { + selectedProcess: Process; + onProcessSelected?: (process: Process) => void; +} + +/** + * Detail panel in the session view. + */ +export const SessionViewDetailPanel = ({ selectedProcess }: SessionViewDetailPanelDeps) => { + const [selectedTabId, setSelectedTabId] = useState('process'); + const processDetail = useMemo(() => getDetailPanelProcess(selectedProcess), [selectedProcess]); + + const tabs: EuiTabProps[] = useMemo( + () => [ + { + id: 'process', + name: 'Process', + content: , + }, + { + id: 'host', + name: 'Host', + content: , + }, + { + id: 'alerts', + disabled: true, + name: 'Alerts', + append: ( + + 10 + + ), + content: null, + }, + ], + [processDetail, selectedProcess.events] + ); + + const onSelectedTabChanged = useCallback((id: string) => { + setSelectedTabId(id); + }, []); + + const tabContent = useMemo( + () => getSelectedTabContent(tabs, selectedTabId), + [tabs, selectedTabId] + ); + + return ( + <> + + {tabs.map((tab, index) => ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTabId} + disabled={tab.disabled} + prepend={tab.prepend} + append={tab.append} + > + {tab.name} + + ))} + + {tabContent} + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/session_view_search_bar/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.test.tsx new file mode 100644 index 00000000000000..b27260668af076 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { processMock } from '../../../common/mocks/constants/session_view_process.mock'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { SessionViewSearchBar } from './index'; +import userEvent from '@testing-library/user-event'; +import { fireEvent } from '@testing-library/dom'; + +describe('SessionViewSearchBar component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + it('handles a typed search query', async () => { + const mockSetSearchQuery = jest.fn((query) => query); + const mockOnProcessSelected = jest.fn((process) => process); + + renderResult = mockedContext.render( + + ); + + const searchInput = renderResult.getByTestId('sessionView:searchInput').querySelector('input'); + + expect(searchInput?.value).toEqual('ls'); + + if (searchInput) { + userEvent.type(searchInput, ' -la'); + fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' }); + } + + expect(searchInput?.value).toEqual('ls -la'); + expect(mockSetSearchQuery.mock.calls.length).toBe(1); + expect(mockSetSearchQuery.mock.results[0].value).toBe('ls -la'); + }); + + it('shows a results navigator when searchResults provided', async () => { + const processMock2 = { ...processMock }; + const processMock3 = { ...processMock }; + const mockResults = [processMock, processMock2, processMock3]; + const mockSetSearchQuery = jest.fn((query) => query); + const mockOnProcessSelected = jest.fn((process) => process); + + renderResult = mockedContext.render( + + ); + + const searchPagination = renderResult.getByTestId('sessionView:searchPagination'); + expect(searchPagination).toBeTruthy(); + + const paginationTextClass = '.euiPagination__compressedText'; + expect(searchPagination.querySelector(paginationTextClass)?.textContent).toEqual('1 of 3'); + + userEvent.click(renderResult.getByTestId('pagination-button-next')); + expect(searchPagination.querySelector(paginationTextClass)?.textContent).toEqual('2 of 3'); + + const searchInput = renderResult.getByTestId('sessionView:searchInput').querySelector('input'); + + if (searchInput) { + userEvent.type(searchInput, ' -la'); + fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' }); + } + + // after search is changed, results index should reset to 1 + expect(searchPagination.querySelector(paginationTextClass)?.textContent).toEqual('1 of 3'); + + // setSelectedProcess should be called 3 times: + // 1. searchResults is set so auto select first item + // 2. next button hit, so call with 2nd item + // 3. search changed, so call with first result. + expect(mockOnProcessSelected.mock.calls.length).toBe(3); + expect(mockOnProcessSelected.mock.results[0].value).toEqual(processMock); + expect(mockOnProcessSelected.mock.results[1].value).toEqual(processMock2); + expect(mockOnProcessSelected.mock.results[1].value).toEqual(processMock); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx new file mode 100644 index 00000000000000..f4e4dac7a94c7e --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, useEffect } from 'react'; +import { EuiSearchBar, EuiPagination } from '@elastic/eui'; +import { EuiSearchBarOnChangeArgs } from '@elastic/eui'; +import { Process } from '../../../common/types/process_tree'; +import { useStyles } from './styles'; + +interface SessionViewSearchBarDeps { + searchQuery: string; + setSearchQuery(val: string): void; + searchResults: Process[] | null; + onProcessSelected(process: Process): void; +} + +/** + * The main wrapper component for the session view. + */ +export const SessionViewSearchBar = ({ + searchQuery, + setSearchQuery, + onProcessSelected, + searchResults, +}: SessionViewSearchBarDeps) => { + const styles = useStyles(); + + const [selectedResult, setSelectedResult] = useState(0); + + const onSearch = ({ query }: EuiSearchBarOnChangeArgs) => { + setSelectedResult(0); + + if (query) { + setSearchQuery(query.text); + } else { + setSearchQuery(''); + } + }; + + useEffect(() => { + if (searchResults) { + const process = searchResults[selectedResult]; + + if (process) { + onProcessSelected(process); + } + } + }, [searchResults, onProcessSelected, selectedResult]); + + const showPagination = !!searchResults?.length; + + return ( +
+ + {showPagination && ( + + )} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/session_view_search_bar/styles.ts b/x-pack/plugins/session_view/public/components/session_view_search_bar/styles.ts new file mode 100644 index 00000000000000..97a49ca2aa8c1d --- /dev/null +++ b/x-pack/plugins/session_view/public/components/session_view_search_bar/styles.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const pagination: CSSObject = { + position: 'absolute', + top: euiTheme.size.s, + right: euiTheme.size.xxl, + }; + + return { + pagination, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/hooks/use_scroll.ts b/x-pack/plugins/session_view/public/hooks/use_scroll.ts new file mode 100644 index 00000000000000..716e35dbb09870 --- /dev/null +++ b/x-pack/plugins/session_view/public/hooks/use_scroll.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import _ from 'lodash'; + +const SCROLL_END_BUFFER_HEIGHT = 20; +const DEBOUNCE_TIMEOUT = 500; + +function getScrollPosition(div: HTMLElement) { + if (div) { + return div.scrollTop; + } else { + return document.documentElement.scrollTop || document.body.scrollTop; + } +} + +interface IUseScrollDeps { + div: HTMLElement | null; + handler(pos: number, endReached: boolean): void; +} + +/** + * listens to scroll events on given div, if scroll reaches bottom, calls a callback + * @param {ref} ref to listen to scroll events on + * @param {function} handler function receives params (scrollTop, endReached) + */ +export function useScroll({ div, handler }: IUseScrollDeps) { + useEffect(() => { + if (div) { + const debounced = _.debounce(() => { + const pos = getScrollPosition(div); + const endReached = pos + div.offsetHeight > div.scrollHeight - SCROLL_END_BUFFER_HEIGHT; + + handler(pos, endReached); + }, DEBOUNCE_TIMEOUT); + + div.onscroll = debounced; + + return () => { + debounced.cancel(); + + div.onscroll = null; + }; + } + }, [div, handler]); +} diff --git a/x-pack/plugins/session_view/public/index.ts b/x-pack/plugins/session_view/public/index.ts new file mode 100644 index 00000000000000..90043e9a691dce --- /dev/null +++ b/x-pack/plugins/session_view/public/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SessionViewPlugin } from './plugin'; + +export function plugin() { + return new SessionViewPlugin(); +} diff --git a/x-pack/plugins/session_view/public/methods/index.tsx b/x-pack/plugins/session_view/public/methods/index.tsx new file mode 100644 index 00000000000000..560bb302ebabfb --- /dev/null +++ b/x-pack/plugins/session_view/public/methods/index.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +// Initializing react-query +const queryClient = new QueryClient(); + +const SessionViewLazy = lazy(() => import('../components/session_view')); + +export const getSessionViewLazy = (sessionEntityId: string) => { + return ( + + }> + + + + ); +}; diff --git a/x-pack/plugins/session_view/public/plugin.ts b/x-pack/plugins/session_view/public/plugin.ts new file mode 100644 index 00000000000000..d25c95b00b2c63 --- /dev/null +++ b/x-pack/plugins/session_view/public/plugin.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 { CoreSetup, CoreStart, Plugin } from '../../../../src/core/public'; +import { SessionViewServices } from './types'; +import { getSessionViewLazy } from './methods'; + +export class SessionViewPlugin implements Plugin { + public setup(core: CoreSetup) {} + + public start(core: CoreStart) { + return { + getSessionView: (sessionEntityId: string) => getSessionViewLazy(sessionEntityId), + }; + } + + public stop() {} +} diff --git a/x-pack/plugins/session_view/public/shared_imports.ts b/x-pack/plugins/session_view/public/shared_imports.ts new file mode 100644 index 00000000000000..0a087e1ac36ae3 --- /dev/null +++ b/x-pack/plugins/session_view/public/shared_imports.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SectionLoading } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/session_view/public/test/index.tsx b/x-pack/plugins/session_view/public/test/index.tsx new file mode 100644 index 00000000000000..8570e142538de8 --- /dev/null +++ b/x-pack/plugins/session_view/public/test/index.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { memo, ReactNode, useMemo } from 'react'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react'; +import { QueryClient, QueryClientProvider, setLogger } from 'react-query'; +import { Router } from 'react-router-dom'; +import { History } from 'history'; +import useObservable from 'react-use/lib/useObservable'; +import { I18nProvider } from '@kbn/i18n-react'; +import { CoreStart } from 'src/core/public'; +import { coreMock } from 'src/core/public/mocks'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; + +type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; + +// hide react-query output in console +setLogger({ + error: () => {}, + // eslint-disable-next-line no-console + log: console.log, + // eslint-disable-next-line no-console + warn: console.warn, +}); + +/** + * Mocked app root context renderer + */ +export interface AppContextTestRender { + history: ReturnType; + coreStart: ReturnType; + /** + * A wrapper around `AppRootContext` component. Uses the mocked modules as input to the + * `AppRootContext` + */ + AppWrapper: React.FC; + /** + * Renders the given UI within the created `AppWrapper` providing the given UI a mocked + * endpoint runtime context environment + */ + render: UiRender; +} + +const createCoreStartMock = ( + history: MemoryHistory +): ReturnType => { + const coreStart = coreMock.createStart({ basePath: '/mock' }); + + // Mock the certain APP Ids returned by `application.getUrlForApp()` + coreStart.application.getUrlForApp.mockImplementation((appId) => { + switch (appId) { + case 'sessionView': + return '/app/sessionView'; + default: + return `${appId} not mocked!`; + } + }); + + coreStart.application.navigateToUrl.mockImplementation((url) => { + history.push(url.replace('/app/sessionView', '')); + return Promise.resolve(); + }); + + return coreStart; +}; + +const AppRootProvider = memo<{ + history: History; + coreStart: CoreStart; + children: ReactNode | ReactNode[]; +}>(({ history, coreStart: { http, notifications, uiSettings, application }, children }) => { + const isDarkMode = useObservable(uiSettings.get$('theme:darkMode')); + const services = useMemo( + () => ({ http, notifications, application }), + [application, http, notifications] + ); + return ( + + + + {children} + + + + ); +}); + +AppRootProvider.displayName = 'AppRootProvider'; + +/** + * Creates a mocked app context custom renderer that can be used to render + * component that depend upon the application's surrounding context providers. + * Factory also returns the content that was used to create the custom renderer, allowing + * for further customization. + */ + +export const createAppRootMockRenderer = (): AppContextTestRender => { + const history = createMemoryHistory(); + const coreStart = createCoreStartMock(history); + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // turns retries off + retry: false, + // prevent jest did not exit errors + cacheTime: Infinity, + }, + }, + }); + + const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => ( + + {children} + + ); + + const render: UiRender = (ui, options = {}) => { + return reactRender(ui, { + wrapper: AppWrapper as React.ComponentType, + ...options, + }); + }; + + return { + history, + coreStart, + AppWrapper, + render, + }; +}; diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts new file mode 100644 index 00000000000000..2349b8423eb363 --- /dev/null +++ b/x-pack/plugins/session_view/public/types.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ReactNode } from 'react'; +import { CoreStart } from '../../../../src/core/public'; +import { TimelinesUIStart } from '../../timelines/public'; + +export type SessionViewServices = CoreStart & { + timelines: TimelinesUIStart; +}; + +export interface EuiTabProps { + id: string; + name: string; + content: ReactNode; + disabled?: boolean; + append?: ReactNode; + prepend?: ReactNode; +} + +export interface DetailPanelProcess { + id: string; + start: string; + end: string; + exit_code: number; + user: string; + args: string[]; + executable: string[][]; + pid: number; + entryLeader: DetailPanelProcessLeader; + sessionLeader: DetailPanelProcessLeader; + groupLeader: DetailPanelProcessLeader; + parent: DetailPanelProcessLeader; +} + +export interface DetailPanelProcessLeader { + id: string; + name: string; + start: string; + entryMetaType: string; + userName: string; + interactive: boolean; + pid: number; + entryMetaSourceIp: string; + executable: string; +} diff --git a/x-pack/plugins/session_view/public/utils/data_or_dash.test.ts b/x-pack/plugins/session_view/public/utils/data_or_dash.test.ts new file mode 100644 index 00000000000000..12ef44cf1d7083 --- /dev/null +++ b/x-pack/plugins/session_view/public/utils/data_or_dash.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { dataOrDash } from './data_or_dash'; + +const TEST_STRING = '123'; +const TEST_NUMBER = 123; +const DASH = '-'; + +describe('dataOrDash(data)', () => { + it('works for a valid string', () => { + expect(dataOrDash(TEST_STRING)).toEqual(TEST_STRING); + }); + it('works for a valid number', () => { + expect(dataOrDash(TEST_NUMBER)).toEqual(TEST_NUMBER); + }); + it('returns dash for undefined', () => { + expect(dataOrDash(undefined)).toEqual(DASH); + }); + it('returns dash for empty string', () => { + expect(dataOrDash('')).toEqual(DASH); + }); + it('returns dash for NaN', () => { + expect(dataOrDash(NaN)).toEqual(DASH); + }); +}); diff --git a/x-pack/plugins/session_view/public/utils/data_or_dash.ts b/x-pack/plugins/session_view/public/utils/data_or_dash.ts new file mode 100644 index 00000000000000..ff6c2fb9bc1ffb --- /dev/null +++ b/x-pack/plugins/session_view/public/utils/data_or_dash.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. + */ + +/** + * Returns a dash ('-') if data is undefined, and empty string, or a NaN. + * + * Used by frontend components + * + * @param {String | Number | undefined} data + * @return {String | Number} either data itself or if invalid, a dash ('-') + */ +export const dataOrDash = (data: string | number | undefined): string | number => { + if (data === undefined || data === '' || (typeof data === 'number' && isNaN(data))) { + return '-'; + } + + return data; +}; diff --git a/x-pack/plugins/session_view/server/index.ts b/x-pack/plugins/session_view/server/index.ts new file mode 100644 index 00000000000000..a86684094dfd70 --- /dev/null +++ b/x-pack/plugins/session_view/server/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from '../../../../src/core/server'; +import { SessionViewPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new SessionViewPlugin(initializerContext); +} diff --git a/x-pack/plugins/session_view/server/plugin.ts b/x-pack/plugins/session_view/server/plugin.ts new file mode 100644 index 00000000000000..c7fd511b3de050 --- /dev/null +++ b/x-pack/plugins/session_view/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CoreSetup, + CoreStart, + Plugin, + Logger, + PluginInitializerContext, +} from '../../../../src/core/server'; +import { SessionViewSetupPlugins, SessionViewStartPlugins } from './types'; +import { registerRoutes } from './routes'; + +export class SessionViewPlugin implements Plugin { + private logger: Logger; + + /** + * Initialize SessionViewPlugin class properties (logger, etc) that is accessible + * through the initializerContext. + */ + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup, plugins: SessionViewSetupPlugins) { + this.logger.debug('session view: Setup'); + const router = core.http.createRouter(); + + // Register server routes + registerRoutes(router); + } + + public start(core: CoreStart, plugins: SessionViewStartPlugins) { + this.logger.debug('session view: Start'); + } + + public stop() { + this.logger.debug('session view: Stop'); + } +} diff --git a/x-pack/plugins/session_view/server/routes/index.ts b/x-pack/plugins/session_view/server/routes/index.ts new file mode 100644 index 00000000000000..7b9cfb45f580b7 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { IRouter } from '../../../../../src/core/server'; +import { registerProcessEventsRoute } from './process_events_route'; +import { sessionEntryLeadersRoute } from './session_entry_leaders_route'; + +export const registerRoutes = (router: IRouter) => { + registerProcessEventsRoute(router); + sessionEntryLeadersRoute(router); +}; diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.test.ts b/x-pack/plugins/session_view/server/routes/process_events_route.test.ts new file mode 100644 index 00000000000000..76f54eb4b8ab65 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/process_events_route.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { doSearch } from './process_events_route'; +import { mockEvents } from '../../common/mocks/constants/session_view_process.mock'; + +const getEmptyResponse = async () => { + return { + hits: { + total: { value: 0, relation: 'eq' }, + hits: [], + }, + }; +}; + +const getResponse = async () => { + return { + hits: { + total: { value: mockEvents.length, relation: 'eq' }, + hits: mockEvents.map((event) => { + return { _source: event }; + }), + }, + }; +}; + +describe('process_events_route.ts', () => { + describe('doSearch(client, entityId, cursor, forward)', () => { + it('should return an empty events array for a non existant entity_id', async () => { + const client = elasticsearchServiceMock.createElasticsearchClient(getEmptyResponse()); + + const body = await doSearch(client, 'asdf', undefined); + + expect(body.events.length).toBe(0); + }); + + it('returns results for a particular session entity_id', async () => { + const client = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + + const body = await doSearch(client, 'mockId', undefined); + + expect(body.events.length).toBe(mockEvents.length); + }); + + it('returns hits in reverse order when paginating backwards', async () => { + const client = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + + const body = await doSearch(client, 'mockId', undefined, false); + + expect(body.events[0]._source).toEqual(mockEvents[mockEvents.length - 1]); + }); + }); +}); diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts new file mode 100644 index 00000000000000..47e2d917733d5b --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; +import type { ElasticsearchClient } from 'kibana/server'; +import { IRouter } from '../../../../../src/core/server'; +import { + PROCESS_EVENTS_ROUTE, + PROCESS_EVENTS_PER_PAGE, + PROCESS_EVENTS_INDEX, + ALERTS_INDEX, + ENTRY_SESSION_ENTITY_ID_PROPERTY, +} from '../../common/constants'; +import { expandDottedObject } from '../../common/utils/expand_dotted_object'; + +export const registerProcessEventsRoute = (router: IRouter) => { + router.get( + { + path: PROCESS_EVENTS_ROUTE, + validate: { + query: schema.object({ + sessionEntityId: schema.string(), + cursor: schema.maybe(schema.string()), + forward: schema.maybe(schema.boolean()), + }), + }, + }, + async (context, request, response) => { + const client = context.core.elasticsearch.client.asCurrentUser; + const { sessionEntityId, cursor, forward = true } = request.query; + const body = await doSearch(client, sessionEntityId, cursor, forward); + + return response.ok({ body }); + } + ); +}; + +export const doSearch = async ( + client: ElasticsearchClient, + sessionEntityId: string, + cursor: string | undefined, + forward = true +) => { + const search = await client.search({ + // TODO: move alerts into it's own route with it's own pagination. + index: [PROCESS_EVENTS_INDEX, ALERTS_INDEX], + ignore_unavailable: true, + body: { + query: { + match: { + [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, + }, + }, + // This runtime_mappings is a temporary fix, so we are able to Query these ECS fields while they are not available + // TODO: Remove the runtime_mappings once process.entry_leader.entity_id is implemented to ECS + runtime_mappings: { + [ENTRY_SESSION_ENTITY_ID_PROPERTY]: { + type: 'keyword', + }, + }, + size: PROCESS_EVENTS_PER_PAGE, + sort: [{ '@timestamp': forward ? 'asc' : 'desc' }], + search_after: cursor ? [cursor] : undefined, + }, + }); + + const events = search.hits.hits.map((hit: any) => { + // TODO: re-eval if this is needed after moving alerts to it's own route. + // the .siem-signals-default index flattens many properties. this util unflattens them. + hit._source = expandDottedObject(hit._source); + + return hit; + }); + + if (!forward) { + events.reverse(); + } + + return { + events, + }; +}; diff --git a/x-pack/plugins/session_view/server/routes/session_entry_leaders_route.ts b/x-pack/plugins/session_view/server/routes/session_entry_leaders_route.ts new file mode 100644 index 00000000000000..98aee357fb91e0 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/session_entry_leaders_route.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; +import { IRouter } from '../../../../../src/core/server'; +import { SESSION_ENTRY_LEADERS_ROUTE, PROCESS_EVENTS_INDEX } from '../../common/constants'; + +export const sessionEntryLeadersRoute = (router: IRouter) => { + router.get( + { + path: SESSION_ENTRY_LEADERS_ROUTE, + validate: { + query: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = context.core.elasticsearch.client.asCurrentUser; + const { id } = request.query; + + const result = await client.get({ + index: PROCESS_EVENTS_INDEX, + id, + }); + + return response.ok({ + body: { + session_entry_leader: result?._source, + }, + }); + } + ); +}; diff --git a/x-pack/plugins/session_view/server/types.ts b/x-pack/plugins/session_view/server/types.ts new file mode 100644 index 00000000000000..0d1375081ca870 --- /dev/null +++ b/x-pack/plugins/session_view/server/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SessionViewSetupPlugins {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SessionViewStartPlugins {} diff --git a/x-pack/plugins/session_view/tsconfig.json b/x-pack/plugins/session_view/tsconfig.json new file mode 100644 index 00000000000000..a99e83976a31d4 --- /dev/null +++ b/x-pack/plugins/session_view/tsconfig.json @@ -0,0 +1,42 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + // add all the folders containg files to be compiled + "common/**/*", + "public/**/*", + "server/**/*", + "server/**/*.json", + "scripts/**/*", + "package.json", + "storybook/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + // add references to other TypeScript projects the plugin depends on + + // requiredPlugins from ./kibana.json + { "path": "../licensing/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../encrypted_saved_objects/tsconfig.json" }, + + // optionalPlugins from ./kibana.json + { "path": "../security/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + + // requiredBundles from ./kibana.json + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../infra/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx index e5c8343fddf6d4..7b27167d5f5f91 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx @@ -11,6 +11,14 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { IndexSelectPopover } from './index_select_popover'; import { EuiComboBox } from '@elastic/eui'; +jest.mock('lodash', () => { + const module = jest.requireActual('lodash'); + return { + ...module, + debounce: (fn: () => unknown) => fn, + }; +}); + jest.mock('../../../../triggers_actions_ui/public', () => { const original = jest.requireActual('../../../../triggers_actions_ui/public'); return { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx index fbfb296c7b2704..a8b9f3f56dd06e 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { isString } from 'lodash'; +import { isString, debounce } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonIcon, @@ -27,7 +27,6 @@ import { firstFieldOption, getFields, getIndexOptions, - getIndexPatterns, getTimeFieldOptions, IErrorObject, } from '../../../../triggers_actions_ui/public'; @@ -62,16 +61,14 @@ export const IndexSelectPopover: React.FunctionComponent = ({ const [indexPopoverOpen, setIndexPopoverOpen] = useState(false); const [indexOptions, setIndexOptions] = useState([]); - const [indexPatterns, setIndexPatterns] = useState([]); const [areIndicesLoading, setAreIndicesLoading] = useState(false); const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); - useEffect(() => { - const indexPatternsFunction = async () => { - setIndexPatterns(await getIndexPatterns()); - }; - indexPatternsFunction(); - }, []); + const loadIndexOptions = debounce(async (search: string) => { + setAreIndicesLoading(true); + setIndexOptions(await getIndexOptions(http!, search)); + setAreIndicesLoading(false); + }, 250); useEffect(() => { const timeFields = getTimeFieldOptions(esFields); @@ -193,11 +190,7 @@ export const IndexSelectPopover: React.FunctionComponent = ({ setTimeFieldOptions([firstFieldOption, ...timeFields]); } }} - onSearchChange={async (search) => { - setAreIndicesLoading(true); - setIndexOptions(await getIndexOptions(http!, search, indexPatterns)); - setAreIndicesLoading(false); - }} + onSearchChange={loadIndexOptions} onBlur={() => { if (!index) { onIndexChange([]); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index e0dd709a54e574..f7a82f69ae817c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -3720,6 +3720,12 @@ "description": "Number of times the user toggled fullscreen mode on formula." } }, + "toggle_autoapply": { + "type": "long", + "_meta": { + "description": "Number of times the user toggled auto-apply." + } + }, "indexpattern_field_info_click": { "type": "long" }, @@ -3967,6 +3973,12 @@ "description": "Number of times the user toggled fullscreen mode on formula." } }, + "toggle_autoapply": { + "type": "long", + "_meta": { + "description": "Number of times the user toggled auto-apply." + } + }, "indexpattern_field_info_click": { "type": "long" }, diff --git a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx index 02fd0553f4016c..31b8e9f62803ec 100644 --- a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx @@ -28,10 +28,7 @@ export const FieldBrowserWrappedComponent = (props: FieldBrowserWrappedComponent return ( - + ); 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/components/t_grid/toolbar/fields_browser/field_browser.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx index dc9837007e1538..d435d7a280840b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx @@ -32,6 +32,7 @@ const testProps = { browserFields: mockBrowserFields, filteredBrowserFields: mockBrowserFields, searchInput: '', + appliedFilterInput: '', isSearching: false, onCategorySelected: jest.fn(), onHide, @@ -84,6 +85,7 @@ describe('FieldsBrowser', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} searchInput={''} + appliedFilterInput={''} isSearching={false} onCategorySelected={jest.fn()} onHide={jest.fn()} diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx index fea22e4efe77c1..e55f54e946ad13 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx @@ -75,6 +75,8 @@ type Props = Pick & isSearching: boolean; /** The text displayed in the search input */ searchInput: string; + /** The text actually being applied to the result set, a debounced version of searchInput */ + appliedFilterInput: string; /** * The category selected on the left-hand side of the field browser */ @@ -115,6 +117,7 @@ const FieldsBrowserComponent: React.FC = ({ onHide, restoreFocusTo, searchInput, + appliedFilterInput, selectedCategoryId, timelineId, width = FIELD_BROWSER_WIDTH, @@ -237,7 +240,7 @@ const FieldsBrowserComponent: React.FC = ({ filteredBrowserFields={filteredBrowserFields} onCategorySelected={onCategorySelected} onUpdateColumns={onUpdateColumns} - searchInput={searchInput} + searchInput={appliedFilterInput} selectedCategoryId={selectedCategoryId} timelineId={timelineId} width={FIELDS_PANE_WIDTH} diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx index 5345475a025018..d1d0254d0c917d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx @@ -98,19 +98,30 @@ export const FieldsPane = React.memo( [filteredBrowserFields] ); + const fieldItems = useMemo(() => { + return getFieldItems({ + category: filteredBrowserFields[selectedCategoryId], + columnHeaders, + highlight: searchInput, + timelineId, + toggleColumn, + }); + }, [ + columnHeaders, + filteredBrowserFields, + searchInput, + selectedCategoryId, + timelineId, + toggleColumn, + ]); + if (filteredBrowserFieldsExists) { return ( = ({ /** all field names shown in the field browser must contain this string (when specified) */ const [filterInput, setFilterInput] = useState(''); + + const [appliedFilterInput, setAppliedFilterInput] = useState(''); /** all fields in this collection have field names that match the filterInput */ const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ @@ -51,15 +53,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); /** show the field browser */ const [show, setShow] = useState(false); - useEffect(() => { - return () => { - if (inputTimeoutId.current !== 0) { - // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: - clearTimeout(inputTimeoutId.current); - inputTimeoutId.current = 0; - } - }; - }, []); /** Shows / hides the field browser */ const onShow = useCallback(() => { @@ -69,52 +62,68 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ /** Invoked when the field browser should be hidden */ const onHide = useCallback(() => { setFilterInput(''); + setAppliedFilterInput(''); setFilteredBrowserFields(null); setIsSearching(false); setSelectedCategoryId(DEFAULT_CATEGORY_NAME); setShow(false); }, []); + const newFilteredBrowserFields = useMemo(() => { + return filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: appliedFilterInput, + }); + }, [appliedFilterInput, browserFields]); + + const newSelectedCategoryId = useMemo(() => { + if (appliedFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0) { + return DEFAULT_CATEGORY_NAME; + } else { + return Object.keys(newFilteredBrowserFields) + .sort() + .reduce((selected, category) => { + const filteredBrowserFieldsByCategory = + (newFilteredBrowserFields[category] && newFilteredBrowserFields[category].fields) || []; + const filteredBrowserFieldsBySelected = + (newFilteredBrowserFields[selected] && newFilteredBrowserFields[selected].fields) || []; + return newFilteredBrowserFields[category].fields != null && + newFilteredBrowserFields[selected].fields != null && + Object.keys(filteredBrowserFieldsByCategory).length > + Object.keys(filteredBrowserFieldsBySelected).length + ? category + : selected; + }, Object.keys(newFilteredBrowserFields)[0]); + } + }, [appliedFilterInput, newFilteredBrowserFields]); + /** Invoked when the user types in the filter input */ - const updateFilter = useCallback( - (newFilterInput: string) => { - setFilterInput(newFilterInput); - setIsSearching(true); - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: newFilterInput, - }); - setFilteredBrowserFields(newFilteredBrowserFields); - setIsSearching(false); - - const newSelectedCategoryId = - newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 - ? DEFAULT_CATEGORY_NAME - : Object.keys(newFilteredBrowserFields) - .sort() - .reduce( - (selected, category) => - newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.keys(newFilteredBrowserFields[category].fields!).length > - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.keys(newFilteredBrowserFields[selected].fields!).length - ? category - : selected, - Object.keys(newFilteredBrowserFields)[0] - ); - setSelectedCategoryId(newSelectedCategoryId); - }, INPUT_TIMEOUT); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [browserFields, filterInput, inputTimeoutId.current] - ); + const updateFilter = useCallback((newFilterInput: string) => { + setFilterInput(newFilterInput); + setIsSearching(true); + }, []); + + useEffect(() => { + if (inputTimeoutId.current !== 0) { + clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers + } + // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: + inputTimeoutId.current = window.setTimeout(() => { + setIsSearching(false); + setAppliedFilterInput(filterInput); + }, INPUT_TIMEOUT); + return () => { + clearTimeout(inputTimeoutId.current); + }; + }, [filterInput]); + + useEffect(() => { + setFilteredBrowserFields(newFilteredBrowserFields); + }, [newFilteredBrowserFields]); + + useEffect(() => { + setSelectedCategoryId(newSelectedCategoryId); + }, [newSelectedCategoryId]); // only merge in the default category if the field browser is visible const browserFieldsWithDefaultCategory = useMemo(() => { @@ -152,6 +161,7 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ onSearchInputChange={updateFilter} restoreFocusTo={customizeColumnsButtonRef} searchInput={filterInput} + appliedFilterInput={appliedFilterInput} selectedCategoryId={selectedCategoryId} timelineId={timelineId} width={width} 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..99e4c6a4ca1f30 --- /dev/null +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -0,0 +1,25705 @@ +{ + "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.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.dateFormatTitle": "Format de date", + "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 \"