diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 736a71b73d14d4..f3f07d5f355be1 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -19,7 +19,7 @@ currentBuild.description = "ES: ${SNAPSHOT_VERSION}
Kibana: ${params.branch def SNAPSHOT_MANIFEST = "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${SNAPSHOT_VERSION}/archives/${SNAPSHOT_ID}/manifest.json" -kibanaPipeline(timeoutMinutes: 150) { +kibanaPipeline(timeoutMinutes: 210) { catchErrors { slackNotifications.onFailure( title: "*<${env.BUILD_URL}|[${SNAPSHOT_VERSION}] ES Snapshot Verification Failure>*", diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4b0479eedea988..b45ff51b70da3c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -74,7 +74,13 @@ #CC# /src/plugins/apm_oss/ @elastic/apm-ui #CC# /x-pack/plugins/observability/ @elastic/apm-ui -# Client Side Monitoring (lives in APM directories but owned by Uptime) +# Uptime +/x-pack/plugins/uptime @elastic/uptime +/x-pack/test/functional_with_es_ssl/apps/uptime @elastic/uptime +/x-pack/test/functional/apps/uptime @elastic/uptime +/x-pack/test/api_integration/apis/uptime @elastic/uptime + +# Client Side Monitoring / Uptime (lives in APM directories but owned by Uptime) /x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm @elastic/uptime /x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @elastic/uptime /x-pack/plugins/apm/public/application/csmApp.tsx @elastic/uptime @@ -106,7 +112,6 @@ /x-pack/plugins/fleet/ @elastic/fleet /x-pack/plugins/observability/ @elastic/observability-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui -/x-pack/plugins/uptime @elastic/uptime # Machine Learning /x-pack/plugins/ml/ @elastic/ml-ui diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 238a21161b1297..79571d51659d6c 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -18,34 +18,20 @@ jobs: ) runs-on: ubuntu-latest steps: - - name: 'Get backport config' - run: | - curl 'https://raw.githubusercontent.com/elastic/kibana/master/.backportrc.json' > .backportrc.json - - - name: Use Node.js 14.x - uses: actions/setup-node@v1 + - name: Checkout Actions + uses: actions/checkout@v2 with: - node-version: 14.x - - - name: Install backport CLI - run: npm install -g backport@5.6.4 + repository: 'elastic/kibana-github-actions' + ref: main + path: ./actions - - name: Backport PR - run: | - git config --global user.name "kibanamachine" - git config --global user.email "42973632+kibanamachine@users.noreply.github.com" - backport --fork true --username kibanamachine --accessToken "${{ secrets.KIBANAMACHINE_TOKEN }}" --ci --pr "$PR_NUMBER" --labels backport --assignee "$PR_OWNER" | tee 'output.log' - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_OWNER: ${{ github.event.pull_request.user.login }} + - name: Install Actions + run: npm install --production --prefix ./actions - - name: Report backport status - run: | - COMMENT="Backport result - \`\`\` - $(cat output.log) - \`\`\`" - - GITHUB_TOKEN="${{ secrets.KIBANAMACHINE_TOKEN }}" gh api -X POST repos/elastic/kibana/issues/$PR_NUMBER/comments -F body="$COMMENT" - env: - PR_NUMBER: ${{ github.event.pull_request.number }} + - name: Run Backport + uses: ./actions/backport + with: + branch: master + github_token: ${{secrets.KIBANAMACHINE_TOKEN}} + commit_user: kibanamachine + commit_email: 42973632+kibanamachine@users.noreply.github.com diff --git a/Jenkinsfile b/Jenkinsfile index 3b68cde206573a..8ab3fecb07a1ba 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) { +kibanaPipeline(timeoutMinutes: 210, checkPrChanges: true, setCommitStatus: true) { slackNotifications.onFailure(disabled: !params.NOTIFY_ON_FAILURE) { githubPr.withDefaultPrComments { ciStats.trackBuild { diff --git a/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md b/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md index 04a3cf9aff6448..52ab5f1098457c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md +++ b/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md @@ -34,7 +34,7 @@ Customize the configuration for the plugins.data.search context. core.logging.configure( of({ appenders: new Map(), - loggers: [{ context: 'search', appenders: ['default'] }] + loggers: [{ name: 'search', appenders: ['default'] }] }) ) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md new file mode 100644 index 00000000000000..baf44de5088fbd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [indexPattern](./kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md) + +## ISearchOptions.indexPattern property + +Index pattern reference is used for better error messages + +Signature: + +```typescript +indexPattern?: IndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index fc2767cd0231f9..2473c9cfdde8df 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -15,6 +15,7 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [indexPattern](./kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md) | IndexPattern | Index pattern reference is used for better error messages | | [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | | [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) | boolean | Whether the session is already saved (i.e. sent to background) | | [legacyHitsTotal](./kibana-plugin-plugins-data-public.isearchoptions.legacyhitstotal.md) | boolean | Request the legacy format for the total number of hits. If sending rest_total_hits_as_int to something other than true, this should be set to false. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md index 5f43f8477cb9f1..b8f21de3e086ec 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `PainlessError` class Signature: ```typescript -constructor(err: IEsError); +constructor(err: IEsError, indexPattern?: IndexPattern); ``` ## Parameters @@ -17,4 +17,5 @@ constructor(err: IEsError); | Parameter | Type | Description | | --- | --- | --- | | err | IEsError | | +| indexPattern | IndexPattern | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.indexpattern.md new file mode 100644 index 00000000000000..4312f2f8d0c91f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.indexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) > [indexPattern](./kibana-plugin-plugins-data-public.painlesserror.indexpattern.md) + +## PainlessError.indexPattern property + +Signature: + +```typescript +indexPattern?: IndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md index c77b8b259136b1..3a887d358e2155 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md @@ -14,12 +14,13 @@ export declare class PainlessError extends EsError | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(err)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the PainlessError class | +| [(constructor)(err, indexPattern)](./kibana-plugin-plugins-data-public.painlesserror._constructor_.md) | | Constructs a new instance of the PainlessError class | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [indexPattern](./kibana-plugin-plugins-data-public.painlesserror.indexpattern.md) | | IndexPattern | | | [painlessStack](./kibana-plugin-plugins-data-public.painlesserror.painlessstack.md) | | string | | ## Methods diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md new file mode 100644 index 00000000000000..cc24363c1bed5e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [indexPattern](./kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md) + +## ISearchOptions.indexPattern property + +Index pattern reference is used for better error messages + +Signature: + +```typescript +indexPattern?: IndexPattern; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index 9de351b2b90194..7fd4dd5b8e566c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -15,6 +15,7 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [indexPattern](./kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md) | IndexPattern | Index pattern reference is used for better error messages | | [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | | [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) | boolean | Whether the session is already saved (i.e. sent to background) | | [legacyHitsTotal](./kibana-plugin-plugins-data-server.isearchoptions.legacyhitstotal.md) | boolean | Request the legacy format for the total number of hits. If sending rest_total_hits_as_int to something other than true, this should be set to false. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index ea3ba28a52defc..9dc38f96df4be6 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index 8c0012fb6c6bf5..a92fc182f388c3 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -1,55 +1,63 @@ [[kuery-query]] === Kibana Query Language -The Kibana Query Language (KQL) makes it easy to find -the fields and syntax for your {es} query. If you have the -https://www.elastic.co/subscriptions[Basic tier] or above, -simply place your cursor in the *Search* field. As you type, you’ll get suggestions for fields, -values, and operators. +The Kibana Query Language (KQL) is a simple syntax for filtering {es} data using +free text search or field-based search. KQL is only used for filtering data, and has +no role in sorting or aggregating the data. + +KQL is able to suggest field names, values, and operators as you type. +The performance of the suggestions is controlled by <>: [role="screenshot"] image::images/kql-autocomplete.png[Autocomplete in Search bar] -If you prefer to use Kibana’s legacy query language, based on the -<>, click *KQL* next to the *Search* field, and then turn off KQL. +KQL has a different set of features than the <>. KQL is able to query +nested fields and <>. KQL does not support regular expressions +or searching with fuzzy terms. To use the legacy Lucene syntax, click *KQL* next to the *Search* field, +and then turn off KQL. [discrete] === Terms query -A terms query matches documents that contain one or more *exact* terms in a field. +A terms query uses *exact search terms*. Spaces separate each search term, and only one term +is required to match the document. Use quotation marks to indicate a *phrase match*. -To match documents where the response field is `200`: +To query using *exact search terms*, enter the field name followed by `:` and +then the values separated by spaces: [source,yaml] ------------------- -response:200 +http.response.status_code:400 401 404 ------------------- -To match documents with the phrase "quick brown fox" in the `message` field. +For text fields, this will match any value regardless of order: [source,yaml] ------------------- -message:"quick brown fox" +http.response.body.content.text:quick brown fox ------------------- -Without the quotes, -the query matches documents regardless of the order in which -they appear. Documents with "quick brown fox" match, -and so does "quick fox brown". +To query for an *exact phrase*, use quotation marks around the values: + +[source,yaml] +------------------- +http.response.body.content.text:"quick brown fox" +------------------- -NOTE: Terms without fields are matched against the default field in your index settings. -If a default field is not -set, terms are matched against all fields. For example, a query -for `response:200` searches for the value 200 -in the response field, but a query for just `200` searches for 200 -across all fields in your index. +Field names are not required by KQL. When a field name is not provided, terms +will be matched by the default fields in your index settings. To search across fields: +[source,yaml] +------------------- +"quick brown fox" +------------------- [discrete] === Boolean queries KQL supports `or`, `and`, and `not`. By default, `and` has a higher precedence than `or`. -To override the default precedence, group operators in parentheses. +To override the default precedence, group operators in parentheses. These operators can +be upper or lower case. To match documents where response is `200`, extension is `php`, or both: @@ -143,7 +151,7 @@ but in some cases you might need to search on dates. Include the date range in q [discrete] === Exist queries -An exist query matches documents that contain a value for a field, in this case, +An exist query matches documents that contain any value for a field, in this case, response: [source,yaml] @@ -151,10 +159,16 @@ response: response:* ------------------- +Existence is defined by {es} and includes all values, including empty text. + [discrete] === Wildcard queries -To match documents where machine.os starts with `win`, such +Wildcards queries can be used to *search by a term prefix* or to *search multiple fields*. +The default settings of {kib} *prevent leading wildcards* for performance reasons, +but this can be allowed with an <>. + +To match documents where `machine.os` starts with `win`, such as "windows 7" and "windows 10": [source,yaml] diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 45f0df5bd773fc..e8faccd50661a8 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -53,36 +53,55 @@ include::kuery.asciidoc[] [[lucene-query]] === Lucene query syntax -Kibana's legacy query language was based on the Lucene query syntax. For the time being this syntax -is still available under the options menu in the Query Bar and in Advanced Settings. The following -are some tips that can help get you started. +Lucene query syntax is available to {kib} users who opt out of the <>. +Full documentation for this syntax is available as part of {es} +{ref}/query-dsl-query-string-query.html#query-string-syntax[query string syntax]. -* To perform a free text search, simply enter a text string. For example, if +The main reason to use the Lucene query syntax in {kib} is for advanced +Lucene features, such as regular expressions or fuzzy term matching. However, +Lucene syntax is not able to search nested objects or scripted fields. + +To perform a free text search, simply enter a text string. For example, if you're searching web server logs, you could enter `safari` to search all -fields for the term `safari`. +fields: + +[source,yaml] +------------------- +safari +------------------- + +To search for a value in a specific field, prefix the value with the name +of the field: -* To search for a value in a specific field, prefix the value with the name -of the field. For example, you could enter `status:200` to find all of -the entries that contain the value `200` in the `status` field. +[source,yaml] +------------------- +status:200 +------------------- -* To search for a range of values, you can use the bracketed range syntax, +To search for a range of values, use the bracketed range syntax, `[START_VALUE TO END_VALUE]`. For example, to find entries that have 4xx status codes, you could enter `status:[400 TO 499]`. -* To specify more complex search criteria, you can use the Boolean operators -`AND`, `OR`, and `NOT`. For example, to find entries that have 4xx status -codes and have an extension of `php` or `html`, you could enter `status:[400 TO -499] AND (extension:php OR extension:html)`. +[source,yaml] +------------------- +status:[400 TO 499] +------------------- + +For an open range, use a wildcard: -IMPORTANT: When you use the Lucene Query Syntax in the *KQL* search bar, {kib} is unable to search on nested objects and perform aggregations across fields that contain nested objects. -Using `include_in_parent` or `copy_to` as a workaround can cause {kib} to fail. +[source,yaml] +------------------- +status:[400 TO *] +------------------- -For more detailed information about the Lucene query syntax, see the -{ref}/query-dsl-query-string-query.html#query-string-syntax[Query String Query] -docs. +To specify more complex search criteria, use the boolean operators +`AND`, `OR`, and `NOT`. For example, to find entries that have 4xx status +codes and have an extension of `php` or `html`: -NOTE: These examples use the Lucene query syntax. When lucene is selected as your -query language you can also submit queries using the {ref}/query-dsl.html[Elasticsearch Query DSL]. +[source,yaml] +------------------- +status:[400 TO 499] AND (extension:php OR extension:html) +------------------- [[save-open-search]] diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index afcb7bc21b66b6..7ffb6b66f5a2b4 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -356,26 +356,26 @@ To enable the <>, specify wh [source,yaml] ---------------------------------------- xpack.security.audit.appender: - kind: rolling-file - path: ./audit.log + type: rolling-file + fileName: ./audit.log policy: - kind: time-interval + type: time-interval interval: 24h <1> strategy: - kind: numeric + type: numeric max: 10 <2> layout: - kind: json + type: json ---------------------------------------- <1> Rotates log files every 24 hours. <2> Keeps maximum of 10 log files before deleting older ones. -| `xpack.security.audit.appender.kind` +| `xpack.security.audit.appender.type` | Required. Specifies where audit logs should be written to. Allowed values are `console`, `file`, or `rolling-file`. Refer to <> and <> for appender specific settings. -| `xpack.security.audit.appender.layout.kind` +| `xpack.security.audit.appender.layout.type` | Required. Specifies how audit logs should be formatted. Allowed values are `json` or `pattern`. Refer to <> for layout specific settings. @@ -396,7 +396,7 @@ The `file` appender writes to a file and can be configured using the following s [cols="2*<"] |====== -| `xpack.security.audit.appender.path` +| `xpack.security.audit.appender.fileName` | Required. Full file path the log file should be written to. |====== @@ -408,14 +408,14 @@ The `rolling-file` appender writes to a file and rotates it using a rolling stra [cols="2*<"] |====== -| `xpack.security.audit.appender.path` +| `xpack.security.audit.appender.fileName` | Required. Full file path the log file should be written to. -| `xpack.security.audit.appender.policy.kind` +| `xpack.security.audit.appender.policy.type` | Specifies when a rollover should occur. Allowed values are `size-limit` and `time-interval`. *Default:* `time-interval`. Refer to <> and <> for policy specific settings. -| `xpack.security.audit.appender.strategy.kind` +| `xpack.security.audit.appender.strategy.type` | Specifies how the rollover should occur. Only allowed value is currently `numeric`. *Default:* `numeric` Refer to <> for strategy specific settings. diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index 440b597520032c..052e40d845fd91 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -24,7 +24,9 @@ When you open *TSVB*, click *Panel options*, then verify the following: ==== Visualization options Time series:: - Supports annotations based on timestamped documents in a separate {es} index. + By default, the Y axis shows the full range of data, including zero. To scale the axis from + the minimum to maximum values of the data automatically, go to *Series > Options > Fill* and set *Fill to 0*. + You can add annotations to the x-axis based on timestamped documents in a separate {es} index. All other chart types:: *Panel options > Data timerange mode* controls the timespan used for matching documents. diff --git a/package.json b/package.json index ed21cb7052c1cb..33d8f6a9c52806 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", "@elastic/ems-client": "7.12.0", - "@elastic/eui": "31.4.0", + "@elastic/eui": "31.7.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", @@ -352,7 +352,7 @@ "@cypress/webpack-preprocessor": "^5.5.0", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "24.5.1", + "@elastic/charts": "24.6.0", "@elastic/eslint-config-kibana": "link:packages/elastic-eslint-config-kibana", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 6351a227ff90b8..2801e0a0688cc6 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -68,10 +68,10 @@ exports[`#get correctly handles silent logging config. 1`] = ` Object { "appenders": Object { "default": Object { - "kind": "legacy-appender", "legacyLoggingConfig": Object { "silent": true, }, + "type": "legacy-appender", }, }, "loggers": undefined, @@ -85,12 +85,12 @@ exports[`#get correctly handles verbose file logging config with json format. 1` Object { "appenders": Object { "default": Object { - "kind": "legacy-appender", "legacyLoggingConfig": Object { "dest": "/some/path.log", "json": true, "verbose": true, }, + "type": "legacy-appender", }, }, "loggers": undefined, diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts index 4d877a26b76418..8ec26ff1f8e71c 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts @@ -44,7 +44,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { const loggingConfig = { appenders: { ...appenders, - default: { kind: 'legacy-appender', legacyLoggingConfig }, + default: { type: 'legacy-appender', legacyLoggingConfig }, }, root: { level: 'info', ...root }, loggers, diff --git a/packages/kbn-ui-shared-deps/scripts/build.js b/packages/kbn-ui-shared-deps/scripts/build.js index 9e1e755b3077a3..0993f785902464 100644 --- a/packages/kbn-ui-shared-deps/scripts/build.js +++ b/packages/kbn-ui-shared-deps/scripts/build.js @@ -7,9 +7,8 @@ */ const Path = require('path'); -const Fs = require('fs'); -const { run, createFailError, CiStatsReporter } = require('@kbn/dev-utils'); +const { run, createFailError } = require('@kbn/dev-utils'); const webpack = require('webpack'); const Stats = require('webpack/lib/Stats'); const del = require('del'); @@ -34,34 +33,6 @@ run( const took = Math.round((stats.endTime - stats.startTime) / 1000); if (!stats.hasErrors() && !stats.hasWarnings()) { - if (!flags.dev) { - const reporter = CiStatsReporter.fromEnv(log); - - const metrics = [ - { - group: '@kbn/ui-shared-deps asset size', - id: 'kbn-ui-shared-deps.js', - value: Fs.statSync(Path.resolve(DIST_DIR, 'kbn-ui-shared-deps.js')).size, - }, - { - group: '@kbn/ui-shared-deps asset size', - id: 'kbn-ui-shared-deps.@elastic.js', - value: Fs.statSync(Path.resolve(DIST_DIR, 'kbn-ui-shared-deps.@elastic.js')).size, - }, - { - group: '@kbn/ui-shared-deps asset size', - id: 'css', - value: - Fs.statSync(Path.resolve(DIST_DIR, 'kbn-ui-shared-deps.css')).size + - Fs.statSync(Path.resolve(DIST_DIR, 'kbn-ui-shared-deps.v7.light.css')).size, - }, - ]; - - log.debug('metrics:', metrics); - - await reporter.metrics(metrics); - } - log.success(`webpack completed in about ${took} seconds`); return; } @@ -101,6 +72,7 @@ run( return; } + log.info('running webpack'); await onCompilationComplete( await new Promise((resolve, reject) => { compiler.run((error, stats) => { diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 7ff5978e1f2ea2..cc761dae3bfe96 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -12,6 +12,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); const { REPO_ROOT } = require('@kbn/utils'); const webpack = require('webpack'); +const { RawSource } = require('webpack-sources'); const UiSharedDeps = require('./index'); @@ -145,6 +146,36 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ test: /\.(js|css)$/, cache: false, }), + new (class MetricsPlugin { + apply(compiler) { + compiler.hooks.emit.tap('MetricsPlugin', (compilation) => { + const metrics = [ + { + group: '@kbn/ui-shared-deps asset size', + id: 'kbn-ui-shared-deps.js', + value: compilation.assets['kbn-ui-shared-deps.js'].size(), + }, + { + group: '@kbn/ui-shared-deps asset size', + id: 'kbn-ui-shared-deps.@elastic.js', + value: compilation.assets['kbn-ui-shared-deps.@elastic.js'].size(), + }, + { + group: '@kbn/ui-shared-deps asset size', + id: 'css', + value: + compilation.assets['kbn-ui-shared-deps.css'].size() + + compilation.assets['kbn-ui-shared-deps.v7.light.css'].size(), + }, + ]; + + compilation.emitAsset( + 'metrics.json', + new RawSource(JSON.stringify(metrics, null, 2)) + ); + }); + } + })(), ]), ], }); diff --git a/renovate.json5 b/renovate.json5 index f1e773427a1034..415aa71fc3820f 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -14,7 +14,6 @@ ], labels: [ 'release_note:skip', - 'Team:Operations', 'renovate', 'v8.0.0', 'v7.11.0', @@ -22,7 +21,6 @@ major: { labels: [ 'release_note:skip', - 'Team:Operations', 'renovate', 'v8.0.0', 'v7.11.0', diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 80e23a32ca5570..575a247ffeccb5 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -715,8 +715,11 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index 7e79725c20307b..d52cc090d5d195 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -11,21 +11,19 @@ Array [ exports[`ModalService openConfirm() renders a mountpoint confirm message 1`] = ` Array [ Array [ - - - - - - - , + + + + + ,
, ], ] @@ -36,18 +34,16 @@ exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = ` exports[`ModalService openConfirm() renders a string confirm message 1`] = ` Array [ Array [ - - - - Some message - - - , + + + Some message + + ,
, ], ] @@ -58,33 +54,29 @@ exports[`ModalService openConfirm() renders a string confirm message 2`] = `" - - - confirm 1 - - - , + + + confirm 1 + + ,
, ], Array [ - - - - some confirm - - - , + + + some confirm + + ,
, ], ] @@ -93,33 +85,29 @@ Array [ exports[`ModalService openConfirm() with a currently active modal replaces the current modal with the new confirm 1`] = ` Array [ Array [ - - - - - - - , + + + + + ,
, ], Array [ - - - - some confirm - - - , + + + some confirm + + ,
, ], ] @@ -128,18 +116,16 @@ Array [ exports[`ModalService openModal() renders a modal to the DOM 1`] = ` Array [ Array [ - - - - - - - , + + + + + ,
, ], ] @@ -150,33 +136,29 @@ exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
- - - confirm 1 - - - , + + + confirm 1 + + ,
, ], Array [ - - - - some confirm - - - , + + + some confirm + + ,
, ], ] @@ -185,33 +167,29 @@ Array [ exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = ` Array [ Array [ - - - - - - - , + + + + + ,
, ], Array [ - - - - - - - , + + + + + ,
, ], ] diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index 1f96e00fef0f89..7e4aee94c958ec 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -9,7 +9,7 @@ /* eslint-disable max-classes-per-file */ import { i18n as t } from '@kbn/i18n'; -import { EuiModal, EuiConfirmModal, EuiOverlayMask, EuiConfirmModalProps } from '@elastic/eui'; +import { EuiModal, EuiConfirmModal, EuiConfirmModalProps } from '@elastic/eui'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; @@ -137,13 +137,11 @@ export class ModalService { this.activeModal = modal; render( - - - modal.close()}> - - - - , + + modal.close()}> + + + , targetDomElement ); @@ -199,11 +197,9 @@ export class ModalService { }; render( - - - - - , + + + , targetDomElement ); }); diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index bd5f23b1c09bc7..e57d8d90a02dcc 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -226,7 +226,7 @@ export class CoreUsageDataService implements CoreService acc.add(a.kind), new Set()) + .reduce((acc, a) => acc.add(a.type), new Set()) .values() ), loggersConfiguredCount: this.loggingConfig?.loggers.length ?? 0, diff --git a/src/core/server/http/integration_tests/logging.test.ts b/src/core/server/http/integration_tests/logging.test.ts index ba265c1ff61bc2..fcf2cd2ba3372d 100644 --- a/src/core/server/http/integration_tests/logging.test.ts +++ b/src/core/server/http/integration_tests/logging.test.ts @@ -50,16 +50,16 @@ describe('request logging', () => { silent: true, appenders: { 'test-console': { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', pattern: '%level|%logger|%message|%meta', }, }, }, loggers: [ { - context: 'http.server.response', + name: 'http.server.response', appenders: ['test-console'], level: 'debug', }, @@ -96,16 +96,16 @@ describe('request logging', () => { silent: true, appenders: { 'test-console': { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', pattern: '%level|%logger|%message|%meta', }, }, }, loggers: [ { - context: 'http.server.response', + name: 'http.server.response', appenders: ['test-console'], level: 'debug', }, diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts index 321eb81708f1e7..88c45962ce4a68 100644 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -29,16 +29,16 @@ function createRoot(legacyLoggingConfig: LegacyLoggingConfig = {}) { // platform config appenders: { 'test-console': { - kind: 'console', + type: 'console', layout: { highlight: false, - kind: 'pattern', + type: 'pattern', }, }, }, loggers: [ { - context: 'test-file', + name: 'test-file', appenders: ['test-console'], level: 'info', }, diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts index 1b76b6748a5bb6..9213403d72d07a 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts @@ -16,13 +16,13 @@ afterEach(() => (LegacyLoggingServer as any).mockClear()); test('`configSchema` creates correct schema.', () => { const appenderSchema = LegacyAppender.configSchema; - const validConfig = { kind: 'legacy-appender', legacyLoggingConfig: { verbose: true } }; + const validConfig = { type: 'legacy-appender', legacyLoggingConfig: { verbose: true } }; expect(appenderSchema.validate(validConfig)).toEqual({ - kind: 'legacy-appender', + type: 'legacy-appender', legacyLoggingConfig: { verbose: true }, }); - const wrongConfig = { kind: 'not-legacy-appender' }; + const wrongConfig = { type: 'not-legacy-appender' }; expect(() => appenderSchema.validate(wrongConfig)).toThrow(); }); diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts index 83e43999eeebf1..a89441a5671b55 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.ts @@ -12,7 +12,7 @@ import { DisposableAppender, LogRecord } from '@kbn/logging'; import { LegacyVars } from '../../types'; export interface LegacyAppenderConfig { - kind: 'legacy-appender'; + type: 'legacy-appender'; legacyLoggingConfig?: any; } @@ -22,7 +22,7 @@ export interface LegacyAppenderConfig { */ export class LegacyAppender implements DisposableAppender { public static configSchema = schema.object({ - kind: schema.literal('legacy-appender'), + type: schema.literal('legacy-appender'), legacyLoggingConfig: schema.any(), }); diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 9e3da1f3e0d715..385d1fd91a5d75 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -24,7 +24,7 @@ Kibana logging system has three main components: _loggers_, _appenders_ and _lay messages according to message type and level, and to control how these messages are formatted and where the final logs will be displayed or stored. -__Loggers__ define what logging settings should be applied at the particular context. +__Loggers__ define what logging settings should be applied at the particular context name. __Appenders__ define where log messages are displayed (eg. stdout or console) and stored (eg. file on the disk). @@ -33,17 +33,17 @@ __Layouts__ define how log messages are formatted and what type of information t ## Logger hierarchy -Every logger has its unique name or context that follows hierarchical naming rule. The logger is considered to be an +Every logger has its unique context name that follows hierarchical naming rule. The logger is considered to be an ancestor of another logger if its name followed by a `.` is a prefix of the descendant logger name. For example logger -with `a.b` context is an ancestor of logger with `a.b.c` context. All top-level loggers are descendants of special -logger with `root` context that resides at the top of the logger hierarchy. This logger always exists and +with `a.b` context name is an ancestor of logger with `a.b.c` context name. All top-level loggers are descendants of special +logger with `root` context name that resides at the top of the logger hierarchy. This logger always exists and fully configured. -Developer can configure _log level_ and _appenders_ that should be used within particular context. If logger configuration +Developer can configure _log level_ and _appenders_ that should be used within particular context name. If logger configuration specifies only _log level_ then _appenders_ configuration will be inherited from the ancestor logger. __Note:__ in the current implementation log messages are only forwarded to appenders configured for a particular logger -context or to appenders of the closest ancestor if current logger doesn't have any appenders configured. That means that +context name or to appenders of the closest ancestor if current logger doesn't have any appenders configured. That means that we __don't support__ so called _appender additivity_ when log messages are forwarded to _every_ distinct appender within ancestor chain including `root`. @@ -55,7 +55,7 @@ A log record is being logged by the logger if its level is higher than or equal the log record is ignored. The _all_ and _off_ levels can be used only in configuration and are just handy shortcuts that allow developer to log every -log record or disable logging entirely for the specific context. +log record or disable logging entirely for the specific context name. ## Layouts @@ -129,7 +129,7 @@ Example of `%date` output: Outputs the process ID. ### JSON layout -With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context, message +With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context name, message text and any other metadata that may be associated with the log message itself. ## Appenders @@ -153,15 +153,15 @@ This policy will rotate the file when it reaches a predetermined size. logging: appenders: rolling-file: - kind: rolling-file - path: /var/logs/kibana.log + type: rolling-file + fileName: /var/logs/kibana.log policy: - kind: size-limit + type: size-limit size: 50mb strategy: //... layout: - kind: pattern + type: pattern ``` The options are: @@ -180,16 +180,16 @@ This policy will rotate the file every given interval of time. logging: appenders: rolling-file: - kind: rolling-file - path: /var/logs/kibana.log + type: rolling-file + fileName: /var/logs/kibana.log policy: - kind: time-interval + type: time-interval interval: 10s modulate: true strategy: //... layout: - kind: pattern + type: pattern ``` The options are: @@ -225,16 +225,16 @@ and will retains a fixed amount of rolled files. logging: appenders: rolling-file: - kind: rolling-file - path: /var/logs/kibana.log + type: rolling-file + fileName: /var/logs/kibana.log policy: // ... strategy: - kind: numeric + type: numeric pattern: '-%i' max: 2 layout: - kind: pattern + type: pattern ``` For example, with this configuration: @@ -253,7 +253,7 @@ The options are: The suffix to append to the file path when rolling. Must include `%i`, as this is the value that will be converted to the file index. -for example, with `path: /var/logs/kibana.log` and `pattern: '-%i'`, the created rolling files +for example, with `fileName: /var/logs/kibana.log` and `pattern: '-%i'`, the created rolling files will be `/var/logs/kibana-1.log`, `/var/logs/kibana-2.log`, and so on. The default value is `-%i` @@ -278,49 +278,49 @@ Here is the configuration example that can be used to configure _loggers_, _appe logging: appenders: console: - kind: console + type: console layout: - kind: pattern + type: pattern highlight: true file: - kind: file - path: /var/log/kibana.log + type: file + fileName: /var/log/kibana.log layout: - kind: pattern + type: pattern custom: - kind: console + type: console layout: - kind: pattern + type: pattern pattern: "[%date][%level] %message" json-file-appender: - kind: file - path: /var/log/kibana-json.log + type: file + fileName: /var/log/kibana-json.log root: appenders: [console, file] level: error loggers: - - context: plugins + - name: plugins appenders: [custom] level: warn - - context: plugins.myPlugin + - name: plugins.myPlugin level: info - - context: server + - name: server level: fatal - - context: optimize + - name: optimize appenders: [console] - - context: telemetry + - name: telemetry level: all appenders: [json-file-appender] - - context: metrics.ops + - name: metrics.ops level: debug appenders: [console] ``` Here is what we get with the config above: -| Context | Appenders | Level | +| Context name | Appenders | Level | | ---------------- |:------------------------:| -----:| | root | console, file | error | | plugins | custom | warn | @@ -331,7 +331,7 @@ Here is what we get with the config above: | metrics.ops | console | debug | -The `root` logger has a dedicated configuration node since this context is special and should always exist. By +The `root` logger has a dedicated configuration node since this context name is special and should always exist. By default `root` is configured with `info` level and `default` appender that is also always available. This is the configuration that all custom loggers will use unless they're re-configured explicitly. @@ -391,7 +391,7 @@ The message contains some high-level information, and the corresponding log meta ## Usage -Usage is very straightforward, one should just get a logger for a specific context and use it to log messages with +Usage is very straightforward, one should just get a logger for a specific context name and use it to log messages with different log level. ```typescript @@ -409,7 +409,7 @@ loggerWithNestedContext.trace('Message with `trace` log level.'); loggerWithNestedContext.debug('Message with `debug` log level.'); ``` -And assuming logger for `server` context with `console` appender and `trace` level was used, console output will look like this: +And assuming logger for `server` name with `console` appender and `trace` level was used, console output will look like this: ```bash [2017-07-25T11:54:41.639-07:00][TRACE][server] Message with `trace` log level. [2017-07-25T11:54:41.639-07:00][DEBUG][server] Message with `debug` log level. @@ -422,7 +422,7 @@ And assuming logger for `server` context with `console` appender and `trace` lev [2017-07-25T11:54:41.639-07:00][DEBUG][server.http] Message with `debug` log level. ``` -The log will be less verbose with `warn` level for the `server` context: +The log will be less verbose with `warn` level for the `server` context name: ```bash [2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. [2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. @@ -433,7 +433,7 @@ The log will be less verbose with `warn` level for the `server` context: Compatibility with the legacy logging system is assured until the end of the `v7` version. All log messages handled by `root` context are forwarded to the legacy logging service. If you re-write root appenders, make sure that it contains `default` appender to provide backward compatibility. -**Note**: If you define an appender for a context, the log messages aren't handled by the +**Note**: If you define an appender for a context name, the log messages aren't handled by the `root` context anymore and not forwarded to the legacy logging service. #### logging.dest @@ -442,21 +442,21 @@ define a custom one. ```yaml logging: loggers: - - context: plugins.myPlugin + - name: plugins.myPlugin appenders: [console] ``` -Logs in a *file* if given file path. You should define a custom appender with `kind: file` +Logs in a *file* if given file path. You should define a custom appender with `type: file` ```yaml logging: appenders: file: - kind: file - path: /var/log/kibana.log + type: file + fileName: /var/log/kibana.log layout: - kind: pattern + type: pattern loggers: - - context: plugins.myPlugin + - name: plugins.myPlugin appenders: [file] ``` #### logging.json @@ -468,7 +468,7 @@ Suppresses all logging output other than error messages. With new logging, confi with adjusting minimum required [logging level](#log-level). ```yaml loggers: - - context: plugins.myPlugin + - name: plugins.myPlugin appenders: [console] level: error # or for all output @@ -494,32 +494,32 @@ to [specify timezone](#date) for `layout: pattern`. Defaults to host timezone wh logging: appenders: custom-console: - kind: console + type: console layout: - kind: pattern + type: pattern highlight: true pattern: "[%level] [%date{ISO8601_TZ}{America/Los_Angeles}][%logger] %message" ``` #### logging.events -Define a custom logger for a specific context. +Define a custom logger for a specific context name. **`logging.events.ops`** outputs sample system and process information at a regular interval. -With the new logging config, these are provided by a dedicated [context](#logger-hierarchy), +With the new logging config, these are provided by a dedicated [context name](#logger-hierarchy), and you can enable them by adjusting the minimum required [logging level](#log-level) to `debug`: ```yaml loggers: - - context: metrics.ops + - name: metrics.ops appenders: [console] level: debug ``` **`logging.events.request` and `logging.events.response`** provide logs for each request handled -by the http service. With the new logging config, these are provided by a dedicated [context](#logger-hierarchy), +by the http service. With the new logging config, these are provided by a dedicated [context name](#logger-hierarchy), and you can enable them by adjusting the minimum required [logging level](#log-level) to `debug`: ```yaml loggers: - - context: http.server.response + - name: http.server.response appenders: [console] level: debug ``` @@ -532,7 +532,7 @@ TBD | Parameter | Platform log record in **pattern** format | Legacy Platform log record **text** format | | --------------- | ------------------------------------------ | ------------------------------------------ | | @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | Absolute `23:33:22.011` | -| context | `parent.child` | `['parent', 'child']` | +| context name | `parent.child` | `['parent', 'child']` | | level | `DEBUG` | `['debug']` | | meta | stringified JSON object `{"to": "v8"}` | N/A | | pid | can be configured as `%pid` | N/A | @@ -540,9 +540,9 @@ TBD | Parameter | Platform log record in **json** format | Legacy Platform log record **json** format | | --------------- | ------------------------------------------ | -------------------------------------------- | | @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | ISO8601 `2012-01-31T23:33:22.011Z` | -| context | `context: parent.child` | `tags: ['parent', 'child']` | -| level | `level: DEBUG` | `tags: ['debug']` | +| context name | `log.logger: parent.child` | `tags: ['parent', 'child']` | +| level | `log.level: DEBUG` | `tags: ['debug']` | | meta | separate property `"meta": {"to": "v8"}` | merged in log record `{... "to": "v8"}` | -| pid | `pid: 12345` | `pid: 12345` | +| pid | `process.pid: 12345` | `pid: 12345` | | type | N/A | `type: log` | | error | `{ message, name, stack }` | `{ message, name, stack, code, signal }` | diff --git a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap index 8013aec4a06fd3..81321a3b1fe44c 100644 --- a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap @@ -84,7 +84,7 @@ Object { } `; -exports[`uses \`root\` logger if context is not specified. 1`] = ` +exports[`uses \`root\` logger if context name is not specified. 1`] = ` Array [ Array [ "[2012-01-31T03:33:22.011-05:00][INFO ][root] This message goes to a root context.", diff --git a/src/core/server/logging/appenders/appenders.test.mocks.ts b/src/core/server/logging/appenders/appenders.test.mocks.ts index 85a86ff9306c7b..1427cd7220de71 100644 --- a/src/core/server/logging/appenders/appenders.test.mocks.ts +++ b/src/core/server/logging/appenders/appenders.test.mocks.ts @@ -12,7 +12,7 @@ jest.mock('../layouts/layouts', () => { const { schema } = require('@kbn/config-schema'); return { Layouts: { - configSchema: schema.object({ kind: schema.literal('mock') }), + configSchema: schema.object({ type: schema.literal('mock') }), create: mockCreateLayout, }, }; diff --git a/src/core/server/logging/appenders/appenders.test.ts b/src/core/server/logging/appenders/appenders.test.ts index 8e1c18ae3ded66..bd32e4061049bd 100644 --- a/src/core/server/logging/appenders/appenders.test.ts +++ b/src/core/server/logging/appenders/appenders.test.ts @@ -21,33 +21,33 @@ beforeEach(() => { test('`configSchema` creates correct schema.', () => { const appendersSchema = Appenders.configSchema; - const validConfig1 = { kind: 'file', layout: { kind: 'mock' }, path: 'path' }; + const validConfig1 = { type: 'file', layout: { type: 'mock' }, fileName: 'path' }; expect(appendersSchema.validate(validConfig1)).toEqual({ - kind: 'file', - layout: { kind: 'mock' }, - path: 'path', + type: 'file', + layout: { type: 'mock' }, + fileName: 'path', }); - const validConfig2 = { kind: 'console', layout: { kind: 'mock' } }; + const validConfig2 = { type: 'console', layout: { type: 'mock' } }; expect(appendersSchema.validate(validConfig2)).toEqual({ - kind: 'console', - layout: { kind: 'mock' }, + type: 'console', + layout: { type: 'mock' }, }); const wrongConfig1 = { - kind: 'console', - layout: { kind: 'mock' }, - path: 'path', + type: 'console', + layout: { type: 'mock' }, + fileName: 'path', }; expect(() => appendersSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'file', layout: { kind: 'mock' } }; + const wrongConfig2 = { type: 'file', layout: { type: 'mock' } }; expect(() => appendersSchema.validate(wrongConfig2)).toThrow(); const wrongConfig3 = { - kind: 'console', - layout: { kind: 'mock' }, - path: 'path', + type: 'console', + layout: { type: 'mock' }, + fileName: 'path', }; expect(() => appendersSchema.validate(wrongConfig3)).toThrow(); }); @@ -56,31 +56,31 @@ test('`create()` creates correct appender.', () => { mockCreateLayout.mockReturnValue({ format: () => '' }); const consoleAppender = Appenders.create({ - kind: 'console', - layout: { highlight: true, kind: 'pattern', pattern: '' }, + type: 'console', + layout: { highlight: true, type: 'pattern', pattern: '' }, }); expect(consoleAppender).toBeInstanceOf(ConsoleAppender); const fileAppender = Appenders.create({ - kind: 'file', - layout: { highlight: true, kind: 'pattern', pattern: '' }, - path: 'path', + type: 'file', + layout: { highlight: true, type: 'pattern', pattern: '' }, + fileName: 'path', }); expect(fileAppender).toBeInstanceOf(FileAppender); const legacyAppender = Appenders.create({ - kind: 'legacy-appender', + type: 'legacy-appender', legacyLoggingConfig: { verbose: true }, }); expect(legacyAppender).toBeInstanceOf(LegacyAppender); const rollingFileAppender = Appenders.create({ - kind: 'rolling-file', - path: 'path', - layout: { highlight: true, kind: 'pattern', pattern: '' }, - strategy: { kind: 'numeric', max: 5, pattern: '%i' }, - policy: { kind: 'size-limit', size: ByteSizeValue.parse('15b') }, + type: 'rolling-file', + fileName: 'path', + layout: { highlight: true, type: 'pattern', pattern: '' }, + strategy: { type: 'numeric', max: 5, pattern: '%i' }, + policy: { type: 'size-limit', size: ByteSizeValue.parse('15b') }, }); expect(rollingFileAppender).toBeInstanceOf(RollingFileAppender); }); diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index 564def5251c132..a41a6a2f68fa1b 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -52,11 +52,11 @@ export class Appenders { * @returns Fully constructed `Appender` instance. */ public static create(config: AppenderConfigType): DisposableAppender { - switch (config.kind) { + switch (config.type) { case 'console': return new ConsoleAppender(Layouts.create(config.layout)); case 'file': - return new FileAppender(Layouts.create(config.layout), config.path); + return new FileAppender(Layouts.create(config.layout), config.fileName); case 'rolling-file': return new RollingFileAppender(config); case 'legacy-appender': diff --git a/src/core/server/logging/appenders/console/console_appender.test.ts b/src/core/server/logging/appenders/console/console_appender.test.ts index f5ad853775eea3..1e8f742c1ecda6 100644 --- a/src/core/server/logging/appenders/console/console_appender.test.ts +++ b/src/core/server/logging/appenders/console/console_appender.test.ts @@ -12,7 +12,7 @@ jest.mock('../../layouts/layouts', () => { return { Layouts: { configSchema: schema.object({ - kind: schema.literal('mock'), + type: schema.literal('mock'), }), }, }; @@ -23,16 +23,16 @@ import { ConsoleAppender } from './console_appender'; test('`configSchema` creates correct schema.', () => { const appenderSchema = ConsoleAppender.configSchema; - const validConfig = { kind: 'console', layout: { kind: 'mock' } }; + const validConfig = { type: 'console', layout: { type: 'mock' } }; expect(appenderSchema.validate(validConfig)).toEqual({ - kind: 'console', - layout: { kind: 'mock' }, + type: 'console', + layout: { type: 'mock' }, }); - const wrongConfig1 = { kind: 'not-console', layout: { kind: 'mock' } }; + const wrongConfig1 = { type: 'not-console', layout: { type: 'mock' } }; expect(() => appenderSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'file', layout: { kind: 'mock' }, path: 'path' }; + const wrongConfig2 = { type: 'file', layout: { type: 'mock' }, fileName: 'path' }; expect(() => appenderSchema.validate(wrongConfig2)).toThrow(); }); diff --git a/src/core/server/logging/appenders/console/console_appender.ts b/src/core/server/logging/appenders/console/console_appender.ts index 00d26d0836ee38..739068ff0a126c 100644 --- a/src/core/server/logging/appenders/console/console_appender.ts +++ b/src/core/server/logging/appenders/console/console_appender.ts @@ -13,7 +13,7 @@ import { Layouts, LayoutConfigType } from '../../layouts/layouts'; const { literal, object } = schema; export interface ConsoleAppenderConfig { - kind: 'console'; + type: 'console'; layout: LayoutConfigType; } @@ -24,7 +24,7 @@ export interface ConsoleAppenderConfig { */ export class ConsoleAppender implements DisposableAppender { public static configSchema = object({ - kind: literal('console'), + type: literal('console'), layout: Layouts.configSchema, }); diff --git a/src/core/server/logging/appenders/file/file_appender.test.mocks.ts b/src/core/server/logging/appenders/file/file_appender.test.mocks.ts index 0f87829dbbaf16..2c2a2015b6fd38 100644 --- a/src/core/server/logging/appenders/file/file_appender.test.mocks.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.mocks.ts @@ -12,7 +12,7 @@ jest.mock('../../layouts/layouts', () => { return { Layouts: { configSchema: schema.object({ - kind: schema.literal('mock'), + type: schema.literal('mock'), }), }, }; diff --git a/src/core/server/logging/appenders/file/file_appender.test.ts b/src/core/server/logging/appenders/file/file_appender.test.ts index 5ef91b98e92f42..081cb16afd2ff3 100644 --- a/src/core/server/logging/appenders/file/file_appender.test.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.ts @@ -20,24 +20,24 @@ beforeEach(() => { test('`createConfigSchema()` creates correct schema.', () => { const appenderSchema = FileAppender.configSchema; - const validConfig = { kind: 'file', layout: { kind: 'mock' }, path: 'path' }; + const validConfig = { type: 'file', layout: { type: 'mock' }, fileName: 'path' }; expect(appenderSchema.validate(validConfig)).toEqual({ - kind: 'file', - layout: { kind: 'mock' }, - path: 'path', + type: 'file', + layout: { type: 'mock' }, + fileName: 'path', }); const wrongConfig1 = { - kind: 'not-file', - layout: { kind: 'mock' }, - path: 'path', + type: 'not-file', + layout: { type: 'mock' }, + fileName: 'path', }; expect(() => appenderSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'file', layout: { kind: 'mock' } }; + const wrongConfig2 = { type: 'file', layout: { type: 'mock' } }; expect(() => appenderSchema.validate(wrongConfig2)).toThrow(); - const wrongConfig3 = { kind: 'console', layout: { kind: 'mock' } }; + const wrongConfig3 = { type: 'console', layout: { type: 'mock' } }; expect(() => appenderSchema.validate(wrongConfig3)).toThrow(); }); diff --git a/src/core/server/logging/appenders/file/file_appender.ts b/src/core/server/logging/appenders/file/file_appender.ts index 0f1cb71c76e9fb..be46c261dc9965 100644 --- a/src/core/server/logging/appenders/file/file_appender.ts +++ b/src/core/server/logging/appenders/file/file_appender.ts @@ -13,9 +13,9 @@ import { createWriteStream, WriteStream } from 'fs'; import { Layouts, LayoutConfigType } from '../../layouts/layouts'; export interface FileAppenderConfig { - kind: 'file'; + type: 'file'; layout: LayoutConfigType; - path: string; + fileName: string; } /** @@ -24,9 +24,9 @@ export interface FileAppenderConfig { */ export class FileAppender implements DisposableAppender { public static configSchema = schema.object({ - kind: schema.literal('file'), + type: schema.literal('file'), layout: Layouts.configSchema, - path: schema.string(), + fileName: schema.string(), }); /** diff --git a/src/core/server/logging/appenders/rolling_file/policies/index.ts b/src/core/server/logging/appenders/rolling_file/policies/index.ts index 20038d31eee8bf..e3e33c6cbfdef6 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/index.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/index.ts @@ -34,7 +34,7 @@ export type TriggeringPolicyConfig = | TimeIntervalTriggeringPolicyConfig; const defaultPolicy: TimeIntervalTriggeringPolicyConfig = { - kind: 'time-interval', + type: 'time-interval', interval: moment.duration(24, 'hour'), modulate: true, }; @@ -48,7 +48,7 @@ export const createTriggeringPolicy = ( config: TriggeringPolicyConfig, context: RollingFileContext ): TriggeringPolicy => { - switch (config.kind) { + switch (config.type) { case 'size-limit': return new SizeLimitTriggeringPolicy(config, context); case 'time-interval': diff --git a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts index 3780bb69a8341a..ee9c96de8a940c 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts @@ -15,7 +15,7 @@ describe('SizeLimitTriggeringPolicy', () => { let context: RollingFileContext; const createPolicy = (size: ByteSizeValue) => - new SizeLimitTriggeringPolicy({ kind: 'size-limit', size }, context); + new SizeLimitTriggeringPolicy({ type: 'size-limit', size }, context); const createLogRecord = (parts: Partial = {}): LogRecord => ({ timestamp: new Date(), diff --git a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts index 77f0a60b0e95c8..82fee352da8df7 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts @@ -12,7 +12,7 @@ import { RollingFileContext } from '../../rolling_file_context'; import { TriggeringPolicy } from '../policy'; export interface SizeLimitTriggeringPolicyConfig { - kind: 'size-limit'; + type: 'size-limit'; /** * The minimum size the file must have to roll over. @@ -21,7 +21,7 @@ export interface SizeLimitTriggeringPolicyConfig { } export const sizeLimitTriggeringPolicyConfigSchema = schema.object({ - kind: schema.literal('size-limit'), + type: schema.literal('size-limit'), size: schema.byteSize({ min: '1b', defaultValue: '100mb' }), }); diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts index 25c5cef65c8851..03f457277b7926 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts @@ -42,7 +42,7 @@ describe('TimeIntervalTriggeringPolicy', () => { interval: string = '15m', modulate: boolean = false ): TimeIntervalTriggeringPolicyConfig => ({ - kind: 'time-interval', + type: 'time-interval', interval: schema.duration().validate(interval), modulate, }); diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts index 892dd54672f146..7c4d18d929cb0d 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts @@ -15,7 +15,7 @@ import { getNextRollingTime } from './get_next_rolling_time'; import { isValidRolloverInterval } from './utils'; export interface TimeIntervalTriggeringPolicyConfig { - kind: 'time-interval'; + type: 'time-interval'; /** * How often a rollover should occur. @@ -38,7 +38,7 @@ export interface TimeIntervalTriggeringPolicyConfig { } export const timeIntervalTriggeringPolicyConfigSchema = schema.object({ - kind: schema.literal('time-interval'), + type: schema.literal('time-interval'), interval: schema.duration({ defaultValue: '24h', validate: (interval) => { diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts index bc28e9137b2fd7..a95d995885d8b2 100644 --- a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts @@ -20,20 +20,20 @@ import { LogLevel, LogRecord } from '@kbn/logging'; import { RollingFileAppender, RollingFileAppenderConfig } from './rolling_file_appender'; const config: RollingFileAppenderConfig = { - kind: 'rolling-file', - path: '/var/log/kibana.log', + type: 'rolling-file', + fileName: '/var/log/kibana.log', layout: { - kind: 'pattern', + type: 'pattern', pattern: '%message', highlight: false, }, policy: { - kind: 'time-interval', + type: 'time-interval', interval: moment.duration(4, 'hour'), modulate: true, }, strategy: { - kind: 'numeric', + type: 'numeric', max: 5, pattern: '-%i', }, @@ -99,7 +99,7 @@ describe('RollingFileAppender', () => { it('constructs its delegates with the correct parameters', () => { expect(RollingFileContextMock).toHaveBeenCalledTimes(1); - expect(RollingFileContextMock).toHaveBeenCalledWith(config.path); + expect(RollingFileContextMock).toHaveBeenCalledWith(config.fileName); expect(RollingFileManagerMock).toHaveBeenCalledTimes(1); expect(RollingFileManagerMock).toHaveBeenCalledWith(context); diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts index 748f47504f00ad..452d9493359544 100644 --- a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts @@ -26,7 +26,7 @@ import { RollingFileManager } from './rolling_file_manager'; import { RollingFileContext } from './rolling_file_context'; export interface RollingFileAppenderConfig { - kind: 'rolling-file'; + type: 'rolling-file'; /** * The layout to use when writing log entries */ @@ -34,7 +34,7 @@ export interface RollingFileAppenderConfig { /** * The absolute path of the file to write to. */ - path: string; + fileName: string; /** * The {@link TriggeringPolicy | policy} to use to determine if a rollover should occur. */ @@ -51,9 +51,9 @@ export interface RollingFileAppenderConfig { */ export class RollingFileAppender implements DisposableAppender { public static configSchema = schema.object({ - kind: schema.literal('rolling-file'), + type: schema.literal('rolling-file'), layout: Layouts.configSchema, - path: schema.string(), + fileName: schema.string(), policy: triggeringPolicyConfigSchema, strategy: rollingStrategyConfigSchema, }); @@ -70,7 +70,7 @@ export class RollingFileAppender implements DisposableAppender { private readonly buffer: BufferAppender; constructor(config: RollingFileAppenderConfig) { - this.context = new RollingFileContext(config.path); + this.context = new RollingFileContext(config.fileName); this.context.refreshFileInfo(); this.fileManager = new RollingFileManager(this.context); this.layout = Layouts.create(config.layout); diff --git a/src/core/server/logging/appenders/rolling_file/strategies/index.ts b/src/core/server/logging/appenders/rolling_file/strategies/index.ts index f63b68e4b92af9..c8364b0e590c67 100644 --- a/src/core/server/logging/appenders/rolling_file/strategies/index.ts +++ b/src/core/server/logging/appenders/rolling_file/strategies/index.ts @@ -19,7 +19,7 @@ export { RollingStrategy } from './strategy'; export type RollingStrategyConfig = NumericRollingStrategyConfig; const defaultStrategy: NumericRollingStrategyConfig = { - kind: 'numeric', + type: 'numeric', pattern: '-%i', max: 7, }; diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts index d2e65f3880b87f..b4ca0131156a38 100644 --- a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts @@ -27,8 +27,8 @@ describe('NumericRollingStrategy', () => { let context: ReturnType; let strategy: NumericRollingStrategy; - const createStrategy = (config: Omit) => - new NumericRollingStrategy({ ...config, kind: 'numeric' }, context); + const createStrategy = (config: Omit) => + new NumericRollingStrategy({ ...config, type: 'numeric' }, context); beforeEach(() => { context = rollingFileAppenderMocks.createContext(logFilePath); diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts index 5ee75bf6fda52a..13a19a40fa561d 100644 --- a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts @@ -19,10 +19,10 @@ import { } from './rolling_tasks'; export interface NumericRollingStrategyConfig { - kind: 'numeric'; + type: 'numeric'; /** * The suffix pattern to apply when renaming a file. The suffix will be applied - * after the `appender.path` file name, but before the file extension. + * after the `appender.fileName` file name, but before the file extension. * * Must include `%i`, as it is the value that will be converted to the file index * @@ -31,8 +31,8 @@ export interface NumericRollingStrategyConfig { * logging: * appenders: * rolling-file: - * kind: rolling-file - * path: /var/logs/kibana.log + * type: rolling-file + * fileName: /var/logs/kibana.log * strategy: * type: default * pattern: "-%i" @@ -52,7 +52,7 @@ export interface NumericRollingStrategyConfig { } export const numericRollingStrategyConfigSchema = schema.object({ - kind: schema.literal('numeric'), + type: schema.literal('numeric'), pattern: schema.string({ defaultValue: '-%i', validate: (pattern) => { @@ -73,8 +73,8 @@ export const numericRollingStrategyConfigSchema = schema.object({ * logging: * appenders: * rolling-file: - * kind: rolling-file - * path: /kibana.log + * type: rolling-file + * fileName: /kibana.log * strategy: * type: numeric * pattern: "-%i" diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 0af6dbfc8611ec..b4eb98546de21b 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -17,22 +17,22 @@ function createRoot() { silent: true, // set "true" in kbnTestServer appenders: { 'test-console': { - kind: 'console', + type: 'console', layout: { highlight: false, - kind: 'pattern', + type: 'pattern', pattern: '%level|%logger|%message', }, }, }, loggers: [ { - context: 'parent', + name: 'parent', appenders: ['test-console'], level: 'warn', }, { - context: 'parent.child', + name: 'parent.child', appenders: ['test-console'], level: 'error', }, @@ -42,7 +42,7 @@ function createRoot() { } describe('logging service', () => { - describe('logs according to context hierarchy', () => { + describe('logs according to context name hierarchy', () => { let root: ReturnType; let mockConsoleLog: jest.SpyInstance; beforeAll(async () => { @@ -61,7 +61,7 @@ describe('logging service', () => { await root.shutdown(); }); - it('uses the most specific context', () => { + it('uses the most specific context name', () => { const logger = root.logger.get('parent.child'); logger.error('error from "parent.child" context'); @@ -74,7 +74,7 @@ describe('logging service', () => { ); }); - it('uses parent context', () => { + it('uses parent context name', () => { const logger = root.logger.get('parent.another-child'); logger.error('error from "parent.another-child" context'); @@ -104,31 +104,31 @@ describe('logging service', () => { }); }); - describe('custom context configuration', () => { + describe('custom context name configuration', () => { const CUSTOM_LOGGING_CONFIG: LoggerContextConfigInput = { appenders: { customJsonConsole: { - kind: 'console', + type: 'console', layout: { - kind: 'json', + type: 'json', }, }, customPatternConsole: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', pattern: 'CUSTOM - PATTERN [%logger][%level] %message', }, }, }, loggers: [ - { context: 'debug_json', appenders: ['customJsonConsole'], level: 'debug' }, - { context: 'debug_pattern', appenders: ['customPatternConsole'], level: 'debug' }, - { context: 'info_json', appenders: ['customJsonConsole'], level: 'info' }, - { context: 'info_pattern', appenders: ['customPatternConsole'], level: 'info' }, + { name: 'debug_json', appenders: ['customJsonConsole'], level: 'debug' }, + { name: 'debug_pattern', appenders: ['customPatternConsole'], level: 'debug' }, + { name: 'info_json', appenders: ['customJsonConsole'], level: 'info' }, + { name: 'info_pattern', appenders: ['customPatternConsole'], level: 'info' }, { - context: 'all', + name: 'all', appenders: ['customJsonConsole', 'customPatternConsole'], level: 'debug', }, diff --git a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts index fb2a714adb687a..b40ce7a4e7b0e3 100644 --- a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts +++ b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts @@ -25,7 +25,7 @@ function createRoot(appenderConfig: any) { }, loggers: [ { - context: 'test.rolling.file', + name: 'test.rolling.file', appenders: ['rolling-file'], level: 'debug', }, @@ -63,18 +63,18 @@ describe('RollingFileAppender', () => { describe('`size-limit` policy with `numeric` strategy', () => { it('rolls the log file in the correct order', async () => { root = createRoot({ - kind: 'rolling-file', - path: logFile, + type: 'rolling-file', + fileName: logFile, layout: { - kind: 'pattern', + type: 'pattern', pattern: '%message', }, policy: { - kind: 'size-limit', + type: 'size-limit', size: '100b', }, strategy: { - kind: 'numeric', + type: 'numeric', max: 5, pattern: '.%i', }, @@ -108,18 +108,18 @@ describe('RollingFileAppender', () => { it('only keep the correct number of files', async () => { root = createRoot({ - kind: 'rolling-file', - path: logFile, + type: 'rolling-file', + fileName: logFile, layout: { - kind: 'pattern', + type: 'pattern', pattern: '%message', }, policy: { - kind: 'size-limit', + type: 'size-limit', size: '60b', }, strategy: { - kind: 'numeric', + type: 'numeric', max: 2, pattern: '-%i', }, @@ -157,19 +157,19 @@ describe('RollingFileAppender', () => { describe('`time-interval` policy with `numeric` strategy', () => { it('rolls the log file at the given interval', async () => { root = createRoot({ - kind: 'rolling-file', - path: logFile, + type: 'rolling-file', + fileName: logFile, layout: { - kind: 'pattern', + type: 'pattern', pattern: '%message', }, policy: { - kind: 'time-interval', + type: 'time-interval', interval: '1s', modulate: true, }, strategy: { - kind: 'numeric', + type: 'numeric', max: 2, pattern: '-%i', }, diff --git a/src/core/server/logging/layouts/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts index 2504ad476576fb..e55f69daab1100 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -63,7 +63,7 @@ const records: LogRecord[] = [ test('`createConfigSchema()` creates correct schema.', () => { const layoutSchema = JsonLayout.configSchema; - expect(layoutSchema.validate({ kind: 'json' })).toEqual({ kind: 'json' }); + expect(layoutSchema.validate({ type: 'json' })).toEqual({ type: 'json' }); }); test('`format()` correctly formats record.', () => { diff --git a/src/core/server/logging/layouts/json_layout.ts b/src/core/server/logging/layouts/json_layout.ts index 9e81303bedea07..bb8423f8240af9 100644 --- a/src/core/server/logging/layouts/json_layout.ts +++ b/src/core/server/logging/layouts/json_layout.ts @@ -14,12 +14,12 @@ import { LogRecord, Layout } from '@kbn/logging'; const { literal, object } = schema; const jsonLayoutSchema = object({ - kind: literal('json'), + type: literal('json'), }); /** @internal */ export interface JsonLayoutConfigType { - kind: 'json'; + type: 'json'; } /** diff --git a/src/core/server/logging/layouts/layouts.test.ts b/src/core/server/logging/layouts/layouts.test.ts index df91994564da17..3ff2fe23aae343 100644 --- a/src/core/server/logging/layouts/layouts.test.ts +++ b/src/core/server/logging/layouts/layouts.test.ts @@ -12,43 +12,43 @@ import { PatternLayout } from './pattern_layout'; test('`configSchema` creates correct schema for `pattern` layout.', () => { const layoutsSchema = Layouts.configSchema; - const validConfigWithOptional = { kind: 'pattern' }; + const validConfigWithOptional = { type: 'pattern' }; expect(layoutsSchema.validate(validConfigWithOptional)).toEqual({ highlight: undefined, - kind: 'pattern', + type: 'pattern', pattern: undefined, }); const validConfig = { highlight: true, - kind: 'pattern', + type: 'pattern', pattern: '%message', }; expect(layoutsSchema.validate(validConfig)).toEqual({ highlight: true, - kind: 'pattern', + type: 'pattern', pattern: '%message', }); - const wrongConfig2 = { kind: 'pattern', pattern: 1 }; + const wrongConfig2 = { type: 'pattern', pattern: 1 }; expect(() => layoutsSchema.validate(wrongConfig2)).toThrow(); }); test('`createConfigSchema()` creates correct schema for `json` layout.', () => { const layoutsSchema = Layouts.configSchema; - const validConfig = { kind: 'json' }; - expect(layoutsSchema.validate(validConfig)).toEqual({ kind: 'json' }); + const validConfig = { type: 'json' }; + expect(layoutsSchema.validate(validConfig)).toEqual({ type: 'json' }); }); test('`create()` creates correct layout.', () => { const patternLayout = Layouts.create({ highlight: false, - kind: 'pattern', + type: 'pattern', pattern: '[%date][%level][%logger] %message', }); expect(patternLayout).toBeInstanceOf(PatternLayout); - const jsonLayout = Layouts.create({ kind: 'json' }); + const jsonLayout = Layouts.create({ type: 'json' }); expect(jsonLayout).toBeInstanceOf(JsonLayout); }); diff --git a/src/core/server/logging/layouts/layouts.ts b/src/core/server/logging/layouts/layouts.ts index d6c14f3713b2c6..9abc8cd753f97b 100644 --- a/src/core/server/logging/layouts/layouts.ts +++ b/src/core/server/logging/layouts/layouts.ts @@ -27,7 +27,7 @@ export class Layouts { * @returns Fully constructed `Layout` instance. */ public static create(config: LayoutConfigType): Layout { - switch (config.kind) { + switch (config.type) { case 'json': return new JsonLayout(); diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index 7dd3c7c51f833c..abdc2f4fb929cb 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -66,28 +66,28 @@ expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); test('`createConfigSchema()` creates correct schema.', () => { const layoutSchema = PatternLayout.configSchema; - const validConfigWithOptional = { kind: 'pattern' }; + const validConfigWithOptional = { type: 'pattern' }; expect(layoutSchema.validate(validConfigWithOptional)).toEqual({ highlight: undefined, - kind: 'pattern', + type: 'pattern', pattern: undefined, }); const validConfig = { highlight: true, - kind: 'pattern', + type: 'pattern', pattern: '%message', }; expect(layoutSchema.validate(validConfig)).toEqual({ highlight: true, - kind: 'pattern', + type: 'pattern', pattern: '%message', }); - const wrongConfig1 = { kind: 'json' }; + const wrongConfig1 = { type: 'json' }; expect(() => layoutSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'pattern', pattern: 1 }; + const wrongConfig2 = { type: 'pattern', pattern: 1 }; expect(() => layoutSchema.validate(wrongConfig2)).toThrow(); }); diff --git a/src/core/server/logging/layouts/pattern_layout.ts b/src/core/server/logging/layouts/pattern_layout.ts index a5e9c0be8409bd..a5dc41786c4400 100644 --- a/src/core/server/logging/layouts/pattern_layout.ts +++ b/src/core/server/logging/layouts/pattern_layout.ts @@ -32,7 +32,7 @@ export const patternSchema = schema.string({ const patternLayoutSchema = schema.object({ highlight: schema.maybe(schema.boolean()), - kind: schema.literal('pattern'), + type: schema.literal('pattern'), pattern: schema.maybe(patternSchema), }); @@ -47,7 +47,7 @@ const conversions: Conversion[] = [ /** @internal */ export interface PatternLayoutConfigType { - kind: 'pattern'; + type: 'pattern'; highlight?: boolean; pattern?: string; } diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index e494ae2413229a..2cb5831a8fb4ce 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -51,12 +51,12 @@ test('correctly fills in default config.', () => { expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, }); expect(configValue.appenders.get('console')).toEqual({ - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, }); }); @@ -65,8 +65,8 @@ test('correctly fills in custom `appenders` config.', () => { config.schema.validate({ appenders: { console: { - kind: 'console', - layout: { kind: 'pattern' }, + type: 'console', + layout: { type: 'pattern' }, }, }, }) @@ -75,13 +75,13 @@ test('correctly fills in custom `appenders` config.', () => { expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, }); expect(configValue.appenders.get('console')).toEqual({ - kind: 'console', - layout: { kind: 'pattern' }, + type: 'console', + layout: { type: 'pattern' }, }); }); @@ -91,7 +91,7 @@ test('correctly fills in default `loggers` config.', () => { expect(configValue.loggers.size).toBe(1); expect(configValue.loggers.get('root')).toEqual({ appenders: ['default'], - context: 'root', + name: 'root', level: 'info', }); }); @@ -101,24 +101,24 @@ test('correctly fills in custom `loggers` config.', () => { config.schema.validate({ appenders: { file: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', + type: 'file', + layout: { type: 'pattern' }, + fileName: 'path', }, }, loggers: [ { appenders: ['file'], - context: 'plugins', + name: 'plugins', level: 'warn', }, { - context: 'plugins.pid', + name: 'plugins.pid', level: 'trace', }, { appenders: ['default'], - context: 'http', + name: 'http', level: 'error', }, ], @@ -128,22 +128,22 @@ test('correctly fills in custom `loggers` config.', () => { expect(configValue.loggers.size).toBe(4); expect(configValue.loggers.get('root')).toEqual({ appenders: ['default'], - context: 'root', + name: 'root', level: 'info', }); expect(configValue.loggers.get('plugins')).toEqual({ appenders: ['file'], - context: 'plugins', + name: 'plugins', level: 'warn', }); expect(configValue.loggers.get('plugins.pid')).toEqual({ appenders: ['file'], - context: 'plugins.pid', + name: 'plugins.pid', level: 'trace', }); expect(configValue.loggers.get('http')).toEqual({ appenders: ['default'], - context: 'http', + name: 'http', level: 'error', }); }); @@ -153,7 +153,7 @@ test('fails if loggers use unknown appenders.', () => { loggers: [ { appenders: ['unknown'], - context: 'some.nested.context', + name: 'some.nested.context', }, ], }); @@ -167,9 +167,9 @@ describe('extend', () => { config.schema.validate({ appenders: { file1: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', + type: 'file', + layout: { type: 'pattern' }, + fileName: 'path', }, }, }) @@ -179,9 +179,9 @@ describe('extend', () => { config.schema.validate({ appenders: { file2: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', + type: 'file', + layout: { type: 'pattern' }, + fileName: 'path', }, }, }) @@ -200,9 +200,9 @@ describe('extend', () => { config.schema.validate({ appenders: { file1: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', + type: 'file', + layout: { type: 'pattern' }, + fileName: 'path', }, }, }) @@ -212,18 +212,18 @@ describe('extend', () => { config.schema.validate({ appenders: { file1: { - kind: 'file', - layout: { kind: 'json' }, - path: 'updatedPath', + type: 'file', + layout: { type: 'json' }, + fileName: 'updatedPath', }, }, }) ); expect(mergedConfigValue.appenders.get('file1')).toEqual({ - kind: 'file', - layout: { kind: 'json' }, - path: 'updatedPath', + type: 'file', + layout: { type: 'json' }, + fileName: 'updatedPath', }); }); @@ -232,7 +232,7 @@ describe('extend', () => { config.schema.validate({ loggers: [ { - context: 'plugins', + name: 'plugins', level: 'warn', }, ], @@ -243,7 +243,7 @@ describe('extend', () => { config.schema.validate({ loggers: [ { - context: 'plugins.pid', + name: 'plugins.pid', level: 'trace', }, ], @@ -258,7 +258,7 @@ describe('extend', () => { config.schema.validate({ loggers: [ { - context: 'plugins', + name: 'plugins', level: 'warn', }, ], @@ -270,7 +270,7 @@ describe('extend', () => { loggers: [ { appenders: ['console'], - context: 'plugins', + name: 'plugins', level: 'trace', }, ], @@ -279,7 +279,7 @@ describe('extend', () => { expect(mergedConfigValue.loggers.get('plugins')).toEqual({ appenders: ['console'], - context: 'plugins', + name: 'plugins', level: 'trace', }); }); diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index 5b79b4e8e15d5b..24496289fb4c84 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -51,7 +51,7 @@ const levelSchema = schema.oneOf( */ export const loggerSchema = schema.object({ appenders: schema.arrayOf(schema.string(), { defaultValue: [] }), - context: schema.string(), + name: schema.string(), level: levelSchema, }); @@ -148,15 +148,15 @@ export class LoggingConfig { [ 'default', { - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, } as AppenderConfigType, ], [ 'console', { - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, } as AppenderConfigType, ], ]); @@ -182,8 +182,8 @@ export class LoggingConfig { public extend(contextConfig: LoggerContextConfigType) { // Use a Map to de-dupe any loggers for the same context. contextConfig overrides existing config. const mergedLoggers = new Map([ - ...this.configType.loggers.map((l) => [l.context, l] as [string, LoggerConfigType]), - ...contextConfig.loggers.map((l) => [l.context, l] as [string, LoggerConfigType]), + ...this.configType.loggers.map((l) => [l.name, l] as [string, LoggerConfigType]), + ...contextConfig.loggers.map((l) => [l.name, l] as [string, LoggerConfigType]), ]); const mergedConfig: LoggingConfigType = { @@ -204,13 +204,10 @@ export class LoggingConfig { private fillLoggersConfig(loggingConfig: LoggingConfigType) { // Include `root` logger into common logger list so that it can easily be a part // of the logger hierarchy and put all the loggers in map for easier retrieval. - const loggers = [ - { context: ROOT_CONTEXT_NAME, ...loggingConfig.root }, - ...loggingConfig.loggers, - ]; + const loggers = [{ name: ROOT_CONTEXT_NAME, ...loggingConfig.root }, ...loggingConfig.loggers]; const loggerConfigByContext = new Map( - loggers.map((loggerConfig) => toTuple(loggerConfig.context, loggerConfig)) + loggers.map((loggerConfig) => toTuple(loggerConfig.name, loggerConfig)) ); for (const [loggerContext, loggerConfig] of loggerConfigByContext) { @@ -247,7 +244,7 @@ function getAppenders( loggerConfig: LoggerConfigType, loggerConfigByContext: Map ) { - let currentContext = loggerConfig.context; + let currentContext = loggerConfig.name; let appenders = loggerConfig.appenders; while (appenders.length === 0) { diff --git a/src/core/server/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts index 66f1c67f114024..341a04736b87a4 100644 --- a/src/core/server/logging/logging_service.test.ts +++ b/src/core/server/logging/logging_service.test.ts @@ -30,11 +30,11 @@ describe('LoggingService', () => { it('forwards configuration changes to logging system', () => { const config1: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }], + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], }; const config2: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['default'], level: 'all' }], + loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }], }; setup.configure(['test', 'context'], of(config1, config2)); @@ -54,11 +54,11 @@ describe('LoggingService', () => { const updates$ = new Subject(); const config1: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }], + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], }; const config2: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['default'], level: 'all' }], + loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }], }; setup.configure(['test', 'context'], updates$); @@ -78,7 +78,7 @@ describe('LoggingService', () => { const updates$ = new Subject(); const config1: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }], + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], }; setup.configure(['test', 'context'], updates$); diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index f76533dadd5c84..f5a4717fdbfaf4 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -31,7 +31,7 @@ export interface LoggingServiceSetup { * core.logging.configure( * of({ * appenders: new Map(), - * loggers: [{ context: 'search', appenders: ['default'] }] + * loggers: [{ name: 'search', appenders: ['default'] }] * }) * ) * ``` diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index 4d4ed191e60f8e..f68d6c6a97fbc4 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -46,7 +46,7 @@ test('uses default memory buffer logger until config is provided', () => { const logger = system.get('test', 'context'); logger.trace('trace message'); - // We shouldn't create new buffer appender for another context. + // We shouldn't create new buffer appender for another context name. const anotherLogger = system.get('test', 'context2'); anotherLogger.fatal('fatal message', { some: 'value' }); @@ -69,7 +69,7 @@ test('flushes memory buffer logger and switches to real logger once config is pr // Switch to console appender with `info` level, so that `trace` message won't go through. await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -102,12 +102,12 @@ test('appends records via multiple appenders.', async () => { await system.upgrade( config.schema.validate({ appenders: { - default: { kind: 'console', layout: { kind: 'pattern' } }, - file: { kind: 'file', layout: { kind: 'pattern' }, path: 'path' }, + default: { type: 'console', layout: { type: 'pattern' } }, + file: { type: 'file', layout: { type: 'pattern' }, fileName: 'path' }, }, loggers: [ - { appenders: ['file'], context: 'tests', level: 'warn' }, - { context: 'tests.child', level: 'error' }, + { appenders: ['file'], name: 'tests', level: 'warn' }, + { name: 'tests.child', level: 'error' }, ], }) ); @@ -121,10 +121,10 @@ test('appends records via multiple appenders.', async () => { expect(mockStreamWrite.mock.calls[1][0]).toMatchSnapshot('file logs'); }); -test('uses `root` logger if context is not specified.', async () => { +test('uses `root` logger if context name is not specified.', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } }, + appenders: { default: { type: 'console', layout: { type: 'pattern' } } }, }) ); @@ -137,7 +137,7 @@ test('uses `root` logger if context is not specified.', async () => { test('`stop()` disposes all appenders.', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -156,7 +156,7 @@ test('asLoggerFactory() only allows to create new loggers.', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'all' }, }) ); @@ -180,7 +180,7 @@ test('setContextConfig() updates config with relative contexts', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -189,10 +189,10 @@ test('setContextConfig() updates config with relative contexts', async () => { appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], }); testsLogger.warn('tests log to default!'); @@ -235,7 +235,7 @@ test('setContextConfig() updates config for a root context', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -244,10 +244,10 @@ test('setContextConfig() updates config for a root context', async () => { appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: '', appenders: ['custom'], level: 'debug' }], + loggers: [{ name: '', appenders: ['custom'], level: 'debug' }], }); testsLogger.warn('tests log to default!'); @@ -273,21 +273,21 @@ test('setContextConfig() updates config for a root context', async () => { ); }); -test('custom context configs are applied on subsequent calls to update()', async () => { +test('custom context name configs are applied on subsequent calls to update()', async () => { await system.setContextConfig(['tests', 'child'], { appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], }); // Calling upgrade after setContextConfig should not throw away the context-specific config await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -310,10 +310,10 @@ test('custom context configs are applied on subsequent calls to update()', async ); }); -test('subsequent calls to setContextConfig() for the same context override the previous config', async () => { +test('subsequent calls to setContextConfig() for the same context name override the previous config', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -322,10 +322,10 @@ test('subsequent calls to setContextConfig() for the same context override the p appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], }); // Call again, this time with level: 'warn' and a different pattern @@ -334,12 +334,12 @@ test('subsequent calls to setContextConfig() for the same context override the p [ 'custom', { - kind: 'console', - layout: { kind: 'pattern', pattern: '[%level][%logger] second pattern! %message' }, + type: 'console', + layout: { type: 'pattern', pattern: '[%level][%logger] second pattern! %message' }, }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'warn' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'warn' }], }); const logger = system.get('tests', 'child', 'grandchild'); @@ -360,10 +360,10 @@ test('subsequent calls to setContextConfig() for the same context override the p ); }); -test('subsequent calls to setContextConfig() for the same context can disable the previous config', async () => { +test('subsequent calls to setContextConfig() for the same context name can disable the previous config', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -372,10 +372,10 @@ test('subsequent calls to setContextConfig() for the same context can disable th appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], }); // Call again, this time no customizations (effectively disabling) diff --git a/src/core/server/logging/logging_system.ts b/src/core/server/logging/logging_system.ts index 9c22cea23720b7..9ae434aff41d3c 100644 --- a/src/core/server/logging/logging_system.ts +++ b/src/core/server/logging/logging_system.ts @@ -79,7 +79,7 @@ export class LoggingSystem implements LoggerFactory { * loggingSystem.setContextConfig( * ['plugins', 'data'], * { - * loggers: [{ context: 'search', appenders: ['default'] }] + * loggers: [{ name: 'search', appenders: ['default'] }] * } * ) * ``` @@ -95,9 +95,7 @@ export class LoggingSystem implements LoggerFactory { // Automatically prepend the base context to the logger sub-contexts loggers: contextConfig.loggers.map((l) => ({ ...l, - context: LoggingConfig.getLoggerContext( - l.context.length > 0 ? [context, l.context] : [context] - ), + name: LoggingConfig.getLoggerContext(l.name.length > 0 ? [context, l.name] : [context]), })), }); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 317bfe33b3a199..95a867934307a4 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -45,16 +45,16 @@ describe('migration v2', () => { logging: { appenders: { file: { - kind: 'file', - path: join(__dirname, 'migration_test_kibana.log'), + type: 'file', + fileName: join(__dirname, 'migration_test_kibana.log'), layout: { - kind: 'json', + type: 'json', }, }, }, loggers: [ { - context: 'root', + name: 'root', appenders: ['file'], }, ], diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index 16ba0c855867ce..c26d4593bede19 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -47,16 +47,16 @@ describe.skip('migration from 7.7.2-xpack with 100k objects', () => { logging: { appenders: { file: { - kind: 'file', - path: join(__dirname, 'migration_test_kibana.log'), + type: 'file', + fileName: join(__dirname, 'migration_test_kibana.log'), layout: { - kind: 'json', + type: 'json', }, }, }, loggers: [ { - context: 'root', + name: 'root', appenders: ['file'], }, ], diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 2deafaaf35a94d..b9898960135fcd 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -56,7 +56,13 @@ export WORKSPACE="${WORKSPACE:-$PARENT_DIR}" nodeVersion="$(cat "$dir/.node-version")" nodeDir="$cacheDir/node/$nodeVersion" nodeBin="$nodeDir/bin" -classifier="x64.tar.gz" +hostArch="$(command uname -m)" +case "${hostArch}" in + x86_64 | amd64) nodeArch="x64" ;; + aarch64) nodeArch="arm64" ;; + *) nodeArch="${hostArch}" ;; +esac +classifier="$nodeArch.tar.gz" UNAME=$(uname) OS="linux" diff --git a/src/dev/code_coverage/shell_scripts/copy_jest_report.sh b/src/dev/code_coverage/shell_scripts/copy_jest_report.sh new file mode 100755 index 00000000000000..8369d5b467c029 --- /dev/null +++ b/src/dev/code_coverage/shell_scripts/copy_jest_report.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +EXTRACT_START_DIR=tmp/extracted_coverage +EXTRACT_END_DIR=target/kibana-coverage +COMBINED_EXRACT_DIR=/${EXTRACT_START_DIR}/${EXTRACT_END_DIR} + + +echo "### Copy combined jest report" +mkdir -p $EXTRACT_END_DIR/jest-combined +cp -r $COMBINED_EXRACT_DIR/jest-combined/. $EXTRACT_END_DIR/jest-combined/ diff --git a/src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh b/src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh index 01003b6dc880c7..6e6ba9e1b11180 100644 --- a/src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh +++ b/src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh @@ -7,12 +7,6 @@ COMBINED_EXRACT_DIR=/${EXTRACT_START_DIR}/${EXTRACT_END_DIR} PWD=$(pwd) du -sh $COMBINED_EXRACT_DIR -echo "### Jest: replacing path in json files" -for i in oss oss-integration xpack; do - sed -i "s|/dev/shm/workspace/kibana|${PWD}|g" $COMBINED_EXRACT_DIR/jest/${i}-coverage-final.json & -done -wait - echo "### Functional: replacing path in json files" for i in {1..9}; do sed -i "s|/dev/shm/workspace/kibana|${PWD}|g" $COMBINED_EXRACT_DIR/functional/${i}*.json & diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index caa1f1a7613670..243dbaa6197e6d 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -32,11 +32,16 @@ TEAM_ASSIGN_PATH=$5 # Build team assignments dat file node scripts/generate_team_assignments.js --verbose --src .github/CODEOWNERS --dest $TEAM_ASSIGN_PATH -for x in jest functional; do +for x in functional jest; do echo "### Ingesting coverage for ${x}" COVERAGE_SUMMARY_FILE=target/kibana-coverage/${x}-combined/coverage-summary.json + if [[ $x == "jest" ]]; then + # Need to override COVERAGE_INGESTION_KIBANA_ROOT since json file has original intake worker path + export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana + fi + node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH done diff --git a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh b/src/dev/code_coverage/shell_scripts/merge_functional.sh old mode 100644 new mode 100755 similarity index 54% rename from src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh rename to src/dev/code_coverage/shell_scripts/merge_functional.sh index 707c6de3f88a08..5f03e5f24528a7 --- a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh +++ b/src/dev/code_coverage/shell_scripts/merge_functional.sh @@ -4,6 +4,4 @@ COVERAGE_TEMP_DIR=/tmp/extracted_coverage/target/kibana-coverage/ export COVERAGE_TEMP_DIR echo "### Merge coverage reports" -for x in jest functional; do - yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.${x}.config.js -done +yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.functional.config.js diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts index bea9965748f27a..a02e28201a1b90 100644 --- a/src/plugins/apm_oss/server/index.ts +++ b/src/plugins/apm_oss/server/index.ts @@ -47,4 +47,5 @@ export { createGoAgentInstructions, createJavaAgentInstructions, createDotNetAgentInstructions, + createPhpAgentInstructions, } from './tutorial/instructions/apm_agent_instructions'; diff --git a/src/plugins/apm_oss/server/tutorial/envs/on_prem.ts b/src/plugins/apm_oss/server/tutorial/envs/on_prem.ts index 7d6e3431396fc7..7d261abb0cc018 100644 --- a/src/plugins/apm_oss/server/tutorial/envs/on_prem.ts +++ b/src/plugins/apm_oss/server/tutorial/envs/on_prem.ts @@ -27,6 +27,7 @@ import { createGoAgentInstructions, createJavaAgentInstructions, createDotNetAgentInstructions, + createPhpAgentInstructions, } from '../instructions/apm_agent_instructions'; export function onPremInstructions({ @@ -152,6 +153,10 @@ export function onPremInstructions({ id: INSTRUCTION_VARIANT.DOTNET, instructions: createDotNetAgentInstructions(), }, + { + id: INSTRUCTION_VARIANT.PHP, + instructions: createPhpAgentInstructions(), + }, ], statusCheck: { title: i18n.translate('apmOss.tutorial.apmAgents.statusCheck.title', { diff --git a/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts b/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts index ea1f961f5e2db1..8886dec12ccd6b 100644 --- a/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts +++ b/src/plugins/apm_oss/server/tutorial/instructions/apm_agent_instructions.ts @@ -701,3 +701,54 @@ export const createDotNetAgentInstructions = (apmServerUrl = '', secretToken = ' }), }, ]; + +export const createPhpAgentInstructions = (apmServerUrl = '', secretToken = '') => [ + { + title: i18n.translate('apmOss.tutorial.phpClient.download.title', { + defaultMessage: 'Download the APM agent', + }), + textPre: i18n.translate('apmOss.tutorial.phpClient.download.textPre', { + defaultMessage: + 'Download the package corresponding to your platform from [GitHub releases]({githubReleasesLink}).', + values: { + githubReleasesLink: 'https://github.com/elastic/apm-agent-php/releases', + }, + }), + }, + { + title: i18n.translate('apmOss.tutorial.phpClient.installPackage.title', { + defaultMessage: 'Install the downloaded package', + }), + textPre: i18n.translate('apmOss.tutorial.phpClient.installPackage.textPre', { + defaultMessage: 'For example on Alpine Linux using APK package:', + }), + commands: ['apk add --allow-untrusted .apk'], + textPost: i18n.translate('apmOss.tutorial.phpClient.installPackage.textPost', { + defaultMessage: + 'See the [documentation]({documentationLink}) for installation commands on other supported platforms and advanced installation.', + values: { + documentationLink: '{config.docs.base_url}guide/en/apm/agent/php/current/setup.html', + }, + }), + }, + { + title: i18n.translate('apmOss.tutorial.phpClient.configureAgent.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('apmOss.tutorial.phpClient.configureAgent.textPre', { + defaultMessage: + 'APM is automatically started when your app boots. Configure the agent either via `php.ini` file:', + }), + commands: `elastic_apm.server_url=http://localhost:8200 +elastic_apm.service_name="My service" +`.split('\n'), + textPost: i18n.translate('apmOss.tutorial.phpClient.configure.textPost', { + defaultMessage: + 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/agent/php/current/configuration.html', + }, + }), + }, +]; diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index f3c8954d01254b..161b67500b47c7 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -22,7 +22,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSwitch, } from '@elastic/eui'; @@ -151,115 +150,107 @@ export function DevToolsSettingsModal(props: Props) { ) : undefined; return ( - - - - - - - + + + + + + + + + + } + > + { + const val = parseInt(e.target.value, 10); + if (!val) return; + setFontSize(val); + }} + /> + - - + } - > - { - const val = parseInt(e.target.value, 10); - if (!val) return; - setFontSize(val); - }} - /> - + onChange={(e) => setWrapMode(e.target.checked)} + /> + - - - } - onChange={(e) => setWrapMode(e.target.checked)} + - - - + } - > - - } - onChange={(e) => setTripleQuotes(e.target.checked)} - /> - + onChange={(e) => setTripleQuotes(e.target.checked)} + /> + - - } - > - { - const { stateSetter, ...rest } = opts; - return rest; - })} - idToSelectedMap={checkboxIdToSelectedMap} - onChange={(e: any) => { - onAutocompleteChange(e as AutocompleteOptions); - }} + - + } + > + { + const { stateSetter, ...rest } = opts; + return rest; + })} + idToSelectedMap={checkboxIdToSelectedMap} + onChange={(e: any) => { + onAutocompleteChange(e as AutocompleteOptions); + }} + /> + - {pollingFields} - + {pollingFields} + - - - - + + + + - - - - - - + + + + + ); } diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index d060327563b25b..f659fa002e922b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -67,7 +67,13 @@ export function DashboardApp({ savedDashboard, history ); - const dashboardContainer = useDashboardContainer(dashboardStateManager, history, false); + const [unsavedChanges, setUnsavedChanges] = useState(false); + const dashboardContainer = useDashboardContainer({ + timeFilter: data.query.timefilter.timefilter, + dashboardStateManager, + setUnsavedChanges, + history, + }); const searchSessionIdQuery$ = useMemo( () => createQueryParamObservable(history, DashboardConstants.SEARCH_SESSION_ID), [history] @@ -200,6 +206,7 @@ export function DashboardApp({ ); dashboardStateManager.registerChangeListener(() => { + setUnsavedChanges(dashboardStateManager?.hasUnsavedPanelState()); // we aren't checking dirty state because there are changes the container needs to know about // that won't make the dashboard "dirty" - like a view mode change. triggerRefresh$.next(); @@ -281,6 +288,7 @@ export function DashboardApp({ embedSettings, indexPatterns, savedDashboard, + unsavedChanges, dashboardContainer, dashboardStateManager, }} diff --git a/src/plugins/dashboard/public/application/dashboard_state.test.ts b/src/plugins/dashboard/public/application/dashboard_state.test.ts index 04112d10ae7e3b..c5bda98c31b700 100644 --- a/src/plugins/dashboard/public/application/dashboard_state.test.ts +++ b/src/plugins/dashboard/public/application/dashboard_state.test.ts @@ -17,6 +17,7 @@ import { createKbnUrlStateStorage } from '../services/kibana_utils'; import { InputTimeRange, TimefilterContract, TimeRange } from '../services/data'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; +import { coreMock } from '../../../../core/public/mocks'; describe('DashboardState', function () { let dashboardState: DashboardStateManager; @@ -45,6 +46,7 @@ describe('DashboardState', function () { kibanaVersion: '7.0.0', kbnUrlStateStorage: createKbnUrlStateStorage(), history: createBrowserHistory(), + toasts: coreMock.createStart().notifications.toasts, hasTaggingCapabilities: mockHasTaggingCapabilities, }); } diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 8494900ea79c7e..e4b2afa8a46ea3 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -43,6 +43,8 @@ import { syncState, } from '../services/kibana_utils'; import { STATE_STORAGE_KEY } from '../url_generator'; +import { NotificationsStart } from '../services/core'; +import { getMigratedToastText } from '../dashboard_strings'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the @@ -59,10 +61,12 @@ export class DashboardStateManager { query: Query; }; private stateDefaults: DashboardAppStateDefaults; + private toasts: NotificationsStart['toasts']; private hideWriteControls: boolean; private kibanaVersion: string; public isDirty: boolean; private changeListeners: Array<(status: { dirty: boolean }) => void>; + private hasShownMigrationToast = false; public get appState(): DashboardAppState { return this.stateContainer.get(); @@ -93,6 +97,7 @@ export class DashboardStateManager { * @param */ constructor({ + toasts, history, kibanaVersion, savedDashboard, @@ -108,11 +113,13 @@ export class DashboardStateManager { hideWriteControls: boolean; allowByValueEmbeddables: boolean; savedDashboard: DashboardSavedObject; + toasts: NotificationsStart['toasts']; usageCollection?: UsageCollectionSetup; kbnUrlStateStorage: IKbnUrlStateStorage; dashboardPanelStorage?: DashboardPanelStorage; hasTaggingCapabilities: SavedObjectTagDecoratorTypeGuard; }) { + this.toasts = toasts; this.kibanaVersion = kibanaVersion; this.savedDashboard = savedDashboard; this.hideWriteControls = hideWriteControls; @@ -283,6 +290,10 @@ export class DashboardStateManager { if (dirty) { this.stateContainer.transitions.set('panels', Object.values(convertedPanelStateMap)); if (dirtyBecauseOfInitialStateMigration) { + if (this.getIsEditMode() && !this.hasShownMigrationToast) { + this.toasts.addSuccess(getMigratedToastText()); + this.hasShownMigrationToast = true; + } this.saveState({ replace: true }); } @@ -693,6 +704,11 @@ export class DashboardStateManager { this.dashboardPanelStorage.clearPanels(this.savedDashboard?.id); } + public hasUnsavedPanelState(): boolean { + const panels = this.dashboardPanelStorage?.getPanels(this.savedDashboard?.id); + return panels !== undefined && panels.length > 0; + } + private getUnsavedPanelState(): { panels?: SavedDashboardPanel[] } { if (!this.allowByValueEmbeddables || this.getIsViewMode() || !this.dashboardPanelStorage) { return {}; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_breadcrumbs.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_breadcrumbs.ts index 6eb1c0bf75b240..50465cc4ab58b2 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_breadcrumbs.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_breadcrumbs.ts @@ -45,7 +45,6 @@ export const useDashboardBreadcrumbs = ( text: getDashboardTitle( dashboardStateManager.getTitle(), dashboardStateManager.getViewMode(), - dashboardStateManager.getIsDirty(timefilter), dashboardStateManager.isNew() ), }, diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.test.tsx index d14b4056a64c67..6a6dc58db78157 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.test.tsx @@ -20,6 +20,7 @@ import { DashboardCapabilities } from '../types'; import { EmbeddableFactory } from '../../../../embeddable/public'; import { HelloWorldEmbeddable } from '../../../../embeddable/public/tests/fixtures'; import { DashboardContainer } from '../embeddable'; +import { coreMock } from 'src/core/public/mocks'; const savedDashboard = getSavedDashboardMock(); @@ -32,12 +33,13 @@ const history = createBrowserHistory(); const createDashboardState = () => new DashboardStateManager({ savedDashboard, + kibanaVersion: '7.0.0', hideWriteControls: false, allowByValueEmbeddables: false, - kibanaVersion: '7.0.0', - kbnUrlStateStorage: createKbnUrlStateStorage(), history: createBrowserHistory(), + kbnUrlStateStorage: createKbnUrlStateStorage(), hasTaggingCapabilities: mockHasTaggingCapabilities, + toasts: coreMock.createStart().notifications.toasts, }); const defaultCapabilities: DashboardCapabilities = { @@ -83,9 +85,9 @@ const setupEmbeddableFactory = () => { test('container is destroyed on unmount', async () => { const { createEmbeddable, destroySpy, embeddable } = setupEmbeddableFactory(); - const state = createDashboardState(); + const dashboardStateManager = createDashboardState(); const { result, unmount, waitForNextUpdate } = renderHook( - () => useDashboardContainer(state, history, false), + () => useDashboardContainer({ dashboardStateManager, history }), { wrapper: ({ children }) => ( {children} @@ -113,7 +115,7 @@ test('old container is destroyed on new dashboardStateManager', async () => { const { result, waitForNextUpdate, rerender } = renderHook< DashboardStateManager, DashboardContainer | null - >((dashboardState) => useDashboardContainer(dashboardState, history, false), { + >((dashboardStateManager) => useDashboardContainer({ dashboardStateManager, history }), { wrapper: ({ children }) => ( {children} ), @@ -148,7 +150,7 @@ test('destroyed if rerendered before resolved', async () => { const { result, waitForNextUpdate, rerender } = renderHook< DashboardStateManager, DashboardContainer | null - >((dashboardState) => useDashboardContainer(dashboardState, history, false), { + >((dashboardStateManager) => useDashboardContainer({ dashboardStateManager, history }), { wrapper: ({ children }) => ( {children} ), diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index d12fea07bdd418..f4fe55f8774004 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -24,12 +24,21 @@ import { getDashboardContainerInput, getSearchSessionIdFromURL } from '../dashbo import { DashboardConstants, DashboardContainer, DashboardContainerInput } from '../..'; import { DashboardAppServices } from '../types'; import { DASHBOARD_CONTAINER_TYPE } from '..'; - -export const useDashboardContainer = ( - dashboardStateManager: DashboardStateManager | null, - history: History, - isEmbeddedExternally: boolean -) => { +import { TimefilterContract } from '../../services/data'; + +export const useDashboardContainer = ({ + history, + timeFilter, + setUnsavedChanges, + dashboardStateManager, + isEmbeddedExternally, +}: { + history: History; + isEmbeddedExternally?: boolean; + timeFilter?: TimefilterContract; + setUnsavedChanges?: (dirty: boolean) => void; + dashboardStateManager: DashboardStateManager | null; +}) => { const { dashboardCapabilities, data, @@ -72,15 +81,20 @@ export const useDashboardContainer = ( .getStateTransfer() .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, true); + // when dashboard state manager initially loads, determine whether or not there are unsaved changes + setUnsavedChanges?.( + Boolean(incomingEmbeddable) || dashboardStateManager.hasUnsavedPanelState() + ); + let canceled = false; let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; (async function createContainer() { pendingContainer = await dashboardFactory.create( getDashboardContainerInput({ + isEmbeddedExternally: Boolean(isEmbeddedExternally), dashboardCapabilities, dashboardStateManager, incomingEmbeddable, - isEmbeddedExternally, query, searchSessionId: searchSessionIdFromURL ?? searchSession.start(), }) @@ -141,8 +155,10 @@ export const useDashboardContainer = ( dashboardCapabilities, dashboardStateManager, isEmbeddedExternally, + setUnsavedChanges, searchSession, scopedHistory, + timeFilter, embeddable, history, query, diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts index ed14223bb0a830..effd598cc3ee87 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts @@ -87,6 +87,7 @@ export const useDashboardStateManager = ( }); const stateManager = new DashboardStateManager({ + toasts: core.notifications.toasts, hasTaggingCapabilities, dashboardPanelStorage, hideWriteControls, @@ -160,7 +161,6 @@ export const useDashboardStateManager = ( const dashboardTitle = getDashboardTitle( stateManager.getTitle(), stateManager.getViewMode(), - stateManager.getIsDirty(timefilter), stateManager.isNew() ); @@ -213,6 +213,7 @@ export const useDashboardStateManager = ( uiSettings, usageCollection, allowByValueEmbeddables, + core.notifications.toasts, dashboardCapabilities.storeSearchSession, ]); diff --git a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx index 41b27b4fd69260..d302bb4216bc49 100644 --- a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx +++ b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx @@ -40,6 +40,60 @@ export const confirmDiscardUnsavedChanges = ( } }); +export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; + +export const confirmDiscardOrKeepUnsavedChanges = ( + overlays: OverlayStart +): Promise => { + return new Promise((resolve) => { + const session = overlays.openModal( + toMountPoint( + <> + + {leaveConfirmStrings.getLeaveEditModeTitle()} + + + + {leaveConfirmStrings.getLeaveEditModeSubtitle()} + + + + session.close()} + > + {leaveConfirmStrings.getCancelButtonText()} + + { + session.close(); + resolve('keep'); + }} + > + {leaveConfirmStrings.getKeepChangesText()} + + { + session.close(); + resolve('discard'); + }} + > + {leaveConfirmStrings.getConfirmButtonText()} + + + + ), + { + 'data-test-subj': 'dashboardDiscardConfirmModal', + } + ); + }); +}; + export const confirmCreateWithUnsaved = ( overlays: OverlayStart, startBlankCallback: () => void, diff --git a/src/plugins/dashboard/public/application/top_nav/__snapshots__/clone_modal.test.js.snap b/src/plugins/dashboard/public/application/top_nav/__snapshots__/clone_modal.test.js.snap index d289d267a2fd6d..1e029e6960cdfa 100644 --- a/src/plugins/dashboard/public/application/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/plugins/dashboard/public/application/top_nav/__snapshots__/clone_modal.test.js.snap @@ -1,65 +1,63 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders DashboardCloneModal 1`] = ` - - - - - - - - - -

- -

-
- - + + + -
- - - - - + + + + +

- - - - +

+
+ + +
+ + + + + + + + +
`; diff --git a/src/plugins/dashboard/public/application/top_nav/clone_modal.tsx b/src/plugins/dashboard/public/application/top_nav/clone_modal.tsx index c1bcad51babf9f..3af186f841a5d0 100644 --- a/src/plugins/dashboard/public/application/top_nav/clone_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/clone_modal.tsx @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiText, EuiCallOut, @@ -138,69 +137,67 @@ export class DashboardCloneModal extends React.Component { render() { return ( - - - - - - - - - - -

- -

-
- - - - + + + + + - {this.renderDuplicateTitleCallout()} -
- - - + + +

- - - - - - - - +

+
+ + + + + + {this.renderDuplicateTitleCallout()} +
+ + + + + + + + + + +
); } } diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 786afc81c400cd..11fb7f0cb56ff4 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -43,9 +43,9 @@ import { showOptionsPopover } from './show_options_popover'; import { TopNavIds } from './top_nav_ids'; import { ShowShareModal } from './show_share_modal'; import { PanelToolbar } from './panel_toolbar'; -import { confirmDiscardUnsavedChanges } from '../listing/confirm_overlays'; +import { confirmDiscardOrKeepUnsavedChanges } from '../listing/confirm_overlays'; import { OverlayRef } from '../../../../../core/public'; -import { getNewDashboardTitle } from '../../dashboard_strings'; +import { getNewDashboardTitle, unsavedChangesBadge } from '../../dashboard_strings'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardContainer } from '..'; @@ -64,6 +64,7 @@ export interface DashboardTopNavProps { timefilter: TimefilterContract; indexPatterns: IndexPattern[]; redirectTo: DashboardRedirect; + unsavedChanges?: boolean; lastDashboardId?: string; viewMode: ViewMode; } @@ -72,6 +73,7 @@ export function DashboardTopNav({ dashboardStateManager, dashboardContainer, lastDashboardId, + unsavedChanges, savedDashboard, onQuerySubmit, embedSettings, @@ -152,34 +154,53 @@ export function DashboardTopNav({ } }, [state.addPanelOverlay]); - const onDiscardChanges = useCallback(() => { - function revertChangesAndExitEditMode() { - dashboardStateManager.resetState(); - dashboardStateManager.clearUnsavedPanels(); - - // We need to do a hard reset of the timepicker. appState will not reload like - // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on - // reload will cause it not to sync. - if (dashboardStateManager.getIsTimeSavedWithDashboard()) { - dashboardStateManager.syncTimefilterWithDashboardTime(timefilter); - dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter); - } - dashboardStateManager.switchViewMode(ViewMode.VIEW); - } - confirmDiscardUnsavedChanges(core.overlays, revertChangesAndExitEditMode); - }, [core.overlays, dashboardStateManager, timefilter]); - const onChangeViewMode = useCallback( (newMode: ViewMode) => { clearAddPanel(); - if (savedDashboard?.id && allowByValueEmbeddables) { - const { getFullEditPath, title, id } = savedDashboard; - chrome.recentlyAccessed.add(getFullEditPath(newMode === ViewMode.EDIT), title, id); + const isPageRefresh = newMode === dashboardStateManager.getViewMode(); + const isLeavingEditMode = !isPageRefresh && newMode === ViewMode.VIEW; + const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter); + + function switchViewMode() { + dashboardStateManager.switchViewMode(newMode); + dashboardStateManager.restorePanels(); + + if (savedDashboard?.id && allowByValueEmbeddables) { + const { getFullEditPath, title, id } = savedDashboard; + chrome.recentlyAccessed.add(getFullEditPath(newMode === ViewMode.EDIT), title, id); + } + } + + if (!willLoseChanges) { + switchViewMode(); + return; + } + + function discardChanges() { + dashboardStateManager.resetState(); + dashboardStateManager.clearUnsavedPanels(); + + // We need to do a hard reset of the timepicker. appState will not reload like + // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on + // reload will cause it not to sync. + if (dashboardStateManager.getIsTimeSavedWithDashboard()) { + dashboardStateManager.syncTimefilterWithDashboardTime(timefilter); + dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter); + } + dashboardStateManager.switchViewMode(ViewMode.VIEW); } - dashboardStateManager.switchViewMode(newMode); - dashboardStateManager.restorePanels(); + confirmDiscardOrKeepUnsavedChanges(core.overlays).then((selection) => { + if (selection === 'discard') { + discardChanges(); + } + if (selection !== 'cancel') { + switchViewMode(); + } + }); }, [ + timefilter, + core.overlays, clearAddPanel, savedDashboard, dashboardStateManager, @@ -381,7 +402,6 @@ export function DashboardTopNav({ }, [TopNavIds.EXIT_EDIT_MODE]: () => onChangeViewMode(ViewMode.VIEW), [TopNavIds.ENTER_EDIT_MODE]: () => onChangeViewMode(ViewMode.EDIT), - [TopNavIds.DISCARD_CHANGES]: onDiscardChanges, [TopNavIds.SAVE]: runSave, [TopNavIds.QUICK_SAVE]: runQuickSave, [TopNavIds.CLONE]: runClone, @@ -417,7 +437,6 @@ export function DashboardTopNav({ }, [ dashboardCapabilities, dashboardStateManager, - onDiscardChanges, onChangeViewMode, savedDashboard, runClone, @@ -450,7 +469,18 @@ export function DashboardTopNav({ isDirty: dashboardStateManager.isDirty, }); + const badges = unsavedChanges + ? [ + { + 'data-test-subj': 'dashboardUnsavedChangesBadge', + badgeText: unsavedChangesBadge.getUnsavedChangedBadgeText(), + color: 'secondary', + }, + ] + : undefined; + return { + badges, appName: 'dashboard', config: showTopNavMenu ? topNav : undefined, className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined, diff --git a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts index abc128369017c5..26eea1b5f718de 100644 --- a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts +++ b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts @@ -41,14 +41,12 @@ export function getTopNavConfig( getOptionsConfig(actions[TopNavIds.OPTIONS]), getShareConfig(actions[TopNavIds.SHARE]), getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getDiscardConfig(actions[TopNavIds.DISCARD_CHANGES]), getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard), ] : [ getOptionsConfig(actions[TopNavIds.OPTIONS]), getShareConfig(actions[TopNavIds.SHARE]), getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getDiscardConfig(actions[TopNavIds.DISCARD_CHANGES]), getSaveConfig(actions[TopNavIds.SAVE]), getQuickSave(actions[TopNavIds.QUICK_SAVE]), ]; @@ -154,23 +152,6 @@ function getViewConfig(action: NavAction) { }; } -/** - * @returns {kbnTopNavConfig} - */ -function getDiscardConfig(action: NavAction) { - return { - id: 'discard', - label: i18n.translate('dashboard.topNave.discardlButtonAriaLabel', { - defaultMessage: 'discard', - }), - description: i18n.translate('dashboard.topNave.discardConfigDescription', { - defaultMessage: 'Discard unsaved changes', - }), - testId: 'dashboardDiscardChanges', - run: action, - }; -} - /** * @returns {kbnTopNavConfig} */ diff --git a/src/plugins/dashboard/public/application/top_nav/top_nav_ids.ts b/src/plugins/dashboard/public/application/top_nav/top_nav_ids.ts index 92a0db6bd0ba2e..ee3d08e2330ae9 100644 --- a/src/plugins/dashboard/public/application/top_nav/top_nav_ids.ts +++ b/src/plugins/dashboard/public/application/top_nav/top_nav_ids.ts @@ -13,7 +13,6 @@ export const TopNavIds = { SAVE: 'save', EXIT_EDIT_MODE: 'exitEditMode', ENTER_EDIT_MODE: 'enterEditMode', - DISCARD_CHANGES: 'discard', CLONE: 'clone', FULL_SCREEN: 'fullScreenMode', }; diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index 96bd32088ec38b..dad347b176c7ef 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -12,36 +12,30 @@ import { ViewMode } from './services/embeddable'; /** * @param title {string} the current title of the dashboard * @param viewMode {DashboardViewMode} the current mode. If in editing state, prepends 'Editing ' to the title. - * @param isDirty {boolean} if the dashboard is in a dirty state. If in dirty state, adds (unsaved) to the - * end of the title. * @returns {string} A title to display to the user based on the above parameters. */ -export function getDashboardTitle( - title: string, - viewMode: ViewMode, - isDirty: boolean, - isNew: boolean -): string { +export function getDashboardTitle(title: string, viewMode: ViewMode, isNew: boolean): string { const isEditMode = viewMode === ViewMode.EDIT; - let displayTitle: string; const dashboardTitle = isNew ? getNewDashboardTitle() : title; + return isEditMode + ? i18n.translate('dashboard.strings.dashboardEditTitle', { + defaultMessage: 'Editing {title}', + values: { title: dashboardTitle }, + }) + : dashboardTitle; +} - if (isEditMode && isDirty) { - displayTitle = i18n.translate('dashboard.strings.dashboardUnsavedEditTitle', { - defaultMessage: 'Editing {title} (unsaved)', - values: { title: dashboardTitle }, - }); - } else if (isEditMode) { - displayTitle = i18n.translate('dashboard.strings.dashboardEditTitle', { - defaultMessage: 'Editing {title}', - values: { title: dashboardTitle }, - }); - } else { - displayTitle = dashboardTitle; - } +export const unsavedChangesBadge = { + getUnsavedChangedBadgeText: () => + i18n.translate('dashboard.unsavedChangesBadge', { + defaultMessage: 'Unsaved changes', + }), +}; - return displayTitle; -} +export const getMigratedToastText = () => + i18n.translate('dashboard.migratedChanges', { + defaultMessage: 'Some panels have been successfully updated to the latest version.', + }); /* Plugin @@ -253,6 +247,18 @@ export const leaveConfirmStrings = { i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesSubtitle', { defaultMessage: 'Leave Dashboard with unsaved work?', }), + getKeepChangesText: () => + i18n.translate('dashboard.appLeaveConfirmModal.keepUnsavedChangesButtonLabel', { + defaultMessage: 'Keep unsaved changes', + }), + getLeaveEditModeTitle: () => + i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditMode', { + defaultMessage: 'Leave edit mode with unsaved work?', + }), + getLeaveEditModeSubtitle: () => + i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesOptionalDescription', { + defaultMessage: `If you discard your changes, there's no getting them back.`, + }), getDiscardTitle: () => i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesTitle', { defaultMessage: 'Discard changes to dashboard?', diff --git a/src/plugins/data/common/search/aggs/metrics/max.ts b/src/plugins/data/common/search/aggs/metrics/max.ts index ee2d5ad03ce3a0..5a41cdbb256c80 100644 --- a/src/plugins/data/common/search/aggs/metrics/max.ts +++ b/src/plugins/data/common/search/aggs/metrics/max.ts @@ -36,7 +36,7 @@ export const getMaxMetricAgg = () => { { name: 'field', type: 'field', - filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM], }, ], }); diff --git a/src/plugins/data/common/search/aggs/metrics/min.ts b/src/plugins/data/common/search/aggs/metrics/min.ts index f9e3c5b59d586b..1805546a9fa346 100644 --- a/src/plugins/data/common/search/aggs/metrics/min.ts +++ b/src/plugins/data/common/search/aggs/metrics/min.ts @@ -36,7 +36,7 @@ export const getMinMetricAgg = () => { { name: 'field', type: 'field', - filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM], }, ], }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 8580fb7910735e..2cf0455ae2df8b 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -297,6 +297,9 @@ export class SearchSource { switchMap(() => { const searchRequest = this.flatten(); this.history = [searchRequest]; + if (searchRequest.index) { + options.indexPattern = searchRequest.index; + } return getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES) ? from(this.legacyFetch(searchRequest, options)) diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 4f687a396a47b9..3ac4c33091f6bb 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -8,6 +8,7 @@ import { Observable } from 'rxjs'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; +import { IndexPattern } from '..'; export type ISearchGeneric = < SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, @@ -111,4 +112,10 @@ export interface ISearchOptions { * rather than starting from scratch) */ isRestore?: boolean; + + /** + * Index pattern reference is used for better error messages + */ + + indexPattern?: IndexPattern; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0920d0d716d73a..0a3e4666da7473 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1640,6 +1640,7 @@ export type ISearchGeneric = { }); const component = mount(e.getErrorMessage(startMock.application)); - const scriptElem = findTestSubject(component, 'painlessScript').getDOMNode(); - const failedShards = e.attributes?.failed_shards![0]; - const script = failedShards!.reason.script; - expect(scriptElem.textContent).toBe(`Error executing Painless script: '${script}'`); const stackTraceElem = findTestSubject(component, 'painlessStackTrace').getDOMNode(); - const stackTrace = failedShards!.reason.script_stack!.join('\n'); + const stackTrace = failedShards!.reason.script_stack!.splice(-2).join('\n'); expect(stackTraceElem.textContent).toBe(stackTrace); + const humanReadableError = findTestSubject( + component, + 'painlessHumanReadableError' + ).getDOMNode(); + expect(humanReadableError.textContent).toBe(failedShards?.reason.caused_by?.reason); + expect(component.find('EuiButton').length).toBe(1); }); }); diff --git a/src/plugins/data/public/search/errors/painless_error.tsx b/src/plugins/data/public/search/errors/painless_error.tsx index a73d112a8de48b..bad4567024d00f 100644 --- a/src/plugins/data/public/search/errors/painless_error.tsx +++ b/src/plugins/data/public/search/errors/painless_error.tsx @@ -14,40 +14,59 @@ import { ApplicationStart } from 'kibana/public'; import { IEsError, isEsError } from './types'; import { EsError } from './es_error'; import { getRootCause } from './utils'; +import { IndexPattern } from '../..'; export class PainlessError extends EsError { painlessStack?: string; - constructor(err: IEsError) { + indexPattern?: IndexPattern; + constructor(err: IEsError, indexPattern?: IndexPattern) { super(err); + this.indexPattern = indexPattern; } public getErrorMessage(application: ApplicationStart) { - function onClick() { + function onClick(indexPatternId?: string) { application.navigateToApp('management', { - path: `/kibana/indexPatterns`, + path: `/kibana/indexPatterns${indexPatternId ? `/patterns/${indexPatternId}` : ''}`, }); } const rootCause = getRootCause(this.err); + const scriptFromStackTrace = rootCause?.script_stack + ? rootCause?.script_stack?.slice(-2).join('\n') + : undefined; + // if the error has been properly processed it will highlight where it occurred. + const hasScript = rootCause?.script_stack?.slice(-1)[0]?.indexOf('HERE') || -1 >= 0; + const humanReadableError = rootCause?.caused_by?.reason; + // fallback, show ES stacktrace const painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined; + const indexPatternId = this?.indexPattern?.id; return ( <> - + {i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', { - defaultMessage: "Error executing Painless script: '{script}'", - values: { script: rootCause?.script }, + defaultMessage: + 'Error executing runtime field or scripted field on index pattern {indexPatternName}', + values: { + indexPatternName: this?.indexPattern?.title, + }, })} - {painlessStack ? ( + {scriptFromStackTrace || painlessStack ? ( - {painlessStack} + {hasScript ? scriptFromStackTrace : painlessStack} ) : null} + {humanReadableError ? ( + {humanReadableError} + ) : null} + + - + onClick(indexPatternId)} size="s"> diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index f46a3d258f9486..ec4b628a6bd3af 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -109,7 +109,7 @@ export class SearchInterceptor { return e; } else if (isEsError(e)) { if (isPainlessError(e)) { - return new PainlessError(e); + return new PainlessError(e, options?.indexPattern); } else { return new EsError(e); } diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx index 7873886432cbec..077b9ac47286d9 100644 --- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx +++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx @@ -9,7 +9,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { EuiButtonEmpty, - EuiOverlayMask, EuiModal, EuiButton, EuiModalHeader, @@ -208,37 +207,35 @@ export function SaveQueryForm({ ); return ( - - - - - {i18n.translate('data.search.searchBar.savedQueryFormTitle', { - defaultMessage: 'Save query', - })} - - - - {saveQueryForm} - - - - {i18n.translate('data.search.searchBar.savedQueryFormCancelButtonText', { - defaultMessage: 'Cancel', - })} - - - - {i18n.translate('data.search.searchBar.savedQueryFormSaveButtonText', { - defaultMessage: 'Save', - })} - - - - + + + + {i18n.translate('data.search.searchBar.savedQueryFormTitle', { + defaultMessage: 'Save query', + })} + + + + {saveQueryForm} + + + + {i18n.translate('data.search.searchBar.savedQueryFormCancelButtonText', { + defaultMessage: 'Cancel', + })} + + + + {i18n.translate('data.search.searchBar.savedQueryFormSaveButtonText', { + defaultMessage: 'Save', + })} + + + ); } diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx index 47a2d050a9bfa3..b7ba3215eb5aa3 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EuiListGroupItem, EuiConfirmModal, EuiOverlayMask, EuiIconTip } from '@elastic/eui'; +import { EuiListGroupItem, EuiConfirmModal, EuiIconTip } from '@elastic/eui'; import React, { Fragment, useState } from 'react'; import classNames from 'classnames'; @@ -114,36 +114,34 @@ export const SavedQueryListItem = ({ /> {showDeletionConfirmationModal && ( - - { - onDelete(savedQuery); - setShowDeletionConfirmationModal(false); - }} - buttonColor="danger" - onCancel={() => { - setShowDeletionConfirmationModal(false); - }} - /> - + { + onDelete(savedQuery); + setShowDeletionConfirmationModal(false); + }} + buttonColor="danger" + onCancel={() => { + setShowDeletionConfirmationModal(false); + }} + /> )} ); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index ab8f6c9ed39511..23aaab36e79055 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -912,6 +912,7 @@ export class IndexPatternsService implements Plugin_3 { jest.useFakeTimers(); }); + + test('nested expressions are aborted when parent aborted', async () => { + jest.useRealTimers(); + const started = jest.fn(); + const completed = jest.fn(); + const aborted = jest.fn(); + + const defer: ExpressionFunctionDefinition<'defer', any, { time: number }, any> = { + name: 'defer', + args: { + time: { + aliases: ['_'], + help: 'Calls function from a context after delay unless aborted', + types: ['number'], + }, + }, + help: '', + fn: async (input, args, { abortSignal }) => { + started(); + await new Promise((r) => { + const timeout = setTimeout(() => { + if (!abortSignal.aborted) { + completed(); + } + r(undefined); + }, args.time); + + abortSignal.addEventListener('abort', () => { + aborted(); + clearTimeout(timeout); + r(undefined); + }); + }); + + return args.time; + }, + }; + + const expression = 'defer time={defer time={defer time=300}}'; + const executor = createUnitTestExecutor(); + executor.registerFunction(defer); + const execution = new Execution({ + executor, + ast: parseExpression(expression), + params: {}, + }); + + execution.start(); + + await waitFor(() => expect(started).toHaveBeenCalledTimes(1)); + + execution.cancel(); + const result = await execution.result; + expect(result).toMatchObject({ + type: 'error', + error: { + message: 'The expression was aborted.', + name: 'AbortError', + }, + }); + + await waitFor(() => expect(aborted).toHaveBeenCalledTimes(1)); + + expect(started).toHaveBeenCalledTimes(1); + expect(aborted).toHaveBeenCalledTimes(1); + expect(completed).toHaveBeenCalledTimes(0); + + jest.useFakeTimers(); + }); }); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index e555258a0cf998..bf545a0075bed7 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -120,6 +120,13 @@ export class Execution< */ private readonly firstResultFuture = new Defer(); + /** + * Keeping track of any child executions + * Needed to cancel child executions in case parent execution is canceled + * @private + */ + private readonly childExecutions: Execution[] = []; + /** * Contract is a public representation of `Execution` instances. Contract we * can return to other plugins for their consumption. @@ -203,8 +210,10 @@ export class Execution< const chainPromise = this.invokeChain(this.state.get().ast.chain, input); this.race(chainPromise).then(resolve, (error) => { - if (this.abortController.signal.aborted) resolve(createAbortErrorValue()); - else reject(error); + if (this.abortController.signal.aborted) { + this.childExecutions.forEach((ex) => ex.cancel()); + resolve(createAbortErrorValue()); + } else reject(error); }); this.firstResultFuture.promise @@ -460,6 +469,7 @@ export class Execution< ast as ExpressionAstExpression, this.execution.params ); + this.childExecutions.push(execution); execution.start(input); return await execution.result; case 'string': diff --git a/src/plugins/home/common/instruction_variant.ts b/src/plugins/home/common/instruction_variant.ts index ae6735568851c8..310ee23460a084 100644 --- a/src/plugins/home/common/instruction_variant.ts +++ b/src/plugins/home/common/instruction_variant.ts @@ -23,6 +23,7 @@ export const INSTRUCTION_VARIANT = { JAVA: 'java', DOTNET: 'dotnet', LINUX: 'linux', + PHP: 'php', }; const DISPLAY_MAP = { @@ -42,6 +43,7 @@ const DISPLAY_MAP = { [INSTRUCTION_VARIANT.JAVA]: 'Java', [INSTRUCTION_VARIANT.DOTNET]: '.NET', [INSTRUCTION_VARIANT.LINUX]: 'Linux', + [INSTRUCTION_VARIANT.PHP]: 'PHP', }; /** diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap index 2b320782cb1634..eaaccdb499b0b4 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -1,14 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DeleteScritpedFieldConfirmationModal should render normally 1`] = ` - - - + `; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx index 5fbd3118b800bb..36069f408f3543 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal } from '@elastic/eui'; import { ScriptedFieldItem } from '../../types'; @@ -42,15 +42,13 @@ export const DeleteScritpedFieldConfirmationModal = ({ ); return ( - - - + ); }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap index 9d92a3689b6983..736dbb611dbbdf 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -1,37 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header should render normally 1`] = ` - - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - onCancel={[Function]} - onConfirm={[Function]} - title={ - + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + - } - /> - + } + /> + } +/> `; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx index 6715d7a6780ae4..fb8d4a38bfe63e 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx @@ -10,7 +10,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; interface DeleteFilterConfirmationModalProps { filterToDeleteValue: string; @@ -26,35 +26,33 @@ export const DeleteFilterConfirmationModal = ({ onDeleteFilter, }: DeleteFilterConfirmationModalProps) => { return ( - - - } - onCancel={onCancelConfirmationModal} - onConfirm={onDeleteFilter} - cancelButtonText={ - - } - buttonColor="danger" - confirmButtonText={ - - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - /> - + + } + onCancel={onCancelConfirmationModal} + onConfirm={onDeleteFilter} + cancelButtonText={ + + } + buttonColor="danger" + confirmButtonText={ + + } + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + /> ); }; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index f22981de857497..829536063a26c9 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -25,7 +25,6 @@ import { EuiFormRow, EuiIcon, EuiLink, - EuiOverlayMask, EuiSelect, EuiSpacer, EuiText, @@ -643,42 +642,40 @@ export class FieldEditor extends PureComponent - { - this.hideDeleteModal(); - this.deleteField(); - }} - cancelButtonText={i18n.translate('indexPatternManagement.deleteField.cancelButton', { - defaultMessage: 'Cancel', - })} - confirmButtonText={i18n.translate('indexPatternManagement.deleteField.deleteButton', { - defaultMessage: 'Delete', - })} - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

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

-
-
+ { + this.hideDeleteModal(); + this.deleteField(); + }} + cancelButtonText={i18n.translate('indexPatternManagement.deleteField.cancelButton', { + defaultMessage: 'Cancel', + })} + confirmButtonText={i18n.translate('indexPatternManagement.deleteField.deleteButton', { + defaultMessage: 'Delete', + })} + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + > +

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

+
) : null; }; diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 7b3322f5f6c2d0..fa0a32fc3d542a 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -21,7 +21,6 @@ import { EuiFlexItem, EuiButton, EuiSpacer, - EuiOverlayMask, EuiConfirmModal, EuiCallOut, EuiBasicTableColumn, @@ -238,42 +237,40 @@ class TableListView extends React.Component - - } - buttonColor="danger" - onCancel={this.closeDeleteModal} - onConfirm={this.deleteSelectedItems} - cancelButtonText={ - - } - confirmButtonText={deleteButton} - defaultFocusedButton="cancel" - > -

- -

-
-
+ + } + buttonColor="danger" + onCancel={this.closeDeleteModal} + onConfirm={this.deleteSelectedItems} + cancelButtonText={ + + } + confirmButtonText={deleteButton} + defaultFocusedButton="cancel" + > +

+ +

+
); } diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss index 230be399febda3..bc27cf061eb68a 100644 --- a/src/plugins/navigation/public/top_nav_menu/_index.scss +++ b/src/plugins/navigation/public/top_nav_menu/_index.scss @@ -1,3 +1,12 @@ .kbnTopNavMenu { margin-right: $euiSizeXS; } + +.kbnTopNavMenu__badgeWrapper { + display: flex; + align-items: baseline; +} + +.kbnTopNavMenu__badgeGroup { + margin-right: $euiSizeM; +} diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 70bc3b10b30adc..22edf9c454466c 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -7,7 +7,7 @@ */ import React, { ReactElement } from 'react'; -import { EuiHeaderLinks } from '@elastic/eui'; +import { EuiBadge, EuiBadgeGroup, EuiBadgeProps, EuiHeaderLinks } from '@elastic/eui'; import classNames from 'classnames'; import { MountPoint } from '../../../../core/public'; @@ -23,6 +23,7 @@ import { TopNavMenuItem } from './top_nav_menu_item'; export type TopNavMenuProps = StatefulSearchBarProps & Omit & { config?: TopNavMenuData[]; + badges?: Array; showSearchBar?: boolean; showQueryBar?: boolean; showQueryInput?: boolean; @@ -61,12 +62,28 @@ export type TopNavMenuProps = StatefulSearchBarProps & **/ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { - const { config, showSearchBar, ...searchBarProps } = props; + const { config, badges, showSearchBar, ...searchBarProps } = props; if ((!config || config.length === 0) && (!showSearchBar || !props.data)) { return null; } + function renderBadges(): ReactElement | null { + if (!badges || badges.length === 0) return null; + return ( + + {badges.map((badge: EuiBadgeProps & { badgeText: string }, i: number) => { + const { badgeText, ...badgeProps } = badge; + return ( + + {badgeText} + + ); + })} + + ); + } + function renderItems(): ReactElement[] | null { if (!config || config.length === 0) return null; return config.map((menuItem: TopNavMenuData, i: number) => { @@ -98,7 +115,10 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { return ( <> - {renderMenu(menuClassName)} + + {renderBadges()} + {renderMenu(menuClassName)} + {renderSearchBar()} diff --git a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap index f88039fbda9bad..1f05ed6b944051 100644 --- a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -1,407 +1,399 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` - +
- - - - - - - - - + + + - + + + + + - - + - } - labelType="label" - > - + + - - - - - - - - - Save - - - + + + + + + + + + Save + +
-
+ `; exports[`SavedObjectSaveModal should render matching snapshot when custom isValid is set 1`] = ` - +
- - - - + + - - - - - - } - labelType="label" - > - + + + + + - - + - } - labelType="label" - > - + + - - - - - - - - - Save - - - + + + + + + + + + Save + +
-
+ `; exports[`SavedObjectSaveModal should render matching snapshot when custom isValid is set 2`] = ` - +
- - - - + + - - - - - - } - labelType="label" - > - + + + + + - - + - } - labelType="label" - > - + + - - - - - - - - - Save - - - + + + + + + + + + Save + +
-
+ `; exports[`SavedObjectSaveModal should render matching snapshot when given options 1`] = ` - +
- - - - + + - - - - - - - - } - labelType="label" - > - + + + + + + + - - + - } - labelType="label" - > - + + - -
- Hello! Main options -
-
- -
- Hey there! Options on the right -
-
-
-
-
- - - - - - Save - - -
+ } + labelType="label" + > + + +
+ Hello! Main options +
+ + +
+ Hey there! Options on the right +
+
+ + + + + + + + + Save + + -
+ `; diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index 39c87c9da60c2b..e476d62a0e793b 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -21,7 +21,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiSwitch, EuiSwitchEvent, @@ -123,52 +122,48 @@ export class SavedObjectSaveModal extends React.Component ); return ( - +
- - - - - - - - - {this.renderDuplicateTitleCallout(duplicateWarningId)} - - - {!this.props.showDescription && this.props.description && ( - - {this.props.description} - - )} - {formBody} - {this.renderCopyOnSave()} - - - - - - - - - {this.renderConfirmButton()} - - + + + + + + + + {this.renderDuplicateTitleCallout(duplicateWarningId)} + + + {!this.props.showDescription && this.props.description && ( + + {this.props.description} + + )} + {formBody} + {this.renderCopyOnSave()} + + + + + + + + + {this.renderConfirmButton()} +
-
+ ); } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx index f67e7bd0b568cd..f6f00c95d9bf19 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx @@ -53,90 +53,88 @@ export const DeleteConfirmModal: FC = ({ // can't use `EuiConfirmModal` here as the confirm modal body is wrapped // inside a `

` element, causing UI glitches with the table. return ( - - - - - - - - -

- -

- - ( - - - - ), - }, - { - field: 'id', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', - { defaultMessage: 'Id' } - ), - }, - { - field: 'meta.title', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', - { defaultMessage: 'Title' } - ), - }, - ]} - pagination={true} - sorting={false} + + + + - - - - - - - - - - - - - - - - - - - - - + + + +

+ +

+ + ( + + + + ), + }, + { + field: 'id', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', + { defaultMessage: 'Id' } + ), + }, + { + field: 'meta.title', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', + { defaultMessage: 'Title' } + ), + }, + ]} + pagination={true} + sorting={false} + /> +
+ + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx index 693fe00ffedccb..0699f77f575219 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx @@ -8,7 +8,6 @@ import React, { FC } from 'react'; import { - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -47,80 +46,78 @@ export const ExportModal: FC = ({ onIncludeReferenceChange, }) => { return ( - - - - + + + + + + + + - - - - - } - labelType="legend" - > - { - onSelectedOptionsChange({ - ...selectedOptions, - ...{ - [optionId]: !selectedOptions[optionId], - }, - }); - }} + id="savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription" + defaultMessage="Select which types to export" /> - - - - } - checked={includeReferences} - onChange={() => onIncludeReferenceChange(!includeReferences)} + } + labelType="legend" + > + { + onSelectedOptionsChange({ + ...selectedOptions, + ...{ + [optionId]: !selectedOptions[optionId], + }, + }); + }} /> - - - - - - - - - - - - - - - - - - - - - + + + + } + checked={includeReferences} + onChange={() => onIncludeReferenceChange(!includeReferences)} + /> + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx index 66753e81ccd3ff..cfe0b2be1d3c05 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx @@ -7,13 +7,7 @@ */ import React, { useState, Fragment, ReactNode } from 'react'; -import { - EuiOverlayMask, - EuiConfirmModal, - EUI_MODAL_CONFIRM_BUTTON, - EuiText, - EuiSuperSelect, -} from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON, EuiText, EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { FailedImportConflict } from '../../../lib/resolve_import_errors'; @@ -98,29 +92,27 @@ export const OverwriteModal = ({ conflict, onFinish }: OverwriteModalProps) => { } ); return ( - - onFinish(false)} - onConfirm={() => onFinish(true, destinationId)} - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - maxWidth="500px" - > -

{bodyText}

- {selectControl} -
-
+ onFinish(false)} + onConfirm={() => onFinish(true, destinationId)} + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + maxWidth="500px" + > +

{bodyText}

+ {selectControl} +
); }; diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index ff352003609a99..d36b734f75be2e 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -8,7 +8,7 @@ import React from 'react'; -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; @@ -121,7 +121,7 @@ class NewVisModal extends React.Component ); - return {selectionModal}; + return selectionModal; } private onCloseModal = () => { diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts index 0171a462b13680..08d577b3df08cd 100644 --- a/test/accessibility/apps/dashboard.ts +++ b/test/accessibility/apps/dashboard.ts @@ -110,12 +110,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Exit out of edit mode', async () => { - await PageObjects.dashboard.clickDiscardChanges(); + await PageObjects.dashboard.clickDiscardChanges(false); await a11y.testAppSnapshot(); }); it('Discard changes', async () => { - await PageObjects.common.clickConfirmOnModal(); + await testSubjects.exists('dashboardDiscardConfirmDiscard'); + await testSubjects.click('dashboardDiscardConfirmDiscard'); await PageObjects.dashboard.getIsInViewMode(); await a11y.testAppSnapshot(); }); diff --git a/test/common/fixtures/plugins/coverage/kibana.json b/test/common/fixtures/plugins/coverage/kibana.json index d80432534d7467..d849db8d0583d9 100644 --- a/test/common/fixtures/plugins/coverage/kibana.json +++ b/test/common/fixtures/plugins/coverage/kibana.json @@ -1,5 +1,5 @@ { - "id": "coverage-fixtures", + "id": "coverageFixtures", "version": "kibana", "server": false, "ui": true diff --git a/test/functional/apps/dashboard/copy_panel_to.ts b/test/functional/apps/dashboard/copy_panel_to.ts index bb02bfee49f006..9abdc2ceffc013 100644 --- a/test/functional/apps/dashboard/copy_panel_to.ts +++ b/test/functional/apps/dashboard/copy_panel_to.ts @@ -115,7 +115,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('confirmCopyToButton'); await PageObjects.dashboard.waitForRenderComplete(); - await PageObjects.dashboard.expectOnDashboard(`Editing New Dashboard (unsaved)`); + await PageObjects.dashboard.expectOnDashboard(`Editing New Dashboard`); }); it('it always appends new panels instead of overwriting', async () => { diff --git a/test/functional/apps/dashboard/dashboard_unsaved_state.ts b/test/functional/apps/dashboard/dashboard_unsaved_state.ts index 851d7ab7461ed0..e6cc91880010ae 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_state.ts +++ b/test/functional/apps/dashboard/dashboard_unsaved_state.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); const dashboardAddPanel = getService('dashboardAddPanel'); @@ -29,10 +30,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('few panels'); - await PageObjects.dashboard.switchToEditMode(); - + await PageObjects.header.waitUntilLoadingHasFinished(); originalPanelCount = await PageObjects.dashboard.getPanelCount(); + }); + it('does not show unsaved changes badge when there are no unsaved changes', async () => { + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + }); + + it('shows the unsaved changes badge after adding panels', async () => { + await PageObjects.dashboard.switchToEditMode(); // add an area chart by value await dashboardAddPanel.clickCreateNewLink(); await PageObjects.visualize.clickAggBasedVisualizations(); @@ -42,6 +49,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // add a metric by reference await dashboardAddPanel.addVisualization('Rendering-Test: metric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); }); it('has correct number of panels', async () => { @@ -73,15 +83,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('resets to original panel count upon entering view mode', async () => { await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.clickCancelOutOfEditMode(); + await PageObjects.header.waitUntilLoadingHasFinished(); const currentPanelCount = await PageObjects.dashboard.getPanelCount(); expect(currentPanelCount).to.eql(originalPanelCount); }); + it('shows unsaved changes badge in view mode if changes have not been discarded', async () => { + await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); + }); + it('retains unsaved panel count after returning to edit mode', async () => { await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.switchToEditMode(); + await PageObjects.header.waitUntilLoadingHasFinished(); const currentPanelCount = await PageObjects.dashboard.getPanelCount(); expect(currentPanelCount).to.eql(unsavedPanelCount); }); + + it('does not show unsaved changes badge after saving', async () => { + await PageObjects.dashboard.saveDashboard('Unsaved State Test'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + }); }); } diff --git a/test/functional/apps/dashboard/view_edit.ts b/test/functional/apps/dashboard/view_edit.ts index 5242e59efa0e99..6c7d60c9a15aa1 100644 --- a/test/functional/apps/dashboard/view_edit.ts +++ b/test/functional/apps/dashboard/view_edit.ts @@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const dashboardAddPanel = getService('dashboardAddPanel'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['dashboard', 'header', 'common', 'visualize', 'timePicker']); const dashboardName = 'dashboard with filter'; const filterBar = getService('filterBar'); @@ -74,9 +75,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await PageObjects.dashboard.clickDiscardChanges(); - // confirm lose changes - await PageObjects.common.clickConfirmOnModal(); - const newTime = await PageObjects.timePicker.getTimeConfig(); expect(newTime.start).to.equal(originalTime.start); @@ -90,9 +88,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickDiscardChanges(); - // confirm lose changes - await PageObjects.common.clickConfirmOnModal(); - const query = await queryBar.getQueryString(); expect(query).to.equal(originalQuery); }); @@ -113,9 +108,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickDiscardChanges(); - // confirm lose changes - await PageObjects.common.clickConfirmOnModal(); - hasFilter = await filterBar.hasFilter('animal', 'dog'); expect(hasFilter).to.be(true); }); @@ -133,12 +125,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { redirectToOrigin: true, }); - await PageObjects.dashboard.clickDiscardChanges(); + await PageObjects.dashboard.clickDiscardChanges(false); // for this sleep see https://github.com/elastic/kibana/issues/22299 await PageObjects.common.sleep(500); // confirm lose changes - await PageObjects.common.clickConfirmOnModal(); + await testSubjects.exists('dashboardDiscardConfirmDiscard'); + await testSubjects.click('dashboardDiscardConfirmDiscard'); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(originalPanelCount); @@ -150,9 +143,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardAddPanel.addVisualization('new viz panel'); await PageObjects.dashboard.clickDiscardChanges(); - // confirm lose changes - await PageObjects.common.clickConfirmOnModal(); - const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.eql(originalPanelCount); }); @@ -171,9 +161,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Sep 19, 2015 @ 06:31:44.000', 'Sep 19, 2015 @ 06:31:44.000' ); - await PageObjects.dashboard.clickDiscardChanges(); + await PageObjects.dashboard.clickDiscardChanges(false); - await PageObjects.common.clickCancelOnModal(); + await testSubjects.exists('dashboardDiscardConfirmCancel'); + await testSubjects.click('dashboardDiscardConfirmCancel'); await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true, }); @@ -200,9 +191,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); const newTime = await PageObjects.timePicker.getTimeConfig(); - await PageObjects.dashboard.clickDiscardChanges(); + await PageObjects.dashboard.clickDiscardChanges(false); - await PageObjects.common.clickCancelOnModal(); + await testSubjects.exists('dashboardDiscardConfirmCancel'); + await testSubjects.click('dashboardDiscardConfirmCancel'); await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true }); await PageObjects.dashboard.loadSavedDashboard(dashboardName); @@ -223,7 +215,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Oct 19, 2014 @ 06:31:44.000', 'Dec 19, 2014 @ 06:31:44.000' ); - await PageObjects.dashboard.clickCancelOutOfEditMode(); + await PageObjects.dashboard.clickCancelOutOfEditMode(false); await PageObjects.common.expectConfirmModalOpenState(false); }); @@ -235,7 +227,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const originalQuery = await queryBar.getQueryString(); await queryBar.setQuery(`${originalQuery}extra stuff`); - await PageObjects.dashboard.clickCancelOutOfEditMode(); + await PageObjects.dashboard.clickCancelOutOfEditMode(false); await PageObjects.common.expectConfirmModalOpenState(false); diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index 3a59f45cef8fa8..401f33b789c859 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -15,8 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const esArchiver = getService('esArchiver'); - // Failing: See https://github.com/elastic/kibana/issues/88826 - describe.skip('Kibana browser back navigation should work', function describeIndexTests() { + describe('Kibana browser back navigation should work', function describeIndexTests() { before(async () => { await esArchiver.loadIfNeeded('discover'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 4291d67a6bc082..9c571f0f0ef86b 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -246,14 +246,26 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide return await testSubjects.exists('dashboardEditMode'); } - public async clickCancelOutOfEditMode() { + public async clickCancelOutOfEditMode(accept = true) { log.debug('clickCancelOutOfEditMode'); await testSubjects.click('dashboardViewOnlyMode'); + if (accept) { + const confirmation = await testSubjects.exists('dashboardDiscardConfirmKeep'); + if (confirmation) { + await testSubjects.click('dashboardDiscardConfirmKeep'); + } + } } - public async clickDiscardChanges() { + public async clickDiscardChanges(accept = true) { log.debug('clickDiscardChanges'); - await testSubjects.click('dashboardDiscardChanges'); + await testSubjects.click('dashboardViewOnlyMode'); + if (accept) { + const confirmation = await testSubjects.exists('dashboardDiscardConfirmDiscard'); + if (confirmation) { + await testSubjects.click('dashboardDiscardConfirmDiscard'); + } + } } public async clickQuickSave() { diff --git a/test/scripts/jenkins_baseline.sh b/test/scripts/jenkins_baseline.sh index 60926238576c77..58d86cddf65fa7 100755 --- a/test/scripts/jenkins_baseline.sh +++ b/test/scripts/jenkins_baseline.sh @@ -7,7 +7,9 @@ echo " -> building and extracting OSS Kibana distributable for use in functional node scripts/build --debug --oss echo " -> shipping metrics from build to ci-stats" -node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json +node scripts/ship_ci_stats \ + --metrics target/optimizer_bundle_metrics.json \ + --metrics packages/kbn-ui-shared-deps/target/metrics.json linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$PARENT_DIR/install/kibana" diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 5819a3ce6765e1..fa0c9522ef5fb1 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -18,7 +18,9 @@ if [[ -z "$CODE_COVERAGE" ]] ; then node scripts/build --debug --oss echo " -> shipping metrics from build to ci-stats" - node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + node scripts/ship_ci_stats \ + --metrics target/optimizer_bundle_metrics.json \ + --metrics packages/kbn-ui-shared-deps/target/metrics.json mkdir -p "$WORKSPACE/kibana-build-oss" cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 9e387f97a016e8..a483f8378b8b41 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -32,4 +32,7 @@ else echo " -> Running jest integration tests with coverage" node scripts/jest_integration --ci --verbose --coverage || true; + + echo " -> Combine code coverage in a single report" + yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.jest.config.js fi diff --git a/test/scripts/jenkins_xpack_baseline.sh b/test/scripts/jenkins_xpack_baseline.sh index aaacdd4ea3aaec..2755a6e0a705dc 100755 --- a/test/scripts/jenkins_xpack_baseline.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -8,7 +8,9 @@ cd "$KIBANA_DIR" node scripts/build --debug --no-oss echo " -> shipping metrics from build to ci-stats" -node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json +node scripts/ship_ci_stats \ + --metrics target/optimizer_bundle_metrics.json \ + --metrics packages/kbn-ui-shared-deps/target/metrics.json linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index 36865ce7c4967a..2887a51f262833 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -34,7 +34,9 @@ if [[ -z "$CODE_COVERAGE" ]] ; then node scripts/build --debug --no-oss echo " -> shipping metrics from build to ci-stats" - node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + node scripts/ship_ci_stats \ + --metrics target/optimizer_bundle_metrics.json \ + --metrics packages/kbn-ui-shared-deps/target/metrics.json linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 39f3057ec9b2a7..7806cf93756e57 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -87,6 +87,7 @@ { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, + { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, { "path": "./x-pack/plugins/observability/tsconfig.json" }, { "path": "./x-pack/plugins/osquery/tsconfig.json" }, { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index e393f3a5d2150f..1d5fd211f830f1 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -148,9 +148,10 @@ def generateReports(title) { cd .. . src/dev/code_coverage/shell_scripts/extract_archives.sh . src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh - . src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh - # zip combined reports - tar -czf kibana-coverage.tar.gz target/kibana-coverage/**/* + . src/dev/code_coverage/shell_scripts/merge_functional.sh + . src/dev/code_coverage/shell_scripts/copy_jest_report.sh + # zip functional combined report + tar -czf kibana-functional-coverage.tar.gz target/kibana-coverage/functional-combined/* """, title) } @@ -162,7 +163,7 @@ def uploadCombinedReports() { kibanaPipeline.uploadGcsArtifact( "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/coverage/combined", - 'kibana-coverage.tar.gz' + 'kibana-functional-coverage.tar.gz' ) } diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 5efcea3edb9bb2..7adf755bfc5834 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -188,7 +188,7 @@ def withGcsArtifactUpload(workerName, closure) { def ARTIFACT_PATTERNS = [ 'target/junit/**/*', 'target/kibana-*', - 'target/kibana-coverage/**/*', + 'target/kibana-coverage/jest/**/*', 'target/kibana-security-solution/**/*.png', 'target/test-metrics/*', 'target/test-suites-ci-plan.json', diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 8dba4453d56827..5a0745d3f00b7d 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -161,6 +161,7 @@ export class AlertingPlugin { private eventLogService?: IEventLogService; private eventLogger?: IEventLogger; private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; + private kibanaBaseUrl: string | undefined; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.create().pipe(first()).toPromise(); @@ -176,6 +177,7 @@ export class AlertingPlugin { core: CoreSetup, plugins: AlertingPluginsSetup ): PluginSetupContract { + this.kibanaBaseUrl = core.http.basePath.publicBaseUrl; this.licenseState = new LicenseState(plugins.licensing.license$); this.security = plugins.security; @@ -371,6 +373,7 @@ export class AlertingPlugin { eventLogger: this.eventLogger!, internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']), alertTypeRegistry: this.alertTypeRegistry!, + kibanaBaseUrl: this.kibanaBaseUrl, }); this.eventLogService!.registerSavedObjectProvider('alert', (request) => { diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 4de53a38958f48..120ab6de296dd8 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -72,6 +72,7 @@ const createExecutionHandlerParams: jest.Mocked< alertName: 'name-of-alert', tags: ['tag-A', 'tag-B'], apiKey: 'MTIzOmFiYw==', + kibanaBaseUrl: 'http://localhost:5601', alertType, logger: loggingSystemMock.create().get(), eventLogger: mockEventLogger, diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index ad024d7ddd8841..9999ea6a4d3d7c 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -39,6 +39,7 @@ export interface CreateExecutionHandlerOptions< actions: AlertAction[]; spaceId: string; apiKey: RawAlert['apiKey']; + kibanaBaseUrl: string | undefined; alertType: NormalizedAlertType< Params, State, @@ -82,6 +83,7 @@ export function createExecutionHandler< spaceId, apiKey, alertType, + kibanaBaseUrl, eventLogger, request, alertParams, @@ -126,6 +128,7 @@ export function createExecutionHandler< context, actionParams: action.params, state, + kibanaBaseUrl, alertParams, }), }; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 08b288f293bd77..bb5e0e5830159d 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -97,6 +97,7 @@ describe('Task Runner', () => { eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), alertTypeRegistry, + kibanaBaseUrl: 'https://localhost:5601', }; const mockedAlertTypeSavedObject: Alert = { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 7e96cf03e06106..744be164519995 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -160,6 +160,7 @@ export class TaskRunner< tags: string[] | undefined, spaceId: string, apiKey: RawAlert['apiKey'], + kibanaBaseUrl: string | undefined, actions: Alert['actions'], alertParams: Params ) { @@ -180,6 +181,7 @@ export class TaskRunner< actions, spaceId, alertType: this.alertType, + kibanaBaseUrl, eventLogger: this.context.eventLogger, request: this.getFakeKibanaRequest(spaceId, apiKey), alertParams, @@ -388,6 +390,7 @@ export class TaskRunner< alert.tags, spaceId, apiKey, + this.context.kibanaBaseUrl, alert.actions, alert.params ); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index 175de7384ed467..343dffa0d5e709 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -77,6 +77,7 @@ describe('Task Runner Factory', () => { eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), alertTypeRegistry: alertTypeRegistryMock.create(), + kibanaBaseUrl: 'https://localhost:5601', }; beforeEach(() => { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index 6d8a3aec92636d..a023776134e9cf 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -40,6 +40,7 @@ export interface TaskRunnerContext { basePathService: IBasePath; internalSavedObjectsRepository: ISavedObjectsRepository; alertTypeRegistry: AlertTypeRegistry; + kibanaBaseUrl: string | undefined; } export class TaskRunnerFactory { diff --git a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts index 7e95fee15e700b..4ce30c46cd9f7e 100644 --- a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts @@ -27,6 +27,7 @@ interface TransformActionParamsOptions { actionParams: AlertActionParams; alertParams: AlertTypeParams; state: AlertInstanceState; + kibanaBaseUrl?: string; context: AlertInstanceContext; } @@ -44,6 +45,7 @@ export function transformActionParams({ context, actionParams, state, + kibanaBaseUrl, alertParams, }: TransformActionParamsOptions): AlertActionParams { // when the list of variables we pass in here changes, @@ -61,6 +63,7 @@ export function transformActionParams({ context, date: new Date().toISOString(), state, + kibanaBaseUrl, params: alertParams, }; return actionsPlugin.renderActionParameterTemplates(actionTypeId, actionParams, variables); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/breakdown_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/breakdown_filter.ts index b7e16f71ce0a49..5b4934eac1f712 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/breakdown_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/breakdown_filter.ts @@ -31,13 +31,13 @@ Then(`breakdown series should appear in chart`, () => { cy.get('.euiLoadingChart').should('not.exist'); cy.get('[data-cy=pageLoadDist]').within(() => { - cy.get('div.echLegendItem__label[title=Chrome] ', DEFAULT_TIMEOUT) + cy.get('button.echLegendItem__label[title=Chrome] ', DEFAULT_TIMEOUT) .invoke('text') .should('eq', 'Chrome'); - cy.get('div.echLegendItem__label', DEFAULT_TIMEOUT).should( + cy.get('button.echLegendItem__label', DEFAULT_TIMEOUT).should( 'have.text', - 'OverallChromeChrome Mobile WebViewSafariFirefoxMobile SafariChrome MobileChrome Mobile iOS' + 'ChromeChrome Mobile WebViewSafariFirefoxMobile SafariChrome MobileChrome Mobile iOSOverall' ); }); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts index 8d01bfa70bc491..47154ee214dc42 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts @@ -52,12 +52,14 @@ Then(`should display percentile for page load chart`, () => { }); Then(`should display chart legend`, () => { - const chartLegend = 'div.echLegendItem__label'; + const chartLegend = 'button.echLegendItem__label'; waitForLoadingToFinish(); cy.get('.euiLoadingChart').should('not.exist'); - cy.get(chartLegend, DEFAULT_TIMEOUT).eq(0).should('have.text', 'Overall'); + cy.get('[data-cy=pageLoadDist]').within(() => { + cy.get(chartLegend, DEFAULT_TIMEOUT).eq(0).should('have.text', 'Overall'); + }); }); Then(`should display tooltip on hover`, () => { diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx index 0d44c08355c107..d069d4a11b4942 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx @@ -25,7 +25,7 @@ export default { core: { http: { get: (endpoint: string) => { - if (endpoint === '/api/apm/ui_filters/environments') { + if (endpoint === '/api/apm/environments') { return Promise.resolve(['production']); } else { return Promise.resolve({ diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts index 3f366300792aca..c40f6ba2b88509 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts @@ -10,11 +10,11 @@ import { useHistory } from 'react-router-dom'; import { LocalUIFilterName } from '../../../../../common/ui_filter'; import { pickKeys } from '../../../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFiltersAPIResponse } from '../../../../../server/lib/ui_filters/local_ui_filters'; +import { LocalUIFiltersAPIResponse } from '../../../../../server/lib/rum_client/ui_filters/local_ui_filters'; import { localUIFilters, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../server/lib/ui_filters/local_ui_filters/config'; +} from '../../../../../server/lib/rum_client/ui_filters/local_ui_filters/config'; import { fromQuery, toQuery, @@ -72,7 +72,7 @@ export function useLocalUIFilters({ (callApmApi) => { if (shouldFetch && urlParams.start && urlParams.end) { return callApmApi({ - endpoint: `GET /api/apm/ui_filters/local_filters/rumOverview`, + endpoint: 'GET /api/apm/rum/local_filters', params: { query: { uiFilters: JSON.stringify(uiFilters), diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index 6554f48ea3c2b3..081a3dbc907c5a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -6,7 +6,7 @@ */ import React, { useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { NotificationsStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; @@ -29,41 +29,39 @@ export function ConfirmDeleteModal({ config, onCancel, onConfirm }: Props) { const { toasts } = useApmPluginContext().core.notifications; return ( - - { + setIsDeleting(true); + await deleteConfig(config, toasts); + setIsDeleting(false); + onConfirm(); + }} + cancelButtonText={i18n.translate( + 'xpack.apm.agentConfig.deleteModal.cancel', + { defaultMessage: `Cancel` } + )} + confirmButtonText={i18n.translate( + 'xpack.apm.agentConfig.deleteModal.confirm', + { defaultMessage: `Delete` } + )} + confirmButtonDisabled={isDeleting} + buttonColor="danger" + defaultFocusedButton="confirm" + > +

+ {i18n.translate('xpack.apm.agentConfig.deleteModal.text', { + defaultMessage: `You are about to delete the configuration for service "{serviceName}" and environment "{environment}".`, + values: { + serviceName: getOptionLabel(config.service.name), + environment: getOptionLabel(config.service.environment), + }, })} - onCancel={onCancel} - onConfirm={async () => { - setIsDeleting(true); - await deleteConfig(config, toasts); - setIsDeleting(false); - onConfirm(); - }} - cancelButtonText={i18n.translate( - 'xpack.apm.agentConfig.deleteModal.cancel', - { defaultMessage: `Cancel` } - )} - confirmButtonText={i18n.translate( - 'xpack.apm.agentConfig.deleteModal.confirm', - { defaultMessage: `Delete` } - )} - confirmButtonDisabled={isDeleting} - buttonColor="danger" - defaultFocusedButton="confirm" - > -

- {i18n.translate('xpack.apm.agentConfig.deleteModal.text', { - defaultMessage: `You are about to delete the configuration for service "{serviceName}" and environment "{environment}".`, - values: { - serviceName: getOptionLabel(config.service.name), - environment: getOptionLabel(config.service.environment), - }, - })} -

-
-
+

+ ); } diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index addef74f5b25b0..63d719205c2ade 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -9,7 +9,7 @@ import { Location } from 'history'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { pickKeys } from '../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { localUIFilterNames } from '../../../server/lib/rum_client/ui_filters/local_ui_filters/config'; import { toQuery } from '../../components/shared/Links/url_helpers'; import { TimeRangeComparisonType } from '../../components/shared/time_comparison/get_time_range_comparison'; import { diff --git a/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx index 9cc11eef79eef3..8312fedc7eb039 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx @@ -18,7 +18,7 @@ import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { LocalUIFilterName } from '../../../common/ui_filter'; import { pickKeys } from '../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { localUIFilterNames } from '../../../server/lib/rum_client/ui_filters/local_ui_filters/config'; import { UIFilters } from '../../../typings/ui_filters'; import { useDeepObjectIdentity } from '../../hooks/useDeepObjectIdentity'; import { getDateRange } from './helpers'; diff --git a/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx index f6d3e848024701..b7dc29a36170d7 100644 --- a/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx @@ -36,7 +36,7 @@ export function useEnvironmentsFetcher({ (callApmApi) => { if (start && end) { return callApmApi({ - endpoint: 'GET /api/apm/ui_filters/environments', + endpoint: 'GET /api/apm/environments', params: { query: { start, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap similarity index 92% rename from x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap rename to x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap index 3baaefe203ce75..a244eee3d0544c 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ui filter queries fetches environments 1`] = ` +exports[`getEnvironments fetches environments 1`] = ` Object { "apm": Object { "events": Array [ @@ -44,7 +44,7 @@ Object { } `; -exports[`ui filter queries fetches environments without a service name 1`] = ` +exports[`getEnvironments fetches environments without a service name 1`] = ` Object { "apm": Object { "events": Array [ diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 112700d0b65833..8fedcf6224e3c9 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -15,6 +15,10 @@ import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_valu import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { withApmSpan } from '../../utils/with_apm_span'; +/** + * This is used for getting *all* environments, and does not filter by range. + * It's used in places where we get the list of all possible environments. + */ export async function getAllEnvironments({ serviceName, setup, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts similarity index 96% rename from x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts rename to x-pack/plugins/apm/server/lib/environments/get_environments.test.ts index 4a7d2029463e51..53292cabfc3389 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts @@ -11,7 +11,7 @@ import { inspectSearchParams, } from '../../utils/test_helpers'; -describe('ui filter queries', () => { +describe('getEnvironments', () => { let mock: SearchParamsMock; afterEach(() => { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.ts similarity index 95% rename from x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts rename to x-pack/plugins/apm/server/lib/environments/get_environments.ts index dc7def06259331..56f0a03910c1a4 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.ts @@ -17,6 +17,10 @@ import { withApmSpan } from '../../utils/with_apm_span'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; +/** + * This is used for getting the list of environments for the environments selector, + * filtered by range. + */ export async function getEnvironments({ setup, serviceName, diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts index 7d8bc59e61124a..63e0edd6097bf4 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts @@ -11,7 +11,7 @@ import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; import { localUIFilters, localUIFilterNames, -} from '../../ui_filters/local_ui_filters/config'; +} from '../../rum_client/ui_filters/local_ui_filters/config'; import { esKuery } from '../../../../../../../src/plugins/data/server'; export function getEsFilter(uiFilters: UIFilters) { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/__snapshots__/index.test.ts.snap similarity index 93% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/__snapshots__/index.test.ts.snap index e7ca65eb740b6a..40504cec36a633 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` +exports[`getLocalUIFilters fetches local ui filter aggregations 1`] = ` Object { "apm": Object { "events": Array [ diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/config.ts similarity index 89% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/config.ts index 27287ce80ca3ed..dfe3efe2aadfe3 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/config.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { filtersByName, LocalUIFilterName } from '../../../../common/ui_filter'; +import { + filtersByName, + LocalUIFilterName, +} from '../../../../../common/ui_filter'; export interface LocalUIFilter { name: LocalUIFilterName; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/get_local_filter_query.ts similarity index 79% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/get_local_filter_query.ts index 14b6eeb78c943f..8ea635467d0a15 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -6,12 +6,12 @@ */ import { omit } from 'lodash'; -import { mergeProjection } from '../../../projections/util/merge_projection'; -import { Projection } from '../../../projections/typings'; -import { UIFilters } from '../../../../typings/ui_filters'; -import { getEsFilter } from '../../helpers/convert_ui_filters/get_es_filter'; +import { mergeProjection } from '../../../../projections/util/merge_projection'; +import { Projection } from '../../../../projections/typings'; +import { UIFilters } from '../../../../../typings/ui_filters'; +import { getEsFilter } from '../../../helpers/convert_ui_filters/get_es_filter'; import { localUIFilters } from './config'; -import { LocalUIFilterName } from '../../../../common/ui_filter'; +import { LocalUIFilterName } from '../../../../../common/ui_filter'; export const getLocalFilterQuery = ({ uiFilters, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.test.ts similarity index 77% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.test.ts index 4452a9a80d0389..7254bb25cc5fe1 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.test.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { getLocalUIFilters } from './'; +import { getLocalUIFilters } from '.'; import { SearchParamsMock, inspectSearchParams, -} from '../../../utils/test_helpers'; -import { getServicesProjection } from '../../../projections/services'; +} from '../../../../utils/test_helpers'; +import { getServicesProjection } from '../../../../projections/services'; -describe('local ui filter queries', () => { +describe('getLocalUIFilters', () => { let mock: SearchParamsMock; beforeEach(() => { - jest.mock('../../helpers/convert_ui_filters/get_es_filter', () => { + jest.mock('../../../helpers/convert_ui_filters/get_es_filter', () => { return []; }); }); diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts similarity index 82% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts index 966f44158a7bbd..8fdeb77171862b 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts @@ -6,14 +6,14 @@ */ import { cloneDeep, orderBy } from 'lodash'; -import { UIFilters } from '../../../../typings/ui_filters'; -import { Projection } from '../../../projections/typings'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { UIFilters } from '../../../../../typings/ui_filters'; +import { Projection } from '../../../../projections/typings'; +import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { getLocalFilterQuery } from './get_local_filter_query'; -import { Setup } from '../../helpers/setup_request'; +import { Setup } from '../../../helpers/setup_request'; import { localUIFilters } from './config'; -import { LocalUIFilterName } from '../../../../common/ui_filter'; -import { withApmSpan } from '../../../utils/with_apm_span'; +import { LocalUIFilterName } from '../../../../../common/ui_filter'; +import { withApmSpan } from '../../../../utils/with_apm_span'; export type LocalUIFiltersAPIResponse = PromiseReturnType< typeof getLocalUIFilters diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index d22bcb1c501e0a..fc5d6a3dd0bcdb 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -11,6 +11,7 @@ import { apmIndexPatternTitleRoute, } from './index_pattern'; import { createApi } from './create_api'; +import { environmentsRoute } from './environments'; import { errorDistributionRoute, errorGroupsRoute, @@ -66,10 +67,6 @@ import { transactionThroughputChatsRoute, transactionGroupsComparisonStatisticsRoute, } from './transactions'; -import { - rumOverviewLocalFiltersRoute, - uiFiltersEnvironmentsRoute, -} from './ui_filters'; import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; import { createCustomLinkRoute, @@ -92,6 +89,7 @@ import { rumClientMetricsRoute, rumJSErrors, rumLongTaskMetrics, + rumOverviewLocalFiltersRoute, rumPageLoadDistBreakdownRoute, rumPageLoadDistributionRoute, rumPageViewsTrendRoute, @@ -113,6 +111,9 @@ const createApmApi = () => { .add(dynamicIndexPatternRoute) .add(apmIndexPatternTitleRoute) + // Environments + .add(environmentsRoute) + // Errors .add(errorDistributionRoute) .add(errorGroupsRoute) @@ -170,9 +171,6 @@ const createApmApi = () => { .add(transactionThroughputChatsRoute) .add(transactionGroupsComparisonStatisticsRoute) - // UI filters - .add(uiFiltersEnvironmentsRoute) - // Service map .add(serviceMapRoute) .add(serviceMapServiceNodeRoute) diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts new file mode 100644 index 00000000000000..448591f7e143ff --- /dev/null +++ b/x-pack/plugins/apm/server/routes/environments.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getEnvironments } from '../lib/environments/get_environments'; +import { createRoute } from './create_route'; +import { rangeRt } from './default_api_types'; + +export const environmentsRoute = createRoute({ + endpoint: 'GET /api/apm/environments', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + }), + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return getEnvironments({ + setup, + serviceName, + searchAggregatedTransactions, + }); + }, +}); diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 69e169e96af78a..c9fa4253bb58e2 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -6,20 +6,33 @@ */ import * as t from 'io-ts'; -import { createRoute } from './create_route'; -import { setupRequest } from '../lib/helpers/setup_request'; +import { omit } from 'lodash'; +import { jsonRt } from '../../common/runtime_types/json_rt'; +import { LocalUIFilterName } from '../../common/ui_filter'; +import { getEsFilter } from '../lib/helpers/convert_ui_filters/get_es_filter'; +import { + Setup, + setupRequest, + SetupTimeRange, +} from '../lib/helpers/setup_request'; import { getClientMetrics } from '../lib/rum_client/get_client_metrics'; -import { rangeRt, uiFiltersRt } from './default_api_types'; -import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; +import { getJSErrors } from '../lib/rum_client/get_js_errors'; +import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution'; +import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown'; import { getRumServices } from '../lib/rum_client/get_rum_services'; +import { getUrlSearch } from '../lib/rum_client/get_url_search'; import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals'; -import { getJSErrors } from '../lib/rum_client/get_js_errors'; -import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; -import { getUrlSearch } from '../lib/rum_client/get_url_search'; import { hasRumData } from '../lib/rum_client/has_rum_data'; +import { getLocalUIFilters } from '../lib/rum_client/ui_filters/local_ui_filters'; +import { localUIFilterNames } from '../lib/rum_client/ui_filters/local_ui_filters/config'; +import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; +import { Projection } from '../projections/typings'; +import { createRoute } from './create_route'; +import { rangeRt, uiFiltersRt } from './default_api_types'; +import { APMRequestHandlerContext } from './typings'; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -253,3 +266,96 @@ export const rumHasDataRoute = createRoute({ return await hasRumData({ setup }); }, }); + +// Everything below here was originally in ui_filters.ts but now is here, since +// UX is the only part of APM using UI filters now. + +const filterNamesRt = t.type({ + filterNames: jsonRt.pipe( + t.array( + t.keyof( + Object.fromEntries( + localUIFilterNames.map((filterName) => [filterName, null]) + ) as Record + ) + ) + ), +}); + +const localUiBaseQueryRt = t.intersection([ + filterNamesRt, + uiFiltersRt, + rangeRt, +]); + +function createLocalFiltersRoute< + TEndpoint extends string, + TProjection extends Projection, + TQueryRT extends t.HasProps +>({ + endpoint, + getProjection, + queryRt, +}: { + endpoint: TEndpoint; + getProjection: GetProjection< + TProjection, + t.IntersectionC<[TQueryRT, BaseQueryType]> + >; + queryRt: TQueryRT; +}) { + return createRoute({ + endpoint, + params: t.type({ + query: t.intersection([localUiBaseQueryRt, queryRt]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { uiFilters } = setup; + const { query } = context.params; + + const { filterNames } = query; + const projection = await getProjection({ + query, + context, + setup: { + ...setup, + esFilter: getEsFilter(omit(uiFilters, filterNames)), + }, + }); + + return getLocalUIFilters({ + projection, + setup, + uiFilters, + localFilterNames: filterNames, + }); + }, + }); +} + +export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ + endpoint: 'GET /api/apm/rum/local_filters', + getProjection: async ({ setup }) => { + return getRumPageLoadTransactionsProjection({ + setup, + }); + }, + queryRt: t.type({}), +}); + +type BaseQueryType = typeof localUiBaseQueryRt; + +type GetProjection< + TProjection extends Projection, + TQueryRT extends t.HasProps +> = ({ + query, + setup, + context, +}: { + query: t.TypeOf; + setup: Setup & SetupTimeRange; + context: APMRequestHandlerContext; +}) => Promise | TProjection; diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts deleted file mode 100644 index b14a47e302caa9..00000000000000 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { omit } from 'lodash'; -import { jsonRt } from '../../common/runtime_types/json_rt'; -import { LocalUIFilterName } from '../../common/ui_filter'; -import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; -import { getEsFilter } from '../lib/helpers/convert_ui_filters/get_es_filter'; -import { - Setup, - setupRequest, - SetupTimeRange, -} from '../lib/helpers/setup_request'; -import { getEnvironments } from '../lib/ui_filters/get_environments'; -import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; -import { localUIFilterNames } from '../lib/ui_filters/local_ui_filters/config'; -import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; -import { Projection } from '../projections/typings'; -import { createRoute } from './create_route'; -import { rangeRt, uiFiltersRt } from './default_api_types'; -import { APMRequestHandlerContext } from './typings'; - -export const uiFiltersEnvironmentsRoute = createRoute({ - endpoint: 'GET /api/apm/ui_filters/environments', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - }), - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - return getEnvironments({ - setup, - serviceName, - searchAggregatedTransactions, - }); - }, -}); - -const filterNamesRt = t.type({ - filterNames: jsonRt.pipe( - t.array( - t.keyof( - Object.fromEntries( - localUIFilterNames.map((filterName) => [filterName, null]) - ) as Record - ) - ) - ), -}); - -const localUiBaseQueryRt = t.intersection([ - filterNamesRt, - uiFiltersRt, - rangeRt, -]); - -function createLocalFiltersRoute< - TEndpoint extends string, - TProjection extends Projection, - TQueryRT extends t.HasProps ->({ - endpoint, - getProjection, - queryRt, -}: { - endpoint: TEndpoint; - getProjection: GetProjection< - TProjection, - t.IntersectionC<[TQueryRT, BaseQueryType]> - >; - queryRt: TQueryRT; -}) { - return createRoute({ - endpoint, - params: t.type({ - query: t.intersection([localUiBaseQueryRt, queryRt]), - }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { uiFilters } = setup; - const { query } = context.params; - - const { filterNames } = query; - const projection = await getProjection({ - query, - context, - setup: { - ...setup, - esFilter: getEsFilter(omit(uiFilters, filterNames)), - }, - }); - - return getLocalUIFilters({ - projection, - setup, - uiFilters, - localFilterNames: filterNames, - }); - }, - }); -} - -export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: 'GET /api/apm/ui_filters/local_filters/rumOverview', - getProjection: async ({ setup }) => { - return getRumPageLoadTransactionsProjection({ - setup, - }); - }, - queryRt: t.type({}), -}); - -type BaseQueryType = typeof localUiBaseQueryRt; - -type GetProjection< - TProjection extends Projection, - TQueryRT extends t.HasProps -> = ({ - query, - setup, - context, -}: { - query: t.TypeOf; - setup: Setup & SetupTimeRange; - context: APMRequestHandlerContext; -}) => Promise | TProjection; diff --git a/x-pack/plugins/apm/server/tutorial/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/elastic_cloud.ts index fac38027e1b82b..08e1ff75d43242 100644 --- a/x-pack/plugins/apm/server/tutorial/elastic_cloud.ts +++ b/x-pack/plugins/apm/server/tutorial/elastic_cloud.ts @@ -18,6 +18,7 @@ import { createGoAgentInstructions, createJavaAgentInstructions, createDotNetAgentInstructions, + createPhpAgentInstructions, } from '../../../../../src/plugins/apm_oss/server'; import { CloudSetup } from '../../../cloud/server'; @@ -105,6 +106,10 @@ function getApmAgentInstructionSet(cloudSetup?: CloudSetup) { id: INSTRUCTION_VARIANT.DOTNET, instructions: createDotNetAgentInstructions(apmServerUrl, secretToken), }, + { + id: INSTRUCTION_VARIANT.PHP, + instructions: createPhpAgentInstructions(apmServerUrl, secretToken), + }, ], }; } diff --git a/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx b/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx index c498f9e06e1d36..5badef9a71fe17 100644 --- a/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButton, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiButton, EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { AssignmentActionType } from '../table'; @@ -58,40 +58,38 @@ export class ActionControl extends React.PureComponent {this.state.showModal && ( - - + } + confirmButtonText={ + + } + onConfirm={() => { + actionHandler(action); + this.setState({ showModal: false }); + }} + onCancel={() => this.setState({ showModal: false })} + title={ + warningHeading ? ( + warningHeading + ) : ( - } - confirmButtonText={ - - } - onConfirm={() => { - actionHandler(action); - this.setState({ showModal: false }); - }} - onCancel={() => this.setState({ showModal: false })} - title={ - warningHeading ? ( - warningHeading - ) : ( - - ) - } - > - {warningMessage} - - + ) + } + > + {warningMessage} + )}
); diff --git a/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx index f09d34eaa6e614..0ab02430e90e61 100644 --- a/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx @@ -13,7 +13,6 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; @@ -104,58 +103,56 @@ class BeatsPageComponent extends React.PureComponent { {this.props.location.pathname === '/overview/enrolled_beats/enroll' && ( - - { - this.props.setUrlState({ - enrollmentToken: '', - }); - this.props.goTo(`/overview/enrolled_beats`); - }} - style={{ width: '640px' }} - > - - - - - - - { - const enrollmentTokens = await this.props.libs.tokens.createEnrollmentTokens(); - this.props.setUrlState({ - enrollmentToken: enrollmentTokens[0], - }); - }} - onBeatEnrolled={() => { - this.props.setUrlState({ - enrollmentToken: '', - }); - }} + { + this.props.setUrlState({ + enrollmentToken: '', + }); + this.props.goTo(`/overview/enrolled_beats`); + }} + style={{ width: '640px' }} + > + + + - {!this.props.urlState.enrollmentToken && ( - - { - this.props.goTo('/overview/enrolled_beats'); - }} - > - Done - - - )} - - - + + + + { + const enrollmentTokens = await this.props.libs.tokens.createEnrollmentTokens(); + this.props.setUrlState({ + enrollmentToken: enrollmentTokens[0], + }); + }} + onBeatEnrolled={() => { + this.props.setUrlState({ + enrollmentToken: '', + }); + }} + /> + {!this.props.urlState.enrollmentToken && ( + + { + this.props.goTo('/overview/enrolled_beats'); + }} + > + Done + + + )} + + )} ); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx index f06c305e47beaa..7795aa9671b83d 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiPanel, EuiProgress, EuiSpacer, @@ -75,71 +74,69 @@ export const AssetManager: FC = (props) => { }; return ( - - onClose()} - className="canvasAssetManager canvasModal--fixedSize" - maxWidth="1000px" - > - - - {strings.getModalTitle()} - - - - {isLoading ? ( - - ) : ( - - )} - - - - - -

{strings.getDescription()}

-
- - {assets.length ? ( - - {assets.map((asset) => ( - - ))} - - ) : ( - emptyAssets - )} -
- - - - onClose()} + className="canvasAssetManager canvasModal--fixedSize" + maxWidth="1000px" + > + + + {strings.getModalTitle()} + + + + {isLoading ? ( + + ) : ( + - - - - {strings.getSpaceUsedText(percentageUsed)} - - - - onClose()}> - {strings.getModalCloseButtonLabel()} - - -
-
+ )} + + + + + +

{strings.getDescription()}

+
+ + {assets.length ? ( + + {assets.map((asset) => ( + + ))} + + ) : ( + emptyAssets + )} +
+ + + + + + + + {strings.getSpaceUsedText(percentageUsed)} + + + + onClose()}> + {strings.getModalCloseButtonLabel()} + + + ); }; diff --git a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx index 38be3b8559af2c..521ced0d731f2c 100644 --- a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx +++ b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { FunctionComponent } from 'react'; @@ -39,21 +39,19 @@ export const ConfirmModal: FunctionComponent = (props) => { } return ( - - - {message} - - + + {message} + ); }; diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js index 4a5861b41d06cc..a55f73a0874676 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js @@ -8,7 +8,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - EuiOverlayMask, EuiModal, EuiModalBody, EuiModalHeader, @@ -27,48 +26,46 @@ const { DatasourceDatasourcePreview: strings } = ComponentStrings; const { DatasourceDatasourceComponent: datasourceStrings } = ComponentStrings; export const DatasourcePreview = ({ done, datatable }) => ( - - - - {strings.getModalTitle()} - - - -

- {datasourceStrings.getSaveButtonLabel()}, - }} + + + {strings.getModalTitle()} + + + +

+ {datasourceStrings.getSaveButtonLabel()}, + }} + /> +

+
+ + {datatable.type === 'error' ? ( + + ) : ( + + {datatable.rows.length > 0 ? ( + + ) : ( + {strings.getEmptyTitle()}} + titleSize="s" + body={ +

+ {strings.getEmptyFirstLineDescription()} +
+ {strings.getEmptySecondLineDescription()} +

+ } /> -

- - - {datatable.type === 'error' ? ( - - ) : ( - - {datatable.rows.length > 0 ? ( - - ) : ( - {strings.getEmptyTitle()}} - titleSize="s" - body={ -

- {strings.getEmptyFirstLineDescription()} -
- {strings.getEmptySecondLineDescription()} -

- } - /> - )} -
- )} -
-
-
+ )} + + )} + + ); DatasourcePreview.propTypes = { diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot index aea9626d7b57ad..a28986c0418a29 100644 --- a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot @@ -5,7 +5,7 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` data-eui="EuiFocusTrap" >
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx index e99cc60dfcaa4b..bc0039245f4322 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx @@ -23,7 +23,6 @@ import { EuiEmptyPrompt, EuiFieldSearch, EuiSpacer, - EuiOverlayMask, EuiButton, } from '@elastic/eui'; import { sortBy } from 'lodash'; @@ -117,16 +116,14 @@ export const SavedElementsModal: FunctionComponent = ({ } return ( - - - + ); }; @@ -176,40 +173,34 @@ export const SavedElementsModal: FunctionComponent = ({ return ( - - - - - {strings.getModalTitle()} - - - - - - - {customElementContent} - - - - {strings.getSavedElementsModalCloseButtonLabel()} - - - - + + + + {strings.getModalTitle()} + + + + + + + {customElementContent} + + + + {strings.getSavedElementsModalCloseButtonLabel()} + + + {renderDeleteModal()} {renderEditModal()} diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index edf7d33eff79c4..6e5c936a113bf8 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -12,7 +12,6 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - EuiOverlayMask, EuiModal, EuiModalFooter, EuiButton, @@ -93,16 +92,14 @@ export const Toolbar: FC = ({ const openWorkpadManager = () => setShowWorkpadManager(true); const workpadManager = ( - - - - - - {strings.getWorkpadManagerCloseButtonLabel()} - - - - + + + + + {strings.getWorkpadManagerCloseButtonLabel()} + + + ); const trays = { diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot index bf5629596d6b67..6277c599032c1e 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot @@ -77,8 +77,11 @@ exports[`Storyshots components/Variables/VarConfig default 1`] = `
= ({ )} {isModalVisible ? ( - - - + ) : null} ); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot index 4e84a9d5a0d211..010037bee4a0fd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot @@ -150,8 +150,11 @@ exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = `
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js index b07583837636ed..034e08b5c6ab84 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js @@ -9,7 +9,7 @@ import React, { PureComponent, Fragment } from 'react'; import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { deleteAutoFollowPattern } from '../store/actions'; import { arrify } from '../../../common/services/utils'; @@ -61,45 +61,43 @@ class AutoFollowPatternDeleteProviderUi extends PureComponent { ); return ( - - {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} - - {!isSingle && ( - -

- -

-
    - {ids.map((id) => ( -
  • {id}
  • - ))} -
-
- )} -
-
+ // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events + + {!isSingle && ( + +

+ +

+
    + {ids.map((id) => ( +
  • {id}
  • + ))} +
+
+ )} +
); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js index e84816d0d71af9..34697a80121ccf 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { pauseFollowerIndex } from '../../store/actions'; import { arrify } from '../../../../common/services/utils'; @@ -69,64 +69,62 @@ class FollowerIndexPauseProviderUi extends PureComponent { const hasCustomSettings = indices.some((index) => !areAllSettingsDefault(index)); return ( - - {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} - - {hasCustomSettings && ( -

- {isSingle ? ( - + {hasCustomSettings && ( +

+ {isSingle ? ( + - ) : ( - - )} + /> + )} +

+ )} + + {!isSingle && ( + +

+

- )} - - {!isSingle && ( - -

- -

- -
    - {indices.map((index) => ( -
  • {index.name}
  • - ))} -
-
- )} -
-
+ +
    + {indices.map((index) => ( +
  • {index.name}
  • + ))} +
+ + )} + ); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js index 0517f841599122..91c6cb6e243acd 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiLink, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EuiLink } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { routing } from '../../services/routing'; import { resumeFollowerIndex } from '../../store/actions'; @@ -68,77 +68,75 @@ class FollowerIndexResumeProviderUi extends PureComponent { ); return ( - - {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} - - {isSingle ? ( + // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events + + {isSingle ? ( +

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

+ ) : ( +

- - - ), - }} + id="xpack.crossClusterReplication.resumeFollowerIndex.confirmModal.multipleResumeDescriptionWithSettingWarning" + defaultMessage="Replication resumes using the default advanced settings." />

- ) : ( - -

- -

-

- -

+

+ +

-
    - {ids.map((id) => ( -
  • {id}
  • - ))} -
-
- )} -
-
+
    + {ids.map((id) => ( +
  • {id}
  • + ))} +
+ + )} + ); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js index 9b0f0ad3111e03..72d262bcf7af32 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { unfollowLeaderIndex } from '../../store/actions'; import { arrify } from '../../../../common/services/utils'; @@ -67,58 +67,56 @@ class FollowerIndexUnfollowProviderUi extends PureComponent { ); return ( - - {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} - - {isSingle ? ( - -

- + {isSingle ? ( + +

+ -

-
- ) : ( - -

- -

-
    - {ids.map((id) => ( -
  • {id}
  • - ))} -
-
- )} -
-
+ /> +

+
    + {ids.map((id) => ( +
  • {id}
  • + ))} +
+ + )} + ); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js index 8fb4fb27006cb9..8d6e47d4004b6e 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js @@ -15,7 +15,6 @@ import { EuiConfirmModal, EuiFlexGroup, EuiFlexItem, - EuiOverlayMask, EuiPageContent, EuiSpacer, } from '@elastic/eui'; @@ -182,47 +181,45 @@ export class FollowerIndexEdit extends PureComponent { ); return ( - - - ) : ( - - ) + -

- {isPaused ? ( - - ) : ( - + ) : ( + + ) + } + > +

+ {isPaused ? ( + + ) : ( + - )} -

-
-
+ /> + )} +

+ ); }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx index d505752ec3fad9..6a952d2f8d9d74 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState } from 'react'; @@ -44,24 +44,22 @@ const DeleteConfirm = ({ }); return ( - - { - setIsLoading(true); - await api.sendCancel(id); - onActionComplete(); - }} - confirmButtonText={confirm} - confirmButtonDisabled={isLoading} - cancelButtonText={cancel} - defaultFocusedButton="confirm" - buttonColor="danger" - > - {message} - - + { + setIsLoading(true); + await api.sendCancel(id); + onActionComplete(); + }} + confirmButtonText={confirm} + confirmButtonDisabled={isLoading} + cancelButtonText={cancel} + defaultFocusedButton="confirm" + buttonColor="danger" + > + {message} + ); }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx index 381c44b1bf7bef..856e7c8d434835 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState } from 'react'; @@ -52,26 +52,24 @@ const ExtendConfirm = ({ }); return ( - - { - setIsLoading(true); - await api.sendExtend(id, `${newExpiration.toISOString()}`); - setIsLoading(false); - onConfirmDismiss(); - onActionComplete(); - }} - confirmButtonText={confirm} - confirmButtonDisabled={isLoading} - cancelButtonText={extend} - defaultFocusedButton="confirm" - buttonColor="primary" - > - {message} - - + { + setIsLoading(true); + await api.sendExtend(id, `${newExpiration.toISOString()}`); + setIsLoading(false); + onConfirmDismiss(); + onActionComplete(); + }} + confirmButtonText={confirm} + confirmButtonDisabled={isLoading} + cancelButtonText={extend} + defaultFocusedButton="confirm" + buttonColor="primary" + > + {message} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx index 2eac65fc210917..593f70cda404c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx @@ -5,18 +5,18 @@ * 2.0. */ -import { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__'; +import { mountWithIntl } from '../../../../../__mocks__'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; +import { runActionColumnTests } from './shared_columns_tests'; + import { AnalyticsTable } from './'; describe('AnalyticsTable', () => { - const { navigateToUrl } = mockKibanaValues; - const items = [ { key: 'some search', @@ -69,18 +69,9 @@ describe('AnalyticsTable', () => { expect(tableContent).toContain('0'); }); - it('renders an action column', () => { + describe('renders an action column', () => { const wrapper = mountWithIntl(); - const viewQuery = wrapper.find('[data-test-subj="AnalyticsTableViewQueryButton"]').first(); - const editQuery = wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first(); - - viewQuery.simulate('click'); - expect(navigateToUrl).toHaveBeenCalledWith( - '/engines/some-engine/analytics/query_detail/some%20search' - ); - - editQuery.simulate('click'); - // TODO + runActionColumnTests(wrapper); }); it('renders an empty prompt if no items are passed', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx index a5a582d3747bcc..f90d86908d470e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx @@ -5,18 +5,18 @@ * 2.0. */ -import { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__'; +import { mountWithIntl } from '../../../../../__mocks__'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; +import { runActionColumnTests } from './shared_columns_tests'; + import { RecentQueriesTable } from './'; describe('RecentQueriesTable', () => { - const { navigateToUrl } = mockKibanaValues; - const items = [ { query_string: 'some search', @@ -63,18 +63,9 @@ describe('RecentQueriesTable', () => { expect(tableContent).toContain('3'); }); - it('renders an action column', () => { + describe('renders an action column', () => { const wrapper = mountWithIntl(); - const viewQuery = wrapper.find('[data-test-subj="AnalyticsTableViewQueryButton"]').first(); - const editQuery = wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first(); - - viewQuery.simulate('click'); - expect(navigateToUrl).toHaveBeenCalledWith( - '/engines/some-engine/analytics/query_detail/some%20search' - ); - - editQuery.simulate('click'); - // TODO + runActionColumnTests(wrapper); }); it('renders an empty prompt if no items are passed', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx index 9d8365a2f7af10..6c3d2539035aee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx @@ -9,10 +9,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { flashAPIErrors } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; import { KibanaLogic } from '../../../../../shared/kibana'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; -import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../../routes'; -import { generateEnginePath } from '../../../engine'; +import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH, ENGINE_CURATION_PATH } from '../../../../routes'; +import { generateEnginePath, EngineLogic } from '../../../engine'; import { Query, RecentQuery } from '../../types'; import { InlineTagsList } from './inline_tags_list'; @@ -63,7 +65,7 @@ export const ACTIONS_COLUMN = { onClick: (item: Query | RecentQuery) => { const { navigateToUrl } = KibanaLogic.values; - const query = (item as Query).key || (item as RecentQuery).query_string; + const query = (item as Query).key || (item as RecentQuery).query_string || '""'; navigateToUrl(generateEnginePath(ENGINE_ANALYTICS_QUERY_DETAIL_PATH, { query })); }, 'data-test-subj': 'AnalyticsTableViewQueryButton', @@ -74,12 +76,25 @@ export const ACTIONS_COLUMN = { }), description: i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.table.editTooltip', - { defaultMessage: 'Edit query analytics' } + { defaultMessage: 'Edit query' } ), type: 'icon', icon: 'pencil', - onClick: () => { - // TODO: CurationsLogic + onClick: async (item: Query | RecentQuery) => { + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + const { engineName } = EngineLogic.values; + + try { + const query = (item as Query).key || (item as RecentQuery).query_string || '""'; + const response = await http.get( + `/api/app_search/engines/${engineName}/curations/find_or_create`, + { query: { query } } + ); + navigateToUrl(generateEnginePath(ENGINE_CURATION_PATH, { curationId: response.id })); + } catch (e) { + flashAPIErrors(e); + } }, 'data-test-subj': 'AnalyticsTableEditQueryButton', }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns_tests.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns_tests.tsx new file mode 100644 index 00000000000000..cb78a6585e43c8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns_tests.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 { + mockHttpValues, + mockKibanaValues, + mockFlashMessageHelpers, +} from '../../../../../__mocks__'; +import '../../../../__mocks__/engine_logic.mock'; + +import { ReactWrapper } from 'enzyme'; + +import { nextTick } from '@kbn/test/jest'; + +export const runActionColumnTests = (wrapper: ReactWrapper) => { + const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('view action', () => { + it('navigates to the query detail view', () => { + wrapper.find('[data-test-subj="AnalyticsTableViewQueryButton"]').first().simulate('click'); + + expect(navigateToUrl).toHaveBeenCalledWith( + '/engines/some-engine/analytics/query_detail/some%20search' + ); + }); + + it('falls back to "" for the empty query', () => { + wrapper.find('[data-test-subj="AnalyticsTableViewQueryButton"]').last().simulate('click'); + expect(navigateToUrl).toHaveBeenCalledWith( + '/engines/some-engine/analytics/query_detail/%22%22' + ); + }); + }); + + describe('edit action', () => { + it('calls the find_or_create curation API, then navigates the user to the curation', async () => { + http.get.mockReturnValue(Promise.resolve({ id: 'cur-123456789' })); + wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first().simulate('click'); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/curations/find_or_create', + { + query: { query: 'some search' }, + } + ); + expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-123456789'); + }); + + it('falls back to "" for the empty query', async () => { + http.get.mockReturnValue(Promise.resolve({ id: 'cur-987654321' })); + wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').last().simulate('click'); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/curations/find_or_create', + { + query: { query: '""' }, + } + ); + expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-987654321'); + }); + + it('handles API errors', async () => { + http.get.mockReturnValue(Promise.reject()); + wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first().simulate('click'); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalled(); + }); + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx new file mode 100644 index 00000000000000..047d00ad98a0d5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx @@ -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 React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { shallow } from 'enzyme'; + +import { CurationsRouter } from './'; + +describe('CurationsRouter', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(5); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx new file mode 100644 index 00000000000000..a7f99044cc1c37 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs'; +import { NotFound } from '../../../shared/not_found'; +import { + ENGINE_CURATIONS_PATH, + ENGINE_CURATIONS_NEW_PATH, + ENGINE_CURATION_PATH, + ENGINE_CURATION_ADD_RESULT_PATH, +} from '../../routes'; + +import { CURATIONS_TITLE } from './constants'; + +interface Props { + engineBreadcrumb: BreadcrumbTrail; +} +export const CurationsRouter: React.FC = ({ engineBreadcrumb }) => { + const CURATIONS_BREADCRUMB = [...engineBreadcrumb, CURATIONS_TITLE]; + + return ( + + + + TODO: Curations overview + + + + TODO: Curation creation view + + + + TODO: Curation view (+ show a NotFound view if ID is invalid) + + + + TODO: Curation Add Result view + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/index.ts index f1eb95a0c878cb..075bc1368b3003 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/index.ts @@ -6,3 +6,4 @@ */ export { CURATIONS_TITLE } from './constants'; +export { CurationsRouter } from './curations_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx index e05fc10053ff1c..9bc838c01f636e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx @@ -20,7 +20,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -62,97 +61,94 @@ export const CustomizationModal: React.FC = ({ ); return ( - - - - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.title', + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.title', + { + defaultMessage: 'Customize document search', + } + )} + + + + + - - - - - - - - - - - - - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.cancel', + fullWidth + helpText={i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.filterFields', { - defaultMessage: 'Cancel', + defaultMessage: + 'Faceted values rendered as filters and available as query refinement', } )} - - { - onSave({ - filterFields: selectedFilterFields.map(comboBoxOptionToFieldName), - sortFields: selectedSortFields.map(comboBoxOptionToFieldName), - }); - }} > - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.save', + + + - - - + > + + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.cancel', + { + defaultMessage: 'Cancel', + } + )} + + { + onSave({ + filterFields: selectedFilterFields.map(comboBoxOptionToFieldName), + sortFields: selectedSortFields.map(comboBoxOptionToFieldName), + }); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.save', + { + defaultMessage: 'Save', + } + )} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 447e4d678bcdb6..a4ce724fdb0974 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -220,8 +220,8 @@ export const EngineNav: React.FC = () => { )} {canManageEngineCurations && ( {CURATIONS_TITLE} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index 3740882dee3db2..e6b829a43dcc1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -17,6 +17,7 @@ import { shallow } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { AnalyticsRouter } from '../analytics'; +import { CurationsRouter } from '../curations'; import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; @@ -97,7 +98,14 @@ describe('EngineRouter', () => { expect(wrapper.find(AnalyticsRouter)).toHaveLength(1); }); - it('renders an relevance tuning view', () => { + it('renders a curations view', () => { + setMockValues({ ...values, myRole: { canManageEngineCurations: true } }); + const wrapper = shallow(); + + expect(wrapper.find(CurationsRouter)).toHaveLength(1); + }); + + it('renders a relevance tuning view', () => { setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 2f1c3bc57d331e..305bdf74ae501b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -28,12 +28,13 @@ import { // META_ENGINE_SOURCE_ENGINES_PATH, ENGINE_RELEVANCE_TUNING_PATH, // ENGINE_SYNONYMS_PATH, - // ENGINE_CURATIONS_PATH, + ENGINE_CURATIONS_PATH, // ENGINE_RESULT_SETTINGS_PATH, // ENGINE_SEARCH_UI_PATH, // ENGINE_API_LOGS_PATH, } from '../../routes'; import { AnalyticsRouter } from '../analytics'; +import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; import { OVERVIEW_TITLE } from '../engine_overview'; import { EngineOverview } from '../engine_overview'; @@ -46,13 +47,13 @@ export const EngineRouter: React.FC = () => { const { myRole: { canViewEngineAnalytics, - canManageEngineRelevanceTuning, // canViewEngineDocuments, // canViewEngineSchema, // canViewEngineCrawler, // canViewMetaEngineSourceEngines, + canManageEngineRelevanceTuning, // canManageEngineSynonyms, - // canManageEngineCurations, + canManageEngineCurations, // canManageEngineResultSettings, // canManageEngineSearchUi, // canViewEngineApiLogs, @@ -97,6 +98,11 @@ export const EngineRouter: React.FC = () => { + {canManageEngineCurations && ( + + + + )} {canManageEngineRelevanceTuning && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx index ca1fa9a8d0737b..ba79d62cfe615a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { EuiTextColor, EuiOverlayMask } from '@elastic/eui'; +import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LogRetentionLogic, LogRetentionOptions } from '../../log_retention'; @@ -40,7 +40,7 @@ export const LogRetentionConfirmationModal: React.FC = () => { } return ( - + <> {openedModal === LogRetentionOptions.Analytics && ( { onSave={() => saveLogRetention(LogRetentionOptions.API, false)} /> )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index dee8858fada8b4..6fe9be083405e0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -44,9 +44,12 @@ export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`; export const ENGINE_RELEVANCE_TUNING_PATH = `${ENGINE_PATH}/relevance_tuning`; export const ENGINE_SYNONYMS_PATH = `${ENGINE_PATH}/synonyms`; -export const ENGINE_CURATIONS_PATH = `${ENGINE_PATH}/curations`; -// TODO: Curations sub-pages export const ENGINE_RESULT_SETTINGS_PATH = `${ENGINE_PATH}/result-settings`; +export const ENGINE_CURATIONS_PATH = `${ENGINE_PATH}/curations`; +export const ENGINE_CURATIONS_NEW_PATH = `${ENGINE_CURATIONS_PATH}/new`; +export const ENGINE_CURATION_PATH = `${ENGINE_CURATIONS_PATH}/:curationId`; +export const ENGINE_CURATION_ADD_RESULT_PATH = `${ENGINE_CURATIONS_PATH}/:curationId/add_result`; + export const ENGINE_SEARCH_UI_PATH = `${ENGINE_PATH}/reference_application/new`; export const ENGINE_API_LOGS_PATH = `${ENGINE_PATH}/api-logs`; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx index bbde6c5d3b55de..bd9b6b51a43b1b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx @@ -20,7 +20,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSelect, EuiSpacer, } from '@elastic/eui'; @@ -79,71 +78,69 @@ export const SchemaAddFieldModal: React.FC = ({ ); return ( - +
- - - {FIELD_NAME_MODAL_TITLE} - - -

{FIELD_NAME_MODAL_DESCRIPTION}

- - - - - + {FIELD_NAME_MODAL_TITLE} + + +

{FIELD_NAME_MODAL_DESCRIPTION}

+ + + + + + - - - - - - updateNewFieldType(e.target.value)} - data-test-subj="SchemaSelect" - /> - - - - -
- - {FIELD_NAME_MODAL_CANCEL} - - {FIELD_NAME_MODAL_ADD_FIELD} - - -
+ autoFocus + isLoading={loading} + data-test-subj="SchemaAddFieldNameField" + /> + + + + + updateNewFieldType(e.target.value)} + data-test-subj="SchemaSelect" + /> + + + + + + + {FIELD_NAME_MODAL_CANCEL} + + {FIELD_NAME_MODAL_ADD_FIELD} + +
-
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx index 9a6af035c1c8d8..717eebf5cf8733 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx @@ -20,7 +20,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSelect, } from '@elastic/eui'; @@ -59,48 +58,46 @@ export const FieldEditorModal: React.FC = () => { const ACTION_LABEL = isEditing ? UPDATE_LABEL : ADD_LABEL; return ( - +
- - - - {ACTION_LABEL} {FIELD_LABEL} - - - - - - setName(e.target.value)} - /> - - - setLabel(e.target.value)} - /> - - - - - {CANCEL_BUTTON} - - {ACTION_LABEL} {FIELD_LABEL} - - - + + + {ACTION_LABEL} {FIELD_LABEL} + + + + + + setName(e.target.value)} + /> + + + setLabel(e.target.value)} + /> + + + + + {CANCEL_BUTTON} + + {ACTION_LABEL} {FIELD_LABEL} + +
-
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 75a1779a1fda8e..d99f9a4cb1a463 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -15,7 +15,6 @@ import { EuiButton, EuiButtonEmpty, EuiConfirmModal, - EuiOverlayMask, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -101,26 +100,24 @@ export const SourceSettings: React.FC = () => { }; const confirmModal = ( - - - , - }} - /> - - + + , + }} + /> + ); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index c62f0b00258d65..247df5556ada01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -53,70 +52,65 @@ export const SourcesView: React.FC = ({ children }) => { addedSourceName: string; serviceType: string; }) => ( - - - - - - - - - - {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sourcesView.modal.heading', - { - defaultMessage: '{addedSourceName} requires additional configuration', - values: { addedSourceName }, - } - )} - - - - - - -

- - {EXTERNAL_IDENTITIES_LINK} - - ), - }} - /> -

+ + + + + + + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.sourcesView.modal.heading', { + defaultMessage: '{addedSourceName} requires additional configuration', + values: { addedSourceName }, + })} + + + + + + +

+ + {EXTERNAL_IDENTITIES_LINK} + + ), + }} + /> +

-

- - {DOCUMENT_PERMISSIONS_LINK} - - ), - }} - /> -

-
-
- - - {UNDERSTAND_BUTTON} - - -
-
+

+ + {DOCUMENT_PERMISSIONS_LINK} + + ), + }} + /> +

+ + + + + {UNDERSTAND_BUTTON} + + + ); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx index 26ac5e484f0d77..784544b0001fa0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiModal } from '@elastic/eui'; import { AddGroupModal } from './add_group_modal'; @@ -36,7 +36,6 @@ describe('AddGroupModal', () => { const wrapper = shallow(); expect(wrapper.find(EuiModal)).toHaveLength(1); - expect(wrapper.find(EuiOverlayMask)).toHaveLength(1); }); it('updates the input value', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx index fb82e9393f2a22..2c5732b4b71573 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -49,37 +48,35 @@ export const AddGroupModal: React.FC<{}> = () => { }; return ( - - -
- - {ADD_GROUP_HEADER} - + + + + {ADD_GROUP_HEADER} + - - - setNewGroupName(e.target.value)} - /> - - + + + setNewGroupName(e.target.value)} + /> + + - - {CANCEL_BUTTON} - - {ADD_GROUP_SUBMIT} - - - -
-
+ + {CANCEL_BUTTON} + + {ADD_GROUP_SUBMIT} + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx index 949ae9d502e73a..7c39414f158eff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiOverlayMask, EuiModal, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiModal, EuiEmptyPrompt } from '@elastic/eui'; import { GroupManagerModal } from './group_manager_modal'; @@ -46,7 +46,6 @@ describe('GroupManagerModal', () => { const wrapper = shallow(); expect(wrapper.find(EuiModal)).toHaveLength(1); - expect(wrapper.find(EuiOverlayMask)).toHaveLength(1); }); it('renders empty state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx index b4317ed9bd417c..1b051394dcdcf6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx @@ -21,7 +21,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -161,14 +160,12 @@ export const GroupManagerModal: React.FC = ({ ); return ( - - - {showEmptyState ? emptyState : modalContent} - - + + {showEmptyState ? emptyState : modalContent} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index df9c0b5db9b7d1..375ac7476f9b69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -12,7 +12,6 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiConfirmModal, - EuiOverlayMask, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -226,18 +225,16 @@ export const GroupOverview: React.FC = () => { {confirmDeleteModalVisible && ( - - - {CONFIRM_REMOVE_DESCRIPTION} - - + + {CONFIRM_REMOVE_DESCRIPTION} + )} { ); const confirmModal = ( - - - {PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT} - - + + {PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT} + ); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx index 28e7e2a33eaa18..3f2e55d23722c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx @@ -18,7 +18,6 @@ import { EuiSwitch, EuiCode, EuiSpacer, - EuiOverlayMask, EuiLink, EuiModal, EuiModalBody, @@ -93,25 +92,28 @@ export const OauthApplication: React.FC = () => { }; const licenseModal = ( - - - - - - - -

{LICENSE_MODAL_TITLE}

-
- - {LICENSE_MODAL_DESCRIPTION} - - - {LICENSE_MODAL_LINK} - - -
-
-
+ + + + + + +

{LICENSE_MODAL_TITLE}

+
+ + {LICENSE_MODAL_DESCRIPTION} + + + {LICENSE_MODAL_LINK} + + +
+
); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index 4ed223931d6a46..47a24e7912c3c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../shared/loading'; @@ -56,22 +56,19 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => { header={header} /> {confirmModalVisible && ( - - deleteSourceConfig(serviceType, name)} - onCancel={hideConfirmModal} - buttonColor="danger" - > - {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfig.message', - { - defaultMessage: - 'Are you sure you want to remove the OAuth configuration for {name}?', - values: { name }, - } - )} - - + deleteSourceConfig(serviceType, name)} + onCancel={hideConfirmModal} + buttonColor="danger" + > + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfig.message', + { + defaultMessage: 'Are you sure you want to remove the OAuth configuration for {name}?', + values: { name }, + } + )} + )} ); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts new file mode 100644 index 00000000000000..5b5d132591f4ef --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerCurationsRoutes } from './curations'; + +describe('curations routes', () => { + describe('GET /api/app_search/engines/{engineName}/curations/find_or_create', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{engineName}/curations/find_or_create', + }); + + registerCurationsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/curations/find_or_create', + }); + }); + + describe('validates', () => { + it('required query param', () => { + const request = { query: { query: 'some query' } }; + mockRouter.shouldValidate(request); + }); + + it('missing query', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts new file mode 100644 index 00000000000000..a4addb3ad0d3ab --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerCurationsRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{engineName}/curations/find_or_create', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + query: schema.object({ + query: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/curations/find_or_create', + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 92fdcb689db1d2..90b86138a4a6d3 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -9,6 +9,7 @@ import { RouteDependencies } from '../../plugin'; import { registerAnalyticsRoutes } from './analytics'; import { registerCredentialsRoutes } from './credentials'; +import { registerCurationsRoutes } from './curations'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; import { registerEnginesRoutes } from './engines'; import { registerSearchSettingsRoutes } from './search_settings'; @@ -21,5 +22,6 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerAnalyticsRoutes(dependencies); registerDocumentsRoutes(dependencies); registerDocumentRoutes(dependencies); + registerCurationsRoutes(dependencies); registerSearchSettingsRoutes(dependencies); }; diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts index 96b6249585bfcd..bed9b6e8390b87 100644 --- a/x-pack/plugins/fleet/common/constants/agent_policy.ts +++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts @@ -28,4 +28,19 @@ export const DEFAULT_AGENT_POLICY: Omit< monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, }; +export const DEFAULT_FLEET_SERVER_AGENT_POLICY: Omit< + AgentPolicy, + 'id' | 'updated_at' | 'updated_by' | 'revision' +> = { + name: 'Default Fleet Server policy', + namespace: 'default', + description: 'Default Fleet Server agent policy created by Kibana', + status: agentPolicyStatuses.Active, + package_policies: [], + is_default: false, + is_default_fleet_server: true, + is_managed: false, + monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, +}; + export const DEFAULT_AGENT_POLICIES_PACKAGES = [defaultPackages.System]; diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index dcddbe3539abd1..d95bc9cf736a6b 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -22,6 +22,8 @@ export * from './settings'; // setting in the future? export const SO_SEARCH_LIMIT = 10000; +export const FLEET_SERVER_INDICES_VERSION = 1; + export const FLEET_SERVER_INDICES = [ '.fleet-actions', '.fleet-agents', 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 5f41b0f70ca74f..bc139537400cc4 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -17,6 +17,7 @@ export interface NewAgentPolicy { namespace: string; description?: string; is_default?: boolean; + is_default_fleet_server?: boolean; // Optional when creating a policy is_managed?: boolean; // Optional when creating a policy monitoring_enabled?: Array>; } diff --git a/x-pack/plugins/fleet/common/types/rest_spec/common.ts b/x-pack/plugins/fleet/common/types/rest_spec/common.ts index d03129efd8fad0..de5e87d2e59a5d 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/common.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/common.ts @@ -14,3 +14,10 @@ export interface ListWithKuery extends HttpFetchQuery { sortOrder?: 'desc' | 'asc'; kuery?: string; } + +export interface ListResult { + items: T[]; + total: number; + page: number; + perPage: number; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx index d161dfcc5894c1..2b7ecc75195b0c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { EuiConfirmModal, EuiFormRow, EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../types'; @@ -92,75 +92,71 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr } return ( - - - - - } - onCancel={closeModal} - onConfirm={copyAgentPolicy} - cancelButtonText={ + - } - confirmButtonText={ + + } + onCancel={closeModal} + onConfirm={copyAgentPolicy} + cancelButtonText={ + + } + confirmButtonText={ + + } + confirmButtonDisabled={isLoading || !newAgentPolicy.name.trim()} + > +

+ +

+ } - confirmButtonDisabled={isLoading || !newAgentPolicy.name.trim()} + fullWidth > -

- -

- - } + - setNewAgentPolicy({ ...newAgentPolicy, name: e.target.value })} + value={newAgentPolicy.name} + onChange={(e) => setNewAgentPolicy({ ...newAgentPolicy, name: e.target.value })} + /> + + - - - } + } + fullWidth + > + - - setNewAgentPolicy({ ...newAgentPolicy, description: e.target.value }) - } - /> - -
-
+ value={newAgentPolicy.description} + onChange={(e) => setNewAgentPolicy({ ...newAgentPolicy, description: e.target.value })} + /> + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx index b03d70a78c51a3..014af7f54d020b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiCallOut } from '@elastic/eui'; +import { EuiConfirmModal, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; @@ -110,69 +110,67 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil } return ( - - - } - onCancel={closeModal} - onConfirm={deleteAgentPolicy} - cancelButtonText={ + + } + onCancel={closeModal} + onConfirm={deleteAgentPolicy} + cancelButtonText={ + + } + confirmButtonText={ + isLoading || isLoadingAgentsCount ? ( - } - confirmButtonText={ - isLoading || isLoadingAgentsCount ? ( - - ) : ( - - ) - } - buttonColor="danger" - confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount} - > - {isLoadingAgentsCount ? ( + ) : ( - ) : agentsCount ? ( - - - - ) : ( + ) + } + buttonColor="danger" + confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount} + > + {isLoadingAgentsCount ? ( + + ) : agentsCount ? ( + - )} - - + + ) : ( + + )} + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx index 63fb1f5b4b6385..9ed4bb6ff6ff4f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx @@ -52,8 +52,6 @@ export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => voi {error.message} ) : ( - // Property 'whiteSpace' does not exist on type 'IntrinsicAttributes & CommonProps & OwnProps & HTMLAttributes & { children?: ReactNode; }'. - // @ts-expect-error linter complains whiteSpace isn't available but docs show it on EuiCodeBlockImpl {fullAgentPolicyToYaml(yamlData!.item)} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx index 02d7a6423edc84..f3d01e6b528cae 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiCallOut, EuiOverlayMask, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { AgentPolicy } from '../../../types'; @@ -18,58 +18,56 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{ agentPolicy: AgentPolicy; }> = ({ onConfirm, onCancel, agentCount, agentPolicy }) => { return ( - - - } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="primary" + + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="primary" + > + - -
- + {agentPolicy.name}, - }} - /> -
-
- - -
-
+ values={{ + policyName: {agentPolicy.name}, + }} + /> +
+ + + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx index 2ea94e88ed8c61..80952fee05bb4d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useMemo, useRef, useState } from 'react'; -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useStartServices, sendRequest, sendDeletePackagePolicy, useConfig } from '../../../hooks'; @@ -142,78 +142,76 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ } return ( - - + } + onCancel={closeModal} + onConfirm={deletePackagePolicies} + cancelButtonText={ + + } + confirmButtonText={ + isLoading || isLoadingAgentsCount ? ( - } - onCancel={closeModal} - onConfirm={deletePackagePolicies} - cancelButtonText={ + ) : ( - } - confirmButtonText={ - isLoading || isLoadingAgentsCount ? ( - - ) : ( + ) + } + buttonColor="danger" + confirmButtonDisabled={isLoading || isLoadingAgentsCount} + > + {isLoadingAgentsCount ? ( + + ) : agentsCount ? ( + <> + + } + > {agentPolicy.name}, }} /> - ) - } - buttonColor="danger" - confirmButtonDisabled={isLoading || isLoadingAgentsCount} - > - {isLoadingAgentsCount ? ( - - ) : agentsCount ? ( - <> - - } - > - {agentPolicy.name}, - }} - /> - - - - ) : null} - {!isLoadingAgentsCount && ( - - )} - - + + + + ) : null} + {!isLoadingAgentsCount && ( + + )} + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx index 69bff78d604132..a50cc18d46f550 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiOverlayMask, EuiFormFieldset, EuiCheckbox } from '@elastic/eui'; +import { EuiConfirmModal, EuiFormFieldset, EuiCheckbox } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; import { @@ -81,90 +81,88 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ } return ( - - - ) : ( - - ) - } - onCancel={onClose} - onConfirm={onSubmit} - cancelButtonText={ + - } - confirmButtonDisabled={isSubmitting} - confirmButtonText={ - isSingleAgent ? ( - - ) : ( + ) : ( + + ) + } + onCancel={onClose} + onConfirm={onSubmit} + cancelButtonText={ + + } + confirmButtonDisabled={isSubmitting} + confirmButtonText={ + isSingleAgent ? ( + + ) : ( + + ) + } + buttonColor="danger" + > +

+ {isSingleAgent ? ( + + ) : ( + + )} +

+ - ) - } - buttonColor="danger" + ), + }} > -

- {isSingleAgent ? ( + - ) : ( - - )} -

- - ), - }} - > - - } - checked={forceUnenroll} - onChange={(e) => setForceUnenroll(e.target.checked)} - disabled={useForceUnenroll} - /> - -
-
+ values={{ count: agentCount }} + /> + } + checked={forceUnenroll} + onChange={(e) => setForceUnenroll(e.target.checked)} + disabled={useForceUnenroll} + /> + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index a836e3ec3149bc..57f4007a002740 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -7,13 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiConfirmModal, - EuiOverlayMask, - EuiBetaBadge, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiConfirmModal, EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; import { @@ -74,85 +68,83 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ } return ( - - - - {isSingleAgent ? ( + + + {isSingleAgent ? ( + + ) : ( + + )} + + + - ) : ( + } + tooltipContent={ - )} - - - - } - tooltipContent={ - - } - /> - - - } - onCancel={onClose} - onConfirm={onSubmit} - cancelButtonText={ + } + /> + + + } + onCancel={onClose} + onConfirm={onSubmit} + cancelButtonText={ + + } + confirmButtonDisabled={isSubmitting} + confirmButtonText={ + isSingleAgent ? ( - } - confirmButtonDisabled={isSubmitting} - confirmButtonText={ - isSingleAgent ? ( - - ) : ( - - ) - } - > -

- {isSingleAgent ? ( - - ) : ( - - )} -

-
-
+ ) : ( + + ) + } + > +

+ {isSingleAgent ? ( + + ) : ( + + )} +

+ ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/confirm_delete_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/confirm_delete_modal.tsx index 22e2e68e6d83e9..565657c70e17f6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/confirm_delete_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/confirm_delete_modal.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiCallOut, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EuiCallOut } from '@elastic/eui'; import { EnrollmentAPIKey } from '../../../../types'; interface Props { @@ -19,33 +19,31 @@ interface Props { export const ConfirmEnrollmentTokenDelete = (props: Props) => { const { onCancel, onConfirm, enrollmentKey } = props; return ( - - + - - - + color="danger" + /> + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx index ef3ca3ce664c11..5144b2a6487862 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -18,50 +18,48 @@ interface ConfirmPackageInstallProps { export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => { const { onCancel, onConfirm, packageName, numOfAssets } = props; return ( - - + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + > + - } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - > - - } + /> + +

+ - -

- -

-
-
+

+ ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx index 7688c0269d3588..2def57b0409447 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -18,58 +18,56 @@ interface ConfirmPackageUninstallProps { export const ConfirmPackageUninstall = (props: ConfirmPackageUninstallProps) => { const { onCancel, onConfirm, packageName, numOfAssets } = props; return ( - - + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + buttonColor="danger" + > + } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - buttonColor="danger" > - - } - > -

- -

-
-

-
-
+ + +

+ +

+ ); }; diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index 8aa66c4ae5f4ac..154e78feae2832 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -7,7 +7,8 @@ import { ElasticsearchClient, SavedObjectsClient } from 'kibana/server'; import * as AgentService from '../services/agents'; -import { isFleetServerSetup } from '../services/fleet_server_migration'; +import { isFleetServerSetup } from '../services/fleet_server'; + export interface AgentUsage { total: number; online: number; diff --git a/x-pack/plugins/fleet/server/mocks.ts b/x-pack/plugins/fleet/server/mocks.ts index c650995c809cbb..430e38bd1bc3ef 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks.ts @@ -53,6 +53,7 @@ export const createPackagePolicyServiceMock = () => { get: jest.fn(), getByIDs: jest.fn(), list: jest.fn(), + listIds: jest.fn(), update: jest.fn(), runExternalCallbacks: jest.fn(), } as jest.Mocked; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index d89db7f1ac3415..d4cd39b274f052 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -83,7 +83,7 @@ import { agentCheckinState } from './services/agents/checkin/state'; import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation } from './services/epm/packages'; import { makeRouterEnforcingSuperuser } from './routes/security'; -import { isFleetServerSetup } from './services/fleet_server_migration'; +import { startFleetServerSetup } from './services/fleet_server'; export interface FleetSetupDeps { licensing: LicensingPluginSetup; @@ -297,18 +297,9 @@ export class FleetPlugin licenseService.start(this.licensing$); agentCheckinState.start(); - const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; - if (fleetServerEnabled) { - // We need licence to be initialized before using the SO service. - await this.licensing$.pipe(first()).toPromise(); - - const fleetSetup = await isFleetServerSetup(); - - if (!fleetSetup) { - this.logger?.warn( - 'Extra setup is needed to be able to use central management for agent, please visit the Fleet app in Kibana.' - ); - } + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + // Break the promise chain, the error handling is done in startFleetServerSetup + startFleetServerSetup(); } return { diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 2b44975cc3b4de..813279f2a800fc 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -47,6 +47,7 @@ jest.mock('../../services/package_policy', (): { get: jest.fn(), getByIDs: jest.fn(), list: jest.fn(), + listIds: jest.fn(), update: jest.fn(), runExternalCallbacks: jest.fn((callbackType, newPackagePolicy, context, request) => Promise.resolve(newPackagePolicy) diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index f2eb8be5c030c6..5b851c692ad3f6 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -162,6 +162,7 @@ const getSavedObjectTypes = ( description: { type: 'text' }, namespace: { type: 'keyword' }, is_default: { type: 'boolean' }, + is_default_fleet_server: { type: 'boolean' }, is_managed: { type: 'boolean' }, status: { type: 'keyword' }, package_policies: { type: 'keyword' }, diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts index 49a0d6fc7737fa..15e68ace987b98 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts @@ -17,12 +17,11 @@ export const migrateAgentToV7120: SavedObjectMigrationFn, + Exclude, AgentPolicy > = (agentPolicyDoc) => { - const isV12 = 'is_managed' in agentPolicyDoc.attributes; - if (!isV12) { - agentPolicyDoc.attributes.is_managed = false; - } + agentPolicyDoc.attributes.is_managed = false; + agentPolicyDoc.attributes.is_default_fleet_server = false; + return agentPolicyDoc; }; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index f31f38796055c1..44962ea31c56c5 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -35,6 +35,7 @@ import { dataTypes, FleetServerPolicy, AGENT_POLICY_INDEX, + DEFAULT_FLEET_SERVER_AGENT_POLICY, } from '../../common'; import { AgentPolicyNameExistsError, @@ -133,6 +134,39 @@ class AgentPolicyService { }; } + public async ensureDefaultFleetServerAgentPolicy( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient + ): Promise<{ + created: boolean; + policy: AgentPolicy; + }> { + const agentPolicies = await soClient.find({ + type: AGENT_POLICY_SAVED_OBJECT_TYPE, + searchFields: ['is_default_fleet_server'], + search: 'true', + }); + + if (agentPolicies.total === 0) { + const newDefaultAgentPolicy: NewAgentPolicy = { + ...DEFAULT_FLEET_SERVER_AGENT_POLICY, + }; + + return { + created: true, + policy: await this.create(soClient, esClient, newDefaultAgentPolicy), + }; + } + + return { + created: false, + policy: { + id: agentPolicies.saved_objects[0].id, + ...agentPolicies.saved_objects[0].attributes, + }, + }; + } + public async create( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, @@ -569,18 +603,19 @@ class AgentPolicyService { if (!(await isAgentsSetup(soClient))) { return; } - const policy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId); - if (!policy || !policy.revision) { + const policy = await agentPolicyService.get(soClient, agentPolicyId); + const fullPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId); + if (!policy || !fullPolicy || !fullPolicy.revision) { return; } const fleetServerPolicy: FleetServerPolicy = { '@timestamp': new Date().toISOString(), - revision_idx: policy.revision, + revision_idx: fullPolicy.revision, coordinator_idx: 0, - data: (policy as unknown) as FleetServerPolicy['data'], - policy_id: policy.id, - default_fleet_server: false, + data: (fullPolicy as unknown) as FleetServerPolicy['data'], + policy_id: fullPolicy.id, + default_fleet_server: policy.is_default_fleet_server === true, }; await esClient.create({ diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index cc4be6b31734a3..1ada940dd793c9 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -80,6 +80,10 @@ class AppContextService { return this.security; } + public hasSecurity() { + return !!this.security; + } + public getCloud() { return this.cloud; } diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts new file mode 100644 index 00000000000000..96e642ba9884e8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 hash from 'object-hash'; +import { setupFleetServerIndexes } from './elastic_index'; +import ESFleetAgentIndex from './elasticsearch/fleet_agents.json'; +import ESFleetPoliciesIndex from './elasticsearch/fleet_policies.json'; +import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.json'; +import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; +import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; +import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; + +const FLEET_INDEXES_MIGRATION_HASH = { + '.fleet-actions': hash(EsFleetActionsIndex), + '.fleet-agents': hash(ESFleetAgentIndex), + '.fleet-enrollment-apy-keys': hash(ESFleetEnrollmentApiKeysIndex), + '.fleet-policies': hash(ESFleetPoliciesIndex), + '.fleet-policies-leader': hash(ESFleetPoliciesLeaderIndex), + '.fleet-servers': hash(ESFleetServersIndex), +}; + +describe('setupFleetServerIndexes ', () => { + it('should create all the indices and aliases if nothings exists', async () => { + const esMock = elasticsearchServiceMock.createInternalClient(); + await setupFleetServerIndexes(esMock); + + const indexesCreated = esMock.indices.create.mock.calls.map((call) => call[0].index).sort(); + expect(indexesCreated).toEqual([ + '.fleet-actions_1', + '.fleet-agents_1', + '.fleet-enrollment-api-keys_1', + '.fleet-policies-leader_1', + '.fleet-policies_1', + '.fleet-servers_1', + ]); + const aliasesCreated = esMock.indices.updateAliases.mock.calls + .map((call) => (call[0].body as any)?.actions[0].add.alias) + .sort(); + + expect(aliasesCreated).toEqual([ + '.fleet-actions', + '.fleet-agents', + '.fleet-enrollment-api-keys', + '.fleet-policies', + '.fleet-policies-leader', + '.fleet-servers', + ]); + }); + + it('should not create any indices and create aliases if indices exists but not the aliases', async () => { + const esMock = elasticsearchServiceMock.createInternalClient(); + // @ts-expect-error + esMock.indices.exists.mockResolvedValue({ body: true }); + // @ts-expect-error + esMock.indices.getMapping.mockImplementation((params: { index: string }) => { + return { + body: { + [params.index]: { + mappings: { + _meta: { + // @ts-expect-error + migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], + }, + }, + }, + }, + }; + }); + + await setupFleetServerIndexes(esMock); + + expect(esMock.indices.create).not.toBeCalled(); + const aliasesCreated = esMock.indices.updateAliases.mock.calls + .map((call) => (call[0].body as any)?.actions[0].add.alias) + .sort(); + + expect(aliasesCreated).toEqual([ + '.fleet-actions', + '.fleet-agents', + '.fleet-enrollment-api-keys', + '.fleet-policies', + '.fleet-policies-leader', + '.fleet-servers', + ]); + }); + + it('should put new indices mapping if the mapping has been updated ', async () => { + const esMock = elasticsearchServiceMock.createInternalClient(); + // @ts-expect-error + esMock.indices.exists.mockResolvedValue({ body: true }); + // @ts-expect-error + esMock.indices.getMapping.mockImplementation((params: { index: string }) => { + return { + body: { + [params.index]: { + mappings: { + _meta: { + migrationHash: 'NOT_VALID_HASH', + }, + }, + }, + }, + }; + }); + + await setupFleetServerIndexes(esMock); + + expect(esMock.indices.create).not.toBeCalled(); + const indexesMappingUpdated = esMock.indices.putMapping.mock.calls + .map((call) => call[0].index) + .sort(); + + expect(indexesMappingUpdated).toEqual([ + '.fleet-actions_1', + '.fleet-agents_1', + '.fleet-enrollment-api-keys_1', + '.fleet-policies-leader_1', + '.fleet-policies_1', + '.fleet-servers_1', + ]); + }); + + it('should not create any indices or aliases if indices and aliases already exists', async () => { + const esMock = elasticsearchServiceMock.createInternalClient(); + + // @ts-expect-error + esMock.indices.exists.mockResolvedValue({ body: true }); + // @ts-expect-error + esMock.indices.getMapping.mockImplementation((params: { index: string }) => { + return { + body: { + [params.index]: { + mappings: { + _meta: { + // @ts-expect-error + migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], + }, + }, + }, + }, + }; + }); + // @ts-expect-error + esMock.indices.existsAlias.mockResolvedValue({ body: true }); + + await setupFleetServerIndexes(esMock); + + expect(esMock.indices.create).not.toBeCalled(); + expect(esMock.indices.updateAliases).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts new file mode 100644 index 00000000000000..15672be756fe2d --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; +import hash from 'object-hash'; + +import { FLEET_SERVER_INDICES, FLEET_SERVER_INDICES_VERSION } from '../../../common'; +import { appContextService } from '../app_context'; +import ESFleetAgentIndex from './elasticsearch/fleet_agents.json'; +import ESFleetPoliciesIndex from './elasticsearch/fleet_policies.json'; +import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.json'; +import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; +import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; +import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; + +const FLEET_INDEXES: Array<[typeof FLEET_SERVER_INDICES[number], any]> = [ + ['.fleet-actions', EsFleetActionsIndex], + ['.fleet-agents', ESFleetAgentIndex], + ['.fleet-enrollment-api-keys', ESFleetEnrollmentApiKeysIndex], + ['.fleet-policies', ESFleetPoliciesIndex], + ['.fleet-policies-leader', ESFleetPoliciesLeaderIndex], + ['.fleet-servers', ESFleetServersIndex], +]; + +export async function setupFleetServerIndexes( + esClient = appContextService.getInternalUserESClient() +) { + await Promise.all( + FLEET_INDEXES.map(async ([indexAlias, indexData]) => { + const index = `${indexAlias}_${FLEET_SERVER_INDICES_VERSION}`; + await createOrUpdateIndex(esClient, index, indexData); + await createAliasIfDoNotExists(esClient, indexAlias, index); + }) + ); +} + +export async function createAliasIfDoNotExists( + esClient: ElasticsearchClient, + alias: string, + index: string +) { + const { body: exists } = await esClient.indices.existsAlias({ + name: alias, + }); + + if (exists === true) { + return; + } + await esClient.indices.updateAliases({ + body: { + actions: [ + { + add: { index, alias }, + }, + ], + }, + }); +} + +async function createOrUpdateIndex( + esClient: ElasticsearchClient, + indexName: string, + indexData: any +) { + const resExists = await esClient.indices.exists({ + index: indexName, + }); + + // Support non destructive migration only (adding new field) + if (resExists.body === true) { + return updateIndex(esClient, indexName, indexData); + } + + return createIndex(esClient, indexName, indexData); +} + +async function updateIndex(esClient: ElasticsearchClient, indexName: string, indexData: any) { + const res = await esClient.indices.getMapping({ + index: indexName, + }); + + const migrationHash = hash(indexData); + if (res.body[indexName].mappings?._meta?.migrationHash !== migrationHash) { + await esClient.indices.putMapping({ + index: indexName, + body: Object.assign({ + ...indexData.mappings, + _meta: { ...(indexData.mappings._meta || {}), migrationHash }, + }), + }); + } +} + +async function createIndex(esClient: ElasticsearchClient, indexName: string, indexData: any) { + try { + const migrationHash = hash(indexData); + await esClient.indices.create({ + index: indexName, + body: { + ...indexData, + mappings: Object.assign({ + ...indexData.mappings, + _meta: { ...(indexData.mappings._meta || {}), migrationHash }, + }), + }, + }); + } catch (err) { + // Swallow already exists errors as concurent Kibana can try to create that indice + if (err?.body?.error?.type !== 'resource_already_exists_exception') { + throw err; + } + } +} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json new file mode 100644 index 00000000000000..3008ee74ab50c8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json @@ -0,0 +1,30 @@ +{ + "settings": {}, + "mappings": { + "dynamic": false, + "properties": { + "action_id": { + "type": "keyword" + }, + "agents": { + "type": "keyword" + }, + "data": { + "enabled": false, + "type": "object" + }, + "expiration": { + "type": "date" + }, + "input_type": { + "type": "keyword" + }, + "@timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + } +} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json new file mode 100644 index 00000000000000..9937e9ad66e56f --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json @@ -0,0 +1,220 @@ +{ + "settings": {}, + "mappings": { + "dynamic": false, + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "action_seq_no": { + "type": "integer" + }, + "active": { + "type": "boolean" + }, + "agent": { + "properties": { + "id": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "default_api_key": { + "type": "keyword" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "properties": { + "elastic": { + "properties": { + "agent": { + "properties": { + "build": { + "properties": { + "original": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "id": { + "type": "keyword" + }, + "log_level": { + "type": "keyword" + }, + "snapshot": { + "type": "boolean" + }, + "upgradeable": { + "type": "boolean" + }, + "version": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 16 + } + } + } + } + } + } + }, + "host": { + "properties": { + "architecture": { + "type": "keyword" + }, + "hostname": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "id": { + "type": "keyword" + }, + "ip": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 64 + } + } + }, + "mac": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 17 + } + } + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "os": { + "properties": { + "family": { + "type": "keyword" + }, + "full": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 128 + } + } + }, + "kernel": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 128 + } + } + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "platform": { + "type": "keyword" + }, + "version": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 32 + } + } + } + } + } + } + }, + "packages": { + "type": "keyword" + }, + "policy_coordinator_idx": { + "type": "integer" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision_idx": { + "type": "integer" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "upgrade_started_at": { + "type": "date" + }, + "upgraded_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "object", + "enabled": false + } + } + } +} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json new file mode 100644 index 00000000000000..fc3898aff55c66 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json @@ -0,0 +1,32 @@ +{ + "settings": {}, + "mappings": { + "dynamic": false, + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "keyword" + }, + "api_key_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + } +} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json new file mode 100644 index 00000000000000..50078aaa5ea988 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json @@ -0,0 +1,27 @@ +{ + "settings": {}, + "mappings": { + "dynamic": false, + "properties": { + "coordinator_idx": { + "type": "integer" + }, + "data": { + "enabled": false, + "type": "object" + }, + "default_fleet_server": { + "type": "boolean" + }, + "policy_id": { + "type": "keyword" + }, + "revision_idx": { + "type": "integer" + }, + "@timestamp": { + "type": "date" + } + } + } +} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json new file mode 100644 index 00000000000000..ad3dfe64df57c3 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json @@ -0,0 +1,21 @@ +{ + "settings": {}, + "mappings": { + "dynamic": false, + "properties": { + "server": { + "properties": { + "id": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "@timestamp": { + "type": "date" + } + } + } +} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json new file mode 100644 index 00000000000000..9ee68735d5b6fc --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json @@ -0,0 +1,47 @@ +{ + "settings": {}, + "mappings": { + "dynamic": false, + "properties": { + "agent": { + "properties": { + "id": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "ip": { + "type": "keyword" + }, + "name": { + "type": "keyword" + } + } + }, + "server": { + "properties": { + "id": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "@timestamp": { + "type": "date" + } + } + } +} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts new file mode 100644 index 00000000000000..0b54dc0d168b4f --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/index.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 { first } from 'rxjs/operators'; +import { appContextService } from '../app_context'; +import { licenseService } from '../license'; +import { setupFleetServerIndexes } from './elastic_index'; +import { runFleetServerMigration } from './saved_object_migrations'; + +let _isFleetServerSetup = false; +let _isPending = false; +let _status: Promise | undefined; +let _onResolve: (arg?: any) => void; + +export function isFleetServerSetup() { + return _isFleetServerSetup; +} + +export function awaitIfFleetServerSetupPending() { + if (!_isPending) { + return; + } + + return _status; +} + +export async function startFleetServerSetup() { + _isPending = true; + _status = new Promise((resolve) => { + _onResolve = resolve; + }); + const logger = appContextService.getLogger(); + if (!appContextService.hasSecurity()) { + // Fleet will not work if security is not enabled + logger?.warn('Fleet requires the security plugin to be enabled.'); + return; + } + + try { + // We need licence to be initialized before using the SO service. + await licenseService.getLicenseInformation$()?.pipe(first())?.toPromise(); + await setupFleetServerIndexes(); + await runFleetServerMigration(); + _isFleetServerSetup = true; + } catch (err) { + logger?.error('Setup for central management of agents failed.'); + logger?.error(err); + } + _isPending = false; + if (_onResolve) { + _onResolve(); + } +} diff --git a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts similarity index 84% rename from x-pack/plugins/fleet/server/services/fleet_server_migration.ts rename to x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index 170bec54983c0e..84e6b06e59844f 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -17,38 +17,12 @@ import { AgentSOAttributes, FleetServerAgent, SO_SEARCH_LIMIT, - FLEET_SERVER_PACKAGE, - FLEET_SERVER_INDICES, -} from '../../common'; -import { listEnrollmentApiKeys, getEnrollmentAPIKey } from './api_keys/enrollment_api_key_so'; -import { appContextService } from './app_context'; -import { getInstallation } from './epm/packages'; - -import { isAgentsSetup } from './agents'; -import { agentPolicyService } from './agent_policy'; - -export async function isFleetServerSetup() { - const pkgInstall = await getInstallation({ - savedObjectsClient: getInternalUserSOClient(), - pkgName: FLEET_SERVER_PACKAGE, - }); - - if (!pkgInstall) { - return false; - } +} from '../../../common'; +import { listEnrollmentApiKeys, getEnrollmentAPIKey } from '../api_keys/enrollment_api_key_so'; +import { appContextService } from '../app_context'; - const esClient = appContextService.getInternalUserESClient(); - const exists = await Promise.all( - FLEET_SERVER_INDICES.map(async (index) => { - const res = await esClient.indices.exists({ - index, - }); - return res.statusCode !== 404; - }) - ); - - return exists.every((exist) => exist === true); -} +import { isAgentsSetup } from '../agents'; +import { agentPolicyService } from '../agent_policy'; export async function runFleetServerMigration() { // If Agents are not setup skip as there is nothing to migrate diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index a882ceb0037f24..335cd7c956faf9 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -20,6 +20,7 @@ import { PackagePolicyInputStream, PackageInfo, ListWithKuery, + ListResult, packageToPackagePolicy, isPackageLimited, doesAgentPolicyAlreadyIncludePackage, @@ -248,7 +249,7 @@ class PackagePolicyService { public async list( soClient: SavedObjectsClientContract, options: ListWithKuery - ): Promise<{ items: PackagePolicy[]; total: number; page: number; perPage: number }> { + ): Promise> { const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; const packagePolicies = await soClient.find({ @@ -272,6 +273,30 @@ class PackagePolicyService { }; } + public async listIds( + soClient: SavedObjectsClientContract, + options: ListWithKuery + ): Promise> { + const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; + + const packagePolicies = await soClient.find<{}>({ + type: SAVED_OBJECT_TYPE, + sortField, + sortOrder, + page, + perPage, + fields: [], + filter: kuery ? normalizeKuery(SAVED_OBJECT_TYPE, kuery) : undefined, + }); + + return { + items: packagePolicies.saved_objects.map((packagePolicySO) => packagePolicySO.id), + total: packagePolicies.total, + page, + perPage, + }; + } + public async update( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 6c8f24e7995742..2a3166e9dc7296 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -23,7 +23,6 @@ import { Output, DEFAULT_AGENT_POLICIES_PACKAGES, FLEET_SERVER_PACKAGE, - FLEET_SERVER_INDICES, } from '../../common'; import { SO_SEARCH_LIMIT } from '../constants'; import { getPackageInfo } from './epm/packages'; @@ -34,7 +33,7 @@ import { awaitIfPending } from './setup_utils'; import { createDefaultSettings } from './settings'; import { ensureAgentActionPolicyChangeExists } from './agents'; import { appContextService } from './app_context'; -import { runFleetServerMigration } from './fleet_server_migration'; +import { awaitIfFleetServerSetupPending } from './fleet_server'; const FLEET_ENROLL_USERNAME = 'fleet_enroll'; const FLEET_ENROLL_ROLE = 'fleet_enroll'; @@ -56,15 +55,20 @@ async function createSetupSideEffects( esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser ): Promise { + const isFleetServerEnabled = appContextService.getConfig()?.agents.fleetServerEnabled; const [ installedPackages, defaultOutput, { created: defaultAgentPolicyCreated, defaultAgentPolicy }, + { created: defaultFleetServerPolicyCreated, policy: defaultFleetServerPolicy }, ] = await Promise.all([ // packages installed by default ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient), + isFleetServerEnabled + ? agentPolicyService.ensureDefaultFleetServerAgentPolicy(soClient, esClient) + : {}, updateFleetRoleIfExists(callCluster), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { @@ -83,26 +87,30 @@ async function createSetupSideEffects( // By moving this outside of the Promise.all, the upgrade will occur first, and then we'll attempt to reinstall any // packages that are stuck in the installing state. await ensurePackagesCompletedInstall(soClient, callCluster); - if (appContextService.getConfig()?.agents.fleetServerEnabled) { - await ensureInstalledPackage({ - savedObjectsClient: soClient, - pkgName: FLEET_SERVER_PACKAGE, - callCluster, - }); - await ensureFleetServerIndicesCreated(esClient); - await runFleetServerMigration(); - } - if (appContextService.getConfig()?.agents?.fleetServerEnabled) { - await ensureInstalledPackage({ + if (isFleetServerEnabled) { + await awaitIfFleetServerSetupPending(); + + const fleetServerPackage = await ensureInstalledPackage({ savedObjectsClient: soClient, pkgName: FLEET_SERVER_PACKAGE, callCluster, }); - await ensureFleetServerIndicesCreated(esClient); - await runFleetServerMigration(); + + if (defaultFleetServerPolicyCreated) { + await addPackageToAgentPolicy( + soClient, + esClient, + callCluster, + fleetServerPackage, + defaultFleetServerPolicy, + defaultOutput + ); + } } + // If we just created the default fleet server policy add the fleet server package + // If we just created the default policy, ensure default packages are added to it if (defaultAgentPolicyCreated) { const agentPolicyWithPackagePolicies = await agentPolicyService.get( @@ -169,21 +177,6 @@ async function updateFleetRoleIfExists(callCluster: CallESAsCurrentUser) { return putFleetRole(callCluster); } -async function ensureFleetServerIndicesCreated(esClient: ElasticsearchClient) { - await Promise.all( - FLEET_SERVER_INDICES.map(async (index) => { - const res = await esClient.indices.exists({ - index, - }); - if (res.statusCode === 404) { - await esClient.indices.create({ - index, - }); - } - }) - ); -} - async function putFleetRole(callCluster: CallESAsCurrentUser) { return callCluster('transport.request', { method: 'PUT', diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 152fb2e132f626..e6dc206912c4bd 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -5,13 +5,14 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, }, "include": [ // add all the folders containg files to be compiled "common/**/*", "public/**/*", "server/**/*", + "server/**/*.json", "scripts/**/*", "package.json", "../../typings/**/*" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx index f20ea0f5d1bf46..8971f18ef8e5fe 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx @@ -13,7 +13,6 @@ import { EuiComboBox, EuiForm, EuiFormRow, - EuiOverlayMask, EuiConfirmModal, EuiFieldText, EuiSpacer, @@ -257,46 +256,44 @@ export const AddPolicyToTemplateConfirmModal: React.FunctionComponent = ( ); return ( - - - -

- {' '} - - } - /> -

-
- - {renderForm()} -
-
+ />{' '} + + } + /> +

+ + + {renderForm()} + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx index 80039a18ef17a9..e42aa97a10d4f1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx @@ -8,7 +8,7 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { PolicyFromES } from '../../../../../common/types'; import { toasts } from '../../../services/notification'; @@ -50,33 +50,31 @@ export class ConfirmDelete extends Component { values: { name: policyToDelete.name }, }); return ( - - - } - confirmButtonText={ - - } - buttonColor="danger" - > -
- -
-
-
+ + } + confirmButtonText={ + + } + buttonColor="danger" + > +
+ +
+
); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx index 550cefb488f667..36df4d9527a5c8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx @@ -16,7 +16,6 @@ import { EuiSelect, EuiForm, EuiFormRow, - EuiOverlayMask, EuiConfirmModal, EuiModal, EuiModalBody, @@ -246,63 +245,59 @@ export class AddLifecyclePolicyConfirmModal extends Component { ); if (!policies.length) { return ( - - - - {title} - + + + {title} + - - + + } + color="warning" + > +

+ - } - color="warning" - > -

- - - -

-
-
-
-
+ +

+ + + ); } return ( - - - } - confirmButtonText={ - - } - > - {this.renderForm()} - - + + } + confirmButtonText={ + + } + > + {this.renderForm()} + ); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.tsx index 8ce4ac052fce23..2f22a0b347db95 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.tsx @@ -8,7 +8,7 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { removeLifecycleForIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; @@ -57,54 +57,52 @@ export class RemoveLifecyclePolicyConfirmModal extends Component { const { closeModal, indexNames } = this.props; return ( - - + } + onCancel={closeModal} + onConfirm={this.removePolicy} + cancelButtonText={ + + } + buttonColor="danger" + confirmButtonText={ + + } + > + +

- } - onCancel={closeModal} - onConfirm={this.removePolicy} - cancelButtonText={ - - } - buttonColor="danger" - confirmButtonText={ - - } - > - -

- -

+

-
    - {indexNames.map((indexName) => ( -
  • {indexName}
  • - ))} -
-
-
-
+
    + {indexNames.map((indexName) => ( +
  • {indexName}
  • + ))} +
+ + ); } } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx index cac26d948b11aa..0b20bebf431436 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -81,49 +81,47 @@ export const ComponentTemplatesDeleteModal = ({ }; return ( - - + } + onCancel={handleOnCancel} + onConfirm={handleDeleteComponentTemplates} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + <> +

- } - onCancel={handleOnCancel} - onConfirm={handleDeleteComponentTemplates} - cancelButtonText={ - - } - confirmButtonText={ - - } - > - <> -

- -

+

-
    - {componentTemplatesToDelete.map((name) => ( -
  • {name}
  • - ))} -
- -
-
+
    + {componentTemplatesToDelete.map((name) => ( +
  • {name}
  • + ))} +
+ + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx index e6a7e42c089365..2a65906ea56b40 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiBadge, EuiCode } from '@elastic/eui'; +import { EuiConfirmModal, EuiBadge, EuiCode } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NormalizedFields, NormalizedField } from '../../../types'; @@ -59,55 +59,53 @@ export const ModalConfirmationDeleteFields = ({ : null; return ( - - + <> + {fieldsTree && ( + <> +

+ {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteFieldsDescription', + { + defaultMessage: 'This will also delete the following fields.', + } + )} +

+ + + )} + {aliases && ( + <> +

+ {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteAliasesDescription', + { + defaultMessage: 'The following aliases will also be deleted.', + } + )} +

+
    + {aliases.map((aliasPath) => ( +
  • + {aliasPath} +
  • + ))} +
+ )} - buttonColor="danger" - confirmButtonText={confirmButtonText} - > - <> - {fieldsTree && ( - <> -

- {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteFieldsDescription', - { - defaultMessage: 'This will also delete the following fields.', - } - )} -

- - - )} - {aliases && ( - <> -

- {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteAliasesDescription', - { - defaultMessage: 'The following aliases will also be deleted.', - } - )} -

-
    - {aliases.map((aliasPath) => ( -
  • - {aliasPath} -
  • - ))} -
- - )} - -
-
+ + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx index a4c88b1e61b8b4..8f023156456dcb 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx @@ -8,14 +8,7 @@ import React, { useState, useRef, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiConfirmModal, - EuiOverlayMask, - EuiCallOut, - EuiText, - EuiSpacer, - EuiButtonEmpty, -} from '@elastic/eui'; +import { EuiConfirmModal, EuiCallOut, EuiText, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; import { JsonEditor, OnJsonEditorUpdateHandler } from '../../shared_imports'; import { validateMappings, MappingsValidationError } from '../../lib'; @@ -220,61 +213,55 @@ export const LoadMappingsProvider = ({ onJson, children }: Props) => { {children(openModal)} {state.isModalOpen && ( - - - {view === 'json' ? ( - // The CSS override for the EuiCodeEditor requires a parent .application css class -
- - mappings, - }} - /> - - - - - + {view === 'json' ? ( + // The CSS override for the EuiCodeEditor requires a parent .application css class +
+ + mappings, }} /> -
- ) : ( - <> - - -

{i18nTexts.validationErrors.description}

-
- -
    - {state.errors!.slice(0, totalErrorsToDisplay).map((error, i) => ( -
  1. {getErrorMessage(error)}
  2. - ))} -
- {state.errors!.length > MAX_ERRORS_TO_DISPLAY && renderErrorsFilterButton()} -
- - )} - - + + + + + +
+ ) : ( + <> + + +

{i18nTexts.validationErrors.description}

+
+ +
    + {state.errors!.slice(0, totalErrorsToDisplay).map((error, i) => ( +
  1. {getErrorMessage(error)}
  2. + ))} +
+ {state.errors!.length > MAX_ERRORS_TO_DISPLAY && renderErrorsFilterButton()} +
+ + )} +
)} ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx index e48172c417d0ad..f9ecca1f8cb61b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { useDispatch } from '../../mappings_state_context'; import { NormalizedRuntimeField } from '../../types'; @@ -68,22 +68,20 @@ export const DeleteRuntimeFieldProvider = ({ children }: Props) => { {children(deleteField)} {state.isModalOpen && ( - - - + )} ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index 2f49e95a1bd62a..d7db98731427db 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -86,7 +86,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { id="xpack.idxMgmt.mappingsEditor.dataType.constantKeywordLongDescription" defaultMessage="Constant keyword fields are a special type of keyword fields for fields that contain the same keyword across all documents in the index. Supports the same queries and aggregations as {keyword} fields." values={{ - keyword: {'keyword'}, + keyword: {'keyword'}, }} />

@@ -836,7 +836,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { id="xpack.idxMgmt.mappingsEditor.dataType.pointLongDescription" defaultMessage="Point fields enable searching of {code} pairs that fall in a 2-dimensional planar coordinate system." values={{ - code: {'x,y'}, + code: {'x,y'}, }} />

diff --git a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx index 0dc2407d22c294..f22fa2a3b4f8a5 100644 --- a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox, EuiBadge } from '@elastic/eui'; +import { EuiConfirmModal, EuiCallOut, EuiCheckbox, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -81,95 +81,93 @@ export const TemplateDeleteModal = ({ }; return ( - - - } - onCancel={handleOnCancel} - onConfirm={handleDeleteTemplates} - cancelButtonText={ - - } - confirmButtonText={ + + } + onCancel={handleOnCancel} + onConfirm={handleDeleteTemplates} + cancelButtonText={ + + } + confirmButtonText={ + + } + confirmButtonDisabled={hasSystemTemplate ? !isDeleteConfirmed : false} + > + +

- } - confirmButtonDisabled={hasSystemTemplate ? !isDeleteConfirmed : false} - > - -

- -

+

-
    - {templatesToDelete.map(({ name }) => ( -
  • - {name} - {name.startsWith('.') ? ( - - {' '} - - - - - ) : null} -
  • - ))} -
- {hasSystemTemplate && ( - + {templatesToDelete.map(({ name }) => ( +
  • + {name} + {name.startsWith('.') ? ( + + {' '} + + + + + ) : null} +
  • + ))} + + {hasSystemTemplate && ( + + } + color="danger" + iconType="alert" + data-test-subj="deleteSystemTemplateCallOut" + > +

    + +

    + } - color="danger" - iconType="alert" - data-test-subj="deleteSystemTemplateCallOut" - > -

    - -

    - - } - checked={isDeleteConfirmed} - onChange={(e) => setIsDeleteConfirmed(e.target.checked)} - /> -
    - )} -
    -
    -
    + checked={isDeleteConfirmed} + onChange={(e) => setIsDeleteConfirmed(e.target.checked)} + /> + + )} + +
    ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx index 7475a87ca24d99..f555706a28cdd4 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment } from 'react'; -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -82,69 +82,67 @@ export const DeleteDataStreamConfirmationModal: React.FunctionComponent = }; return ( - - - } - onCancel={() => onClose()} - onConfirm={handleDeleteDataStreams} - cancelButtonText={ - - } - confirmButtonText={ - - } - > - - - } - color="danger" - iconType="alert" - > -

    - -

    -
    - - - + + } + onCancel={() => onClose()} + onConfirm={handleDeleteDataStreams} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + + + } + color="danger" + iconType="alert" + >

    +
    + + + +

    + +

    -
      - {dataStreams.map((name) => ( -
    • {name}
    • - ))} -
    -
    -
    -
    +
      + {dataStreams.map((name) => ( +
    • {name}
    • + ))} +
    + + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 6282469b092669..20a4af59bab111 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -20,7 +20,6 @@ import { EuiPopover, EuiSpacer, EuiConfirmModal, - EuiOverlayMask, EuiCheckbox, } from '@elastic/eui'; @@ -301,102 +300,97 @@ export class IndexActionsContextMenu extends Component { const selectedIndexCount = indexNames.length; return ( - - { - if (!this.forcemergeSegmentsError()) { - this.closePopoverAndExecute(() => { - forcemergeIndices(this.state.forcemergeSegments); - this.setState({ - forcemergeSegments: null, - showForcemergeSegmentsModal: null, - }); + { + if (!this.forcemergeSegmentsError()) { + this.closePopoverAndExecute(() => { + forcemergeIndices(this.state.forcemergeSegments); + this.setState({ + forcemergeSegments: null, + showForcemergeSegmentsModal: null, }); - } - }} - cancelButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.forceMerge.confirmModal.cancelButtonText', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.forceMerge.confirmModal.confirmButtonText', + }); + } + }} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.forceMerge.confirmModal.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.forceMerge.confirmModal.confirmButtonText', + { + defaultMessage: 'Force merge', + } + )} + > +

    + +

    + +
      + {indexNames.map((indexName) => ( +
    • {indexName}
    • + ))} +
    + +

    -

    - -
      - {indexNames.map((indexName) => ( -
    • {indexName}
    • - ))} -
    - - -

    - -

    -
    + /> +

    +
    - + - + - - { - this.setState({ forcemergeSegments: event.target.value }); - }} - min={1} - name="maxNumberSegments" - /> - - -
    -
    + { + this.setState({ forcemergeSegments: event.target.value }); + }} + min={1} + name="maxNumberSegments" + /> + + + ); }; @@ -494,39 +488,37 @@ export class IndexActionsContextMenu extends Component { ); return ( - - { - this.confirmAction(false); - this.closeConfirmModal(); - }} - onConfirm={() => this.closePopoverAndExecute(deleteIndices)} - buttonColor="danger" - confirmButtonDisabled={hasSystemIndex ? !isActionConfirmed : false} - cancelButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.cancelButtonText', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.confirmButtonText', - { - defaultMessage: 'Delete {selectedIndexCount, plural, one {index} other {indices} }', - values: { selectedIndexCount }, - } - )} - > - {hasSystemIndex ? systemIndexModalBody : standardIndexModalBody} - - + { + this.confirmAction(false); + this.closeConfirmModal(); + }} + onConfirm={() => this.closePopoverAndExecute(deleteIndices)} + buttonColor="danger" + confirmButtonDisabled={hasSystemIndex ? !isActionConfirmed : false} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.confirmButtonText', + { + defaultMessage: 'Delete {selectedIndexCount, plural, one {index} other {indices} }', + values: { selectedIndexCount }, + } + )} + > + {hasSystemIndex ? systemIndexModalBody : standardIndexModalBody} + ); }; @@ -536,96 +528,91 @@ export class IndexActionsContextMenu extends Component { const selectedIndexCount = indexNames.length; return ( - - { + this.confirmAction(false); + this.closeConfirmModal(); + }} + onConfirm={() => this.closePopoverAndExecute(closeIndices)} + buttonColor="danger" + confirmButtonDisabled={!isActionConfirmed} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.closeIndex.confirmModal.confirmButtonText', + { + defaultMessage: 'Close {selectedIndexCount, plural, one {index} other {indices} }', + values: { selectedIndexCount }, + } + )} + > +

    + +

    + +
      + {indexNames.map((indexName) => ( +
    • + {indexName} + {isSystemIndexByName[indexName] ? ( + + {' '} + + + + + ) : ( + '' + )} +
    • + ))} +
    + + { - this.confirmAction(false); - this.closeConfirmModal(); - }} - onConfirm={() => this.closePopoverAndExecute(closeIndices)} - buttonColor="danger" - confirmButtonDisabled={!isActionConfirmed} - cancelButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.cancelButtonText', + 'xpack.idxMgmt.indexActionsMenu.closeIndex.proceedWithCautionCallOutTitle', { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.closeIndex.confirmModal.confirmButtonText', - { - defaultMessage: 'Close {selectedIndexCount, plural, one {index} other {indices} }', - values: { selectedIndexCount }, + defaultMessage: 'Closing a system index can break Kibana', } )} + color="danger" + iconType="alert" >

    - -
      - {indexNames.map((indexName) => ( -
    • - {indexName} - {isSystemIndexByName[indexName] ? ( - - {' '} - - - - - ) : ( - '' - )} -
    • - ))} -
    - - -

    + -

    - - } - checked={isActionConfirmed} - onChange={(e) => this.confirmAction(e.target.checked)} - /> -
    -
    -
    + } + checked={isActionConfirmed} + onChange={(e) => this.confirmAction(e.target.checked)} + /> + + ); }; @@ -633,71 +620,69 @@ export class IndexActionsContextMenu extends Component { const { freezeIndices, indexNames } = this.props; return ( - - this.closePopoverAndExecute(freezeIndices)} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.freezeEntity.confirmModal.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.freezeEntity.confirmModal.confirmButtonText', + { + defaultMessage: 'Freeze {count, plural, one {index} other {indices}}', + values: { + count: indexNames.length, + }, + } + )} + > +

    + +

    + +
      + {indexNames.map((indexName) => ( +
    • {indexName}
    • + ))} +
    + + this.closePopoverAndExecute(freezeIndices)} - cancelButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.freezeEntity.confirmModal.cancelButtonText', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.freezeEntity.confirmModal.confirmButtonText', + 'xpack.idxMgmt.indexActionsMenu.freezeEntity.proceedWithCautionCallOutTitle', { - defaultMessage: 'Freeze {count, plural, one {index} other {indices}}', - values: { - count: indexNames.length, - }, + defaultMessage: 'Proceed with caution', } )} + color="warning" + iconType="help" >

    -

    - -
      - {indexNames.map((indexName) => ( -
    • {indexName}
    • - ))} -
    - - -

    - -

    -
    -
    -
    + /> +

    + + ); }; diff --git a/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx index 985db9872ef3f6..654cba0721bb8a 100644 --- a/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx @@ -16,7 +16,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiFieldText, EuiSpacer, EuiSwitch, @@ -40,69 +39,67 @@ export const SavedViewCreateModal = ({ close, save, isInvalid }: Props) => { }, [includeTime, save, viewName]); return ( - - - - - - - - - - + + + - - - } - checked={includeTime} - onChange={onCheckChange} - /> - - - - - + + - - + + + + - - - - - - - + } + checked={includeTime} + onChange={onCheckChange} + /> + + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/components/saved_views/update_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/update_modal.tsx index 15d0d162604a4b..c6d87d9a8ca158 100644 --- a/x-pack/plugins/infra/public/components/saved_views/update_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/update_modal.tsx @@ -16,7 +16,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiFieldText, EuiSpacer, EuiSwitch, @@ -46,69 +45,67 @@ export function SavedViewUpdateModal - - - - - - - - - + + + - - - } - checked={includeTime} - onChange={onCheckChange} - /> - - - - - + + - - + + + + - - - - - - -
    + } + checked={includeTime} + onChange={onCheckChange} + /> + + + + + + + + + + + + + + + ); } diff --git a/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx index 9ab742d720eb5e..aad50c4dcb45d0 100644 --- a/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx @@ -9,13 +9,7 @@ import React, { useCallback, useState, useMemo } from 'react'; import { EuiButtonEmpty, EuiModalFooter, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiOverlayMask, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, -} from '@elastic/eui'; +import { EuiModal, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody } from '@elastic/eui'; import { EuiSelectable } from '@elastic/eui'; import { EuiSelectableOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -64,51 +58,49 @@ export function SavedViewListModal - - - - - - - - - {(list, search) => ( - <> - {search} -
    {list}
    - - )} -
    -
    - - - - - - - - -
    - + + + + + + + + + {(list, search) => ( + <> + {search} +
    {list}
    + + )} +
    +
    + + + + + + + + +
    ); } diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx index df21a66a5226f4..5537ef9541f892 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx @@ -9,7 +9,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiModal, - EuiOverlayMask, EuiText, EuiTextColor, EuiToolTip, @@ -51,33 +50,26 @@ export const PageViewLogInContext: React.FC = () => { } return ( - - - - - - - - - - - - - - + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx index 10da326322ad4d..c0f9c758fc2785 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent, useRef, useState, useCallback } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; +import { EuiConfirmModal, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../../shared_imports'; @@ -78,64 +78,62 @@ export const ModalProvider: FunctionComponent = ({ onDone, children }) => <> {children(() => setIsModalVisible(true))} {isModalVisible ? ( - - { + { + setIsModalVisible(false); + }} + onConfirm={async () => { + try { + const json = jsonContent.current.data.format(); + const { processors, on_failure: onFailure } = json; + // This function will throw if it cannot parse the pipeline object + deserialize({ processors, onFailure }); + onDone(json as any); setIsModalVisible(false); - }} - onConfirm={async () => { - try { - const json = jsonContent.current.data.format(); - const { processors, on_failure: onFailure } = json; - // This function will throw if it cannot parse the pipeline object - deserialize({ processors, onFailure }); - onDone(json as any); - setIsModalVisible(false); - } catch (e) { - setError(e); - } - }} - cancelButtonText={i18nTexts.buttons.cancel} - confirmButtonDisabled={!isValidJson} - confirmButtonText={i18nTexts.buttons.confirm} - maxWidth={600} - > -
    - - - + } catch (e) { + setError(e); + } + }} + cancelButtonText={i18nTexts.buttons.cancel} + confirmButtonDisabled={!isValidJson} + confirmButtonText={i18nTexts.buttons.confirm} + maxWidth={600} + > +
    + + + - + - {error && ( - <> - - {i18nTexts.error.body} - - - - )} + {error && ( + <> + + {i18nTexts.error.body} + + + + )} - -
    - - + +
    +
    ) : undefined} ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx index d6cf2d0ae05e89..19176a27a07781 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx @@ -85,7 +85,7 @@ const fieldsConfig: FieldsConfig = { {','} }} + values={{ value: {','} }} /> ), }, @@ -104,7 +104,7 @@ const fieldsConfig: FieldsConfig = { {'"'} }} + values={{ value: {'"'} }} /> ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx index 17af47f1569e04..e8e956daff2074 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx @@ -61,7 +61,7 @@ const fieldsConfig: FieldsConfig = { {'UTC'} }} + values={{ timezone: {'UTC'} }} /> ), }, @@ -75,7 +75,7 @@ const fieldsConfig: FieldsConfig = { {'ENGLISH'} }} + values={{ timezone: {'ENGLISH'} }} /> ), }, @@ -102,7 +102,7 @@ export const DateProcessor: FunctionComponent = () => { id="xpack.ingestPipelines.pipelineEditor.dateForm.targetFieldHelpText" defaultMessage="Output field. If empty, the input field is updated in place. Defaults to {defaultField}." values={{ - defaultField: {'@timestamp'}, + defaultField: {'@timestamp'}, }} /> } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx index d4b1ec876bfd50..182b9ecd845e92 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx @@ -82,7 +82,7 @@ const fieldsConfig: FieldsConfig = { {'yyyy-MM-dd'} }} + values={{ value: {'yyyy-MM-dd'} }} /> ), }, @@ -102,7 +102,7 @@ const fieldsConfig: FieldsConfig = { {"yyyy-MM-dd'T'HH:mm:ss.SSSXX"} }} + values={{ value: {"yyyy-MM-dd'T'HH:mm:ss.SSSXX"} }} /> ), }, @@ -119,7 +119,7 @@ const fieldsConfig: FieldsConfig = { {'UTC'} }} + values={{ timezone: {'UTC'} }} /> ), }, @@ -136,7 +136,7 @@ const fieldsConfig: FieldsConfig = { {'ENGLISH'} }} + values={{ locale: {'ENGLISH'} }} /> ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx index 609ce8a1f8ae63..641a6e73d90251 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx @@ -82,7 +82,7 @@ const getFieldsConfig = (esDocUrl: string): Record => { {'""'} }} + values={{ value: {'""'} }} /> ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx index 2f0699fac729d8..7848872800df45 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx @@ -31,8 +31,8 @@ const fieldsConfig: FieldsConfig = { id="xpack.ingestPipelines.pipelineEditor.geoIPForm.databaseFileHelpText" defaultMessage="GeoIP2 database file in the {ingestGeoIP} configuration directory. Defaults to {databaseFile}." values={{ - databaseFile: {'GeoLite2-City.mmdb'}, - ingestGeoIP: {'ingest-geoip'}, + databaseFile: {'GeoLite2-City.mmdb'}, + ingestGeoIP: {'ingest-geoip'}, }} /> ), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx index 2b14a79afb8df1..9575e6d690e006 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx @@ -168,7 +168,7 @@ export const Inference: FunctionComponent = () => { {'ml.inference.'} }} + values={{ targetField: {'ml.inference.'} }} /> } /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx index 4309df214410b2..d14048c4e00dce 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx @@ -66,7 +66,7 @@ export const UserAgent: FunctionComponent = () => { id="xpack.ingestPipelines.pipelineEditor.userAgentForm.targetFieldHelpText" defaultMessage="Output field. Defaults to {defaultField}." values={{ - defaultField: {'user_agent'}, + defaultField: {'user_agent'}, }} /> } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx index eb5a1baac78fb9..26ae69ead3b5b6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { ProcessorInternal, ProcessorSelector } from '../types'; interface Props { @@ -18,39 +18,37 @@ interface Props { export const ProcessorRemoveModal = ({ processor, onResult, selector }: Props) => { return ( - - - } - onCancel={() => onResult({ confirmed: false, selector })} - onConfirm={() => onResult({ confirmed: true, selector })} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -

    - -

    -
    -
    + + } + onCancel={() => onResult({ confirmed: false, selector })} + onConfirm={() => onResult({ confirmed: true, selector })} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

    + +

    +
    ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index aac4da7c16bbf0..9095ab1927cb98 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -135,7 +135,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { {'my-index-yyyy-MM-dd'} }} + values={{ value: {'my-index-yyyy-MM-dd'} }} /> ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx index 305ccce4e31b5a..d71a6fb80bde1e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; interface Props { confirmResetTestOutput: () => void; @@ -46,18 +46,16 @@ export const ResetDocumentsModal: FunctionComponent = ({ closeModal, }) => { return ( - - -

    {i18nTexts.modalDescription}

    -
    -
    + +

    {i18nTexts.modalDescription}

    +
    ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx index 230cc52a1c1696..63cf7af2737aa3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -78,49 +78,47 @@ export const PipelineDeleteModal = ({ }; return ( - - + } + onCancel={handleOnCancel} + onConfirm={handleDeletePipelines} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + <> +

    - } - onCancel={handleOnCancel} - onConfirm={handleDeletePipelines} - cancelButtonText={ - - } - confirmButtonText={ - - } - > - <> -

    - -

    +

    -
      - {pipelinesToDelete.map((name) => ( -
    • {name}
    • - ))} -
    - -
    -
    +
      + {pipelinesToDelete.map((name) => ( +
    • {name}
    • + ))} +
    + +
    ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 8047807093eefe..e487e185a8c8fe 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -57,7 +57,15 @@ function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { return fieldA.displayName.localeCompare(fieldB.displayName, undefined, { sensitivity: 'base' }); } -const supportedFieldTypes = new Set(['string', 'number', 'boolean', 'date', 'ip', 'document']); +const supportedFieldTypes = new Set([ + 'string', + 'number', + 'boolean', + 'date', + 'ip', + 'histogram', + 'document', +]); const fieldTypeNames: Record = { document: i18n.translate('xpack.lens.datatypes.record', { defaultMessage: 'record' }), @@ -66,6 +74,7 @@ const fieldTypeNames: Record = { boolean: i18n.translate('xpack.lens.datatypes.boolean', { defaultMessage: 'boolean' }), date: i18n.translate('xpack.lens.datatypes.date', { defaultMessage: 'date' }), ip: i18n.translate('xpack.lens.datatypes.ipAddress', { defaultMessage: 'IP' }), + histogram: i18n.translate('xpack.lens.datatypes.histogram', { defaultMessage: 'histogram' }), }; // Wrapper around esQuery.buildEsQuery, handling errors (e.g. because a query can't be parsed) by diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index e11ee580deb9bf..e724a34be20e8f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -32,6 +32,8 @@ const typeToFn: Record = { median: 'aggMedian', }; +const supportedTypes = ['number', 'histogram']; + function buildMetricOperation>({ type, displayName, @@ -61,7 +63,7 @@ function buildMetricOperation>({ timeScalingMode: optionalTimeScaling ? 'optional' : undefined, getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => { if ( - fieldType === 'number' && + supportedTypes.includes(fieldType) && aggregatable && (!aggregationRestrictions || aggregationRestrictions[type]) ) { @@ -77,7 +79,7 @@ function buildMetricOperation>({ return Boolean( newField && - newField.type === 'number' && + supportedTypes.includes(newField.type) && newField.aggregatable && (!newField.aggregationRestrictions || newField.aggregationRestrictions![type]) ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index 07bab16b7096f4..9ac91be5a17ec2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -68,6 +68,52 @@ describe('percentile', () => { }; }); + describe('getPossibleOperationForField', () => { + it('should accept number', () => { + expect( + percentileOperation.getPossibleOperationForField({ + name: 'bytes', + displayName: 'bytes', + type: 'number', + esTypes: ['long'], + aggregatable: true, + }) + ).toEqual({ + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }); + }); + + it('should accept histogram', () => { + expect( + percentileOperation.getPossibleOperationForField({ + name: 'response_time', + displayName: 'response_time', + type: 'histogram', + esTypes: ['histogram'], + aggregatable: true, + }) + ).toEqual({ + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }); + }); + + it('should reject keywords', () => { + expect( + percentileOperation.getPossibleOperationForField({ + name: 'origin', + displayName: 'origin', + type: 'string', + esTypes: ['keyword'], + aggregatable: true, + }) + ).toBeUndefined(); + }); + }); + describe('toEsAggsFn', () => { it('should reflect params correctly', () => { const percentileColumn = layer.columns.col2 as PercentileIndexPatternColumn; @@ -134,6 +180,34 @@ describe('percentile', () => { }); }); + describe('isTransferable', () => { + it('should transfer from number to histogram', () => { + const indexPattern = createMockedIndexPattern(); + indexPattern.getFieldByName = jest.fn().mockReturnValue({ + name: 'response_time', + displayName: 'response_time', + type: 'histogram', + esTypes: ['histogram'], + aggregatable: true, + }); + expect( + percentileOperation.isTransferable( + { + label: '', + sourceField: 'response_time', + isBucketed: false, + dataType: 'number', + operationType: 'percentile', + params: { + percentile: 95, + }, + }, + indexPattern + ) + ).toBeTruthy(); + }); + }); + describe('param editor', () => { it('should render current percentile', () => { const updateLayerSpy = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index f236b2932b2d3f..e7654380bd85f1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -42,6 +42,8 @@ function ofName(name: string, percentile: number) { const DEFAULT_PERCENTILE_VALUE = 95; +const supportedFieldTypes = ['number', 'histogram']; + export const percentileOperation: OperationDefinition = { type: 'percentile', displayName: i18n.translate('xpack.lens.indexPattern.percentile', { @@ -49,7 +51,7 @@ export const percentileOperation: OperationDefinition { - if (fieldType === 'number' && aggregatable && !aggregationRestrictions) { + if (supportedFieldTypes.includes(fieldType) && aggregatable && !aggregationRestrictions) { return { dataType: 'number', isBucketed: false, @@ -62,7 +64,7 @@ export const percentileOperation: OperationDefinition = DatasourceDimensionDropProp dropType: DropType; }; -export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; +export type FieldOnlyDataType = 'document' | 'ip' | 'histogram'; +export type DataType = 'string' | 'number' | 'date' | 'boolean' | FieldOnlyDataType; // An operation represents a column in a table, not any information // about how the column was created such as whether it is a sum or average. diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 8b121232162aad..772934160a0584 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -26,6 +26,7 @@ const columnSortOrder = { ip: 3, boolean: 4, number: 5, + histogram: 6, }; /** diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index e1681a74c2951c..7fd884755d86df 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -86,7 +86,11 @@ export async function initFieldsRoute(setup: CoreSetup) { return result; }; - if (field.type === 'number') { + if (field.type === 'histogram') { + return res.ok({ + body: await getNumberHistogram(search, field, false), + }); + } else if (field.type === 'number') { return res.ok({ body: await getNumberHistogram(search, field), }); @@ -120,21 +124,31 @@ export async function initFieldsRoute(setup: CoreSetup) { export async function getNumberHistogram( aggSearchWithBody: (body: unknown) => Promise, - field: IFieldType + field: IFieldType, + useTopHits = true ): Promise { const fieldRef = getFieldRef(field); - const searchBody = { + const baseAggs = { + min_value: { + min: { field: field.name }, + }, + max_value: { + max: { field: field.name }, + }, + sample_count: { value_count: { ...fieldRef } }, + }; + const searchWithoutHits = { + sample: { + sampler: { shard_size: SHARD_SIZE }, + aggs: { ...baseAggs }, + }, + }; + const searchWithHits = { sample: { sampler: { shard_size: SHARD_SIZE }, aggs: { - min_value: { - min: { field: field.name }, - }, - max_value: { - max: { field: field.name }, - }, - sample_count: { value_count: { ...fieldRef } }, + ...baseAggs, top_values: { terms: { ...fieldRef, size: 10 }, }, @@ -142,14 +156,18 @@ export async function getNumberHistogram( }, }; - const minMaxResult = (await aggSearchWithBody(searchBody)) as ESSearchResponse< - unknown, - { body: { aggs: typeof searchBody } } - >; + const minMaxResult = (await aggSearchWithBody( + useTopHits ? searchWithHits : searchWithoutHits + )) as + | ESSearchResponse + | ESSearchResponse; const minValue = minMaxResult.aggregations!.sample.min_value.value; const maxValue = minMaxResult.aggregations!.sample.max_value.value; - const terms = minMaxResult.aggregations!.sample.top_values; + const terms = + 'top_values' in minMaxResult.aggregations!.sample + ? minMaxResult.aggregations!.sample.top_values + : { buckets: [] }; const topValuesBuckets = { buckets: terms.buckets.map((bucket) => ({ count: bucket.doc_count, @@ -169,7 +187,12 @@ export async function getNumberHistogram( sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, sampledDocuments: minMaxResult.aggregations!.sample.doc_count, topValues: topValuesBuckets, - histogram: { buckets: [] }, + histogram: useTopHits + ? { buckets: [] } + : { + // Insert a fake bucket for a single-value histogram + buckets: [{ count: minMaxResult.aggregations!.sample.doc_count, key: minValue }], + }, }; } diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9db44bd8225ea2..bc69ab5352a4f5 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -296,140 +296,140 @@ exports[`UploadLicense should display a modal when license requires acknowledgem className="euiSpacer euiSpacer--l" />
    - - -
    + + } + confirmButtonText={ + + } + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } + > + + + -
    -
    - Confirm License Upload -
    -
    -
    +
    -
    -
    - } - > - - } - confirmButtonText={ - - } - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - > -
    -
    -
    - - + + + +
    - - } - onCancel={cancelStartBasicLicense} - onConfirm={() => startBasicLicense(licenseType, true)} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -
    - {firstLine} - -
      - {messages.map((message) => ( -
    • {message}
    • - ))} -
    -
    -
    -
    - + + } + onCancel={cancelStartBasicLicense} + onConfirm={() => startBasicLicense(licenseType, true)} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +
    + {firstLine} + +
      + {messages.map((message) => ( +
    • {message}
    • + ))} +
    +
    +
    +
    ); } render() { diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx index 4f7d1ab4365b68..36af5c3b9c7adc 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -14,7 +14,6 @@ import { EuiFlexItem, EuiCard, EuiLink, - EuiOverlayMask, EuiText, EuiModal, EuiModalFooter, @@ -78,154 +77,152 @@ export class StartTrial extends Component { } return ( - - - - - - - - - -
    - -

    - + + + + + + + +

    + +

    + - - - ), - }} + values={{ + subscriptionFeaturesLinkText: ( + + + + ), + }} + /> +

    +
      +
    • + -

      -
        -
      • - -
      • -
      • - -
      • -
      • - -
      • -
      • - -
      • -
      -

      +

    • +
    • - - - ), - }} + id="xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.alertingFeatureTitle" + defaultMessage="Alerting" + /> +
    • +
    • + -

      -

      +

    • +
    • - - - ), + jdbcStandard: 'JDBC', + odbcStandard: 'ODBC', + sqlDataBase: 'SQL', }} /> -

      - -
    -
    - - - - - - {shouldShowTelemetryOptIn(telemetry) && ( - + +

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

    +

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

    + +
    +
    +
    + + + + + {shouldShowTelemetryOptIn(telemetry) && ( + + )} + + + + + + + + + + + + + + + + + +
    ); } diff --git a/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js index 77efe30bbb71ea..4d639ec3123dff 100644 --- a/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js +++ b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js @@ -13,7 +13,6 @@ import { EuiForm, EuiSpacer, EuiConfirmModal, - EuiOverlayMask, EuiText, EuiTitle, EuiFlexGroup, @@ -62,41 +61,39 @@ export class UploadLicense extends React.PureComponent { return null; } return ( - - - } - onCancel={this.cancel} - onConfirm={() => this.send(true)} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -
    - {firstLine} - -
      - {messages.map((message) => ( -
    • {message}
    • - ))} -
    -
    -
    -
    -
    + + } + onCancel={this.cancel} + onConfirm={() => this.send(true)} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +
    + {firstLine} + +
      + {messages.map((message) => ( +
    • {message}
    • + ))} +
    +
    +
    +
    ); } errorMessage() { diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index ba5891092fe124..6dcda5d1f8c24d 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -47,4 +47,4 @@ export { OsTypeArray, } from './schemas'; -export { ENDPOINT_LIST_ID } from './constants'; +export { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from './constants'; diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.test.ts b/x-pack/plugins/lists/server/saved_objects/migrations.test.ts index 143443b9320923..f71109b9bb85dc 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.test.ts @@ -6,61 +6,102 @@ */ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import uuid from 'uuid'; -import { ENDPOINT_LIST_ID } from '../../common/constants'; +import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; +import { ExceptionListSoSchema } from '../../common/schemas/saved_objects'; import { OldExceptionListSoSchema, migrations } from './migrations'; +const DEFAULT_EXCEPTION_LIST_SO: ExceptionListSoSchema = { + comments: undefined, + created_at: '2020-06-09T20:18:20.349Z', + created_by: 'user', + description: 'description', + entries: undefined, + immutable: false, + item_id: undefined, + list_id: 'some_list', + list_type: 'list', + meta: undefined, + name: 'name', + os_types: [], + tags: [], + tie_breaker_id: uuid.v4(), + type: 'endpoint', + updated_by: 'user', + version: undefined, +}; + +const DEFAULT_OLD_EXCEPTION_LIST_SO: OldExceptionListSoSchema = { + ...DEFAULT_EXCEPTION_LIST_SO, + _tags: [], +}; + +const createOldExceptionListSoSchemaSavedObject = ( + attributes: Partial +): SavedObjectUnsanitizedDoc => ({ + attributes: { ...DEFAULT_OLD_EXCEPTION_LIST_SO, ...attributes }, + id: 'abcd', + migrationVersion: {}, + references: [], + type: 'so-type', + updated_at: '2020-06-09T20:18:20.349Z', +}); + +const createExceptionListSoSchemaSavedObject = ( + attributes: Partial +): SavedObjectUnsanitizedDoc => ({ + attributes: { ...DEFAULT_EXCEPTION_LIST_SO, ...attributes }, + id: 'abcd', + migrationVersion: {}, + references: [], + type: 'so-type', + updated_at: '2020-06-09T20:18:20.349Z', +}); + describe('7.10.0 lists migrations', () => { const migration = migrations['7.10.0']; test('properly converts .text fields to .caseless', () => { - const doc = { - attributes: { - entries: [ - { - field: 'file.path.text', - operator: 'included', - type: 'match', - value: 'C:\\Windows\\explorer.exe', - }, - { - field: 'host.os.name', - operator: 'included', - type: 'match', - value: 'my-host', - }, - { - entries: [ - { - field: 'process.command_line.text', - operator: 'included', - type: 'match', - value: '/usr/bin/bash', - }, - { - field: 'process.parent.command_line.text', - operator: 'included', - type: 'match', - value: '/usr/bin/bash', - }, - ], - field: 'nested.field', - type: 'nested', - }, - ], - list_id: ENDPOINT_LIST_ID, - }, - id: 'abcd', - migrationVersion: {}, - references: [], - type: 'so-type', - updated_at: '2020-06-09T20:18:20.349Z', - }; - expect( - migration((doc as unknown) as SavedObjectUnsanitizedDoc) - ).toEqual({ - attributes: { + const doc = createOldExceptionListSoSchemaSavedObject({ + entries: [ + { + field: 'file.path.text', + operator: 'included', + type: 'match', + value: 'C:\\Windows\\explorer.exe', + }, + { + field: 'host.os.name', + operator: 'included', + type: 'match', + value: 'my-host', + }, + { + entries: [ + { + field: 'process.command_line.text', + operator: 'included', + type: 'match', + value: '/usr/bin/bash', + }, + { + field: 'process.parent.command_line.text', + operator: 'included', + type: 'match', + value: '/usr/bin/bash', + }, + ], + field: 'nested.field', + type: 'nested', + }, + ], + list_id: ENDPOINT_LIST_ID, + }); + + expect(migration(doc)).toEqual( + createOldExceptionListSoSchemaSavedObject({ entries: [ { field: 'file.path.caseless', @@ -94,40 +135,98 @@ describe('7.10.0 lists migrations', () => { }, ], list_id: ENDPOINT_LIST_ID, - }, - id: 'abcd', - migrationVersion: {}, - references: [], - type: 'so-type', - updated_at: '2020-06-09T20:18:20.349Z', - }); + }) + ); }); test('properly copies os tags to os_types', () => { - const doc = { - attributes: { - _tags: ['1234', 'os:windows'], - comments: [], - }, - id: 'abcd', - migrationVersion: {}, - references: [], - type: 'so-type', - updated_at: '2020-06-09T20:18:20.349Z', - }; - expect( - migration((doc as unknown) as SavedObjectUnsanitizedDoc) - ).toEqual({ - attributes: { + const doc = createOldExceptionListSoSchemaSavedObject({ + _tags: ['1234', 'os:windows'], + comments: [], + }); + + expect(migration(doc)).toEqual( + createOldExceptionListSoSchemaSavedObject({ _tags: ['1234', 'os:windows'], comments: [], os_types: ['windows'], - }, - id: 'abcd', - migrationVersion: {}, - references: [], - type: 'so-type', - updated_at: '2020-06-09T20:18:20.349Z', + }) + ); + }); +}); + +describe('7.12.0 lists migrations', () => { + const migration = migrations['7.12.0']; + + test('should not convert non trusted apps lists', () => { + const doc = createExceptionListSoSchemaSavedObject({ list_id: ENDPOINT_LIST_ID, tags: [] }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_LIST_ID, + tags: [], + tie_breaker_id: expect.anything(), + }) + ); + }); + + test('converts empty tags to contain list containing "policy:all" tag', () => { + const doc = createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: [], + }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:all'], + tie_breaker_id: expect.anything(), + }) + ); + }); + + test('preserves existing non policy related tags', () => { + const doc = createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['tag1', 'tag2'], + }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['tag1', 'tag2', 'policy:all'], + tie_breaker_id: expect.anything(), + }) + ); + }); + + test('preserves existing "policy:all" tag and does not add another one', () => { + const doc = createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:all', 'tag1', 'tag2'], + }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:all', 'tag1', 'tag2'], + tie_breaker_id: expect.anything(), + }) + ); + }); + + test('preserves existing policy reference tag and does not add "policy:all" tag', () => { + const doc = createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:056d2d4645421fb92e5cd39f33d70856', 'tag1', 'tag2'], }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:056d2d4645421fb92e5cd39f33d70856', 'tag1', 'tag2'], + tie_breaker_id: expect.anything(), + }) + ); }); }); diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.ts b/x-pack/plugins/lists/server/saved_objects/migrations.ts index 43faa7a5e8fb64..2fa19a6810a8ad 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.ts @@ -40,6 +40,9 @@ const reduceOsTypes = (acc: string[], tag: string): string[] => { return [...acc]; }; +const containsPolicyTags = (tags: string[]): boolean => + tags.some((tag) => tag.startsWith('policy:')); + export type OldExceptionListSoSchema = ExceptionListSoSchema & { _tags: string[]; }; @@ -64,4 +67,25 @@ export const migrations = { }, references: doc.references || [], }), + '7.12.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + if (doc.attributes.list_id === ENDPOINT_TRUSTED_APPS_LIST_ID) { + return { + ...doc, + ...{ + attributes: { + ...doc.attributes, + tags: [ + ...(doc.attributes.tags || []), + ...(containsPolicyTags(doc.attributes.tags) ? [] : ['policy:all']), + ], + }, + }, + references: doc.references || [], + }; + } else { + return { ...doc, references: doc.references || [] }; + } + }, }; diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap index 8fc0ecacd4a3c1..31b8be8aab9ce1 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap @@ -1,41 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ConfirmDeletePipelineModal component renders as expected 1`] = ` - - - } - confirmButtonText={ - - } - defaultFocusedButton="cancel" - onCancel={[MockFunction]} - onConfirm={[MockFunction]} - title={ - + } + confirmButtonText={ + + } + defaultFocusedButton="cancel" + onCancel={[MockFunction]} + onConfirm={[MockFunction]} + title={ + - } - > -

    - You cannot recover a deleted pipeline. -

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

    + You cannot recover a deleted pipeline. +

    + `; diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js index 37ce05f42073af..d8cf85919bd425 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js @@ -7,41 +7,39 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiConfirmModal, EUI_MODAL_CANCEL_BUTTON, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { PIPELINE_EDITOR } from './constants'; export function ConfirmDeletePipelineModal({ id, cancelDeleteModal, confirmDeletePipeline }) { return ( - - - } - confirmButtonText={ - - } - defaultFocusedButton={EUI_MODAL_CANCEL_BUTTON} - onCancel={cancelDeleteModal} - onConfirm={confirmDeletePipeline} - title={ - - } - > -

    {PIPELINE_EDITOR.DELETE_PIPELINE_MODAL_MESSAGE}

    -
    -
    + + } + confirmButtonText={ + + } + defaultFocusedButton={EUI_MODAL_CANCEL_BUTTON} + onCancel={cancelDeleteModal} + onConfirm={confirmDeletePipeline} + title={ + + } + > +

    {PIPELINE_EDITOR.DELETE_PIPELINE_MODAL_MESSAGE}

    +
    ); } diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap index c58337612f2871..9eabf4120ef233 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap +++ b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap @@ -1,93 +1,89 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ConfirmDeleteModal component confirms delete for multiple pipelines 1`] = ` - - - } - confirmButtonText={ - + } + confirmButtonText={ + - } - defaultFocusedButton="cancel" - onCancel={[MockFunction]} - onConfirm={[MockFunction]} - title={ - + } + defaultFocusedButton="cancel" + onCancel={[MockFunction]} + onConfirm={[MockFunction]} + title={ + - } - > -

    - -

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

    + +

    + `; exports[`ConfirmDeleteModal component confirms delete for single pipeline 1`] = ` - - - } - confirmButtonText={ - - } - defaultFocusedButton="cancel" - onCancel={[MockFunction]} - onConfirm={[MockFunction]} - title={ - + } + confirmButtonText={ + + } + defaultFocusedButton="cancel" + onCancel={[MockFunction]} + onConfirm={[MockFunction]} + title={ + - } - > -

    - -

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

    + +

    + `; diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js index c20db3d3fc5796..5dbefd2ae58e89 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EUI_MODAL_CANCEL_BUTTON, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export function ConfirmDeleteModal({ @@ -67,23 +67,21 @@ export function ConfirmDeleteModal({ }; return ( - - - } - confirmButtonText={confirmText.button} - defaultFocusedButton={EUI_MODAL_CANCEL_BUTTON} - onCancel={cancelDeletePipelines} - onConfirm={deleteSelectedPipelines} - title={confirmText.title} - > -

    {confirmText.message}

    -
    -
    + + } + confirmButtonText={confirmText.button} + defaultFocusedButton={EUI_MODAL_CANCEL_BUTTON} + onCancel={cancelDeletePipelines} + onConfirm={deleteSelectedPipelines} + title={confirmText.title} + > +

    {confirmText.message}

    +
    ); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 25bd589cda658a..5ca370f7d54c8e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -84,6 +84,8 @@ export class MBMap extends Component { private _checker?: ResizeChecker; private _isMounted: boolean = false; private _containerRef: HTMLDivElement | null = null; + private _prevDisableInteractive?: boolean; + private _navigationControl = new mapboxgl.NavigationControl({ showCompass: false }); state: State = { prevLayerList: undefined, @@ -181,7 +183,6 @@ export class MBMap extends Component { style: mbStyle, scrollZoom: this.props.scrollZoom, preserveDrawingBuffer: getPreserveDrawingBuffer(), - interactive: !this.props.settings.disableInteractive, maxZoom: this.props.settings.maxZoom, minZoom: this.props.settings.minZoom, }; @@ -197,9 +198,6 @@ export class MBMap extends Component { const mbMap = new mapboxgl.Map(options); mbMap.dragRotate.disable(); mbMap.touchZoomRotate.disableRotation(); - if (!this.props.settings.disableInteractive) { - mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); - } const tooManyFeaturesImageSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAARLSURBVHic7ZnPbxRVAMe/7735sWO3293ZlUItJsivCxEE0oTYRgu1FqTQoFSwKTYx8SAH/wHjj4vRozGGi56sMcW2UfqTEuOhppE0KJc2GIuKQFDY7qzdtrudX88D3YTUdFuQN8+k87ltZt7uZz958/bNLAGwBWsYKltANmEA2QKyCQPIFpBNGEC2gGzCALIFZBMGkC0gmzCAbAHZhAFkC8gmDCBbQDZhANkCslnzARQZH6oDpNs0D5UDSUIInePcOpPLfdfnODNBuwQWIAWwNOABwHZN0x8npE6hNLJ4DPWRyFSf40wE5VOEQPBjcR0g3YlE4ybGmtK+/1NzJtOZA/xSYwZMs3nG962T2ez3It2AANaA/kSidYuivOQBs5WM1fUnk6f0u+GXJUqIuUtVXx00zRbRfkIDfBqL7a1WlIYbjvNtTTr99jXXHVpH6dMjK0R4cXq6c9rzxjcx9sKX8XitSEdhAToMI7VP10/97fsTh7PZrgWAN1lW72KE2vOm2b5chDTgtWQyn93x/bEEIetEOQIC14CxVOr1CkKefH929t0v8vn0vcdGEoljGxXl4C3PGz2YyXy+AHARDqtByAxoUdWKBKV70r4/vvTLA0CjZfX+5nkDGxirKzUTgkBIgNaysh3gnF627R+XO+dQJvP1ddcdrmSsbtA020pF+CAW21qrqmUiXIUEqGRsIwD0FQq/lzqv0bJ6rrvucBVjzwyb5ivLRTiiaW+8VV7eIEBVTAANiIIQd9RxZlc6t9Gyem647vn1jD07ZJonl4sQASoevqmgABzwwHnJzc69PGdZ3X+47sgGxuqHTPPE0ggeVtg5/QeEBMhxPg1Aa1DV2GrHPG9ZXy1G2D+wNALn9jyQEeHKAJgP+033Kgrdqij7AFwZtu3bqx3XWShMHtV1o1pRGo4YxiNd+fyEB2DKdX/4aG5u0hbwcylkBryTy/3scT6zW9Nq7ndso2Wdvea6Q1WUHuiPx1/WAXLBcWZXun94UMRcAoD/p+ddTFK6u8MwUvc7vsmyem+67oVqVT0wkEgcF+FYRNhW+L25uX6f84XThtHxIBudE5bVY/t++jFVrU/dvVSFICzAqG3PX/S8rihj2/61qK1AOUB7ksl2jdLUL7Z9rvgcQQRCFsEi5wqFmw26XnhCUQ63GcZmCly95Lrzpca0G0byk3j8tEnpU1c975tmyxoU5QcE8EAEAM5WVOzfoarHAeC2749dcpzxMwsLv07Ztg0AOzVNf03Ttu/S9T2PMlbjc25fdpyutmx2TLRbIAEA4M1otKo1EjmaoHQn4ZwBgA/kAVAK6MXXdzxv/ONcrq/HcbJBeAUWoEizqsaORaPbKglZrxMSZZyrM76f/ovzWx/m85PFWREUgQf4v7Hm/xcIA8gWkE0YQLaAbMIAsgVkEwaQLSCbMIBsAdmEAWQLyCYMIFtANmEA2QKyCQPIFpDNmg/wD3OFdEybUvJjAAAAAElFTkSuQmCC'; @@ -357,6 +355,28 @@ export class MBMap extends Component { return; } + if ( + this._prevDisableInteractive === undefined || + this._prevDisableInteractive !== this.props.settings.disableInteractive + ) { + this._prevDisableInteractive = this.props.settings.disableInteractive; + if (this.props.settings.disableInteractive) { + this.state.mbMap.boxZoom.disable(); + this.state.mbMap.doubleClickZoom.disable(); + this.state.mbMap.dragPan.disable(); + try { + this.state.mbMap.removeControl(this._navigationControl); + } catch (error) { + // ignore removeControl errors + } + } else { + this.state.mbMap.boxZoom.enable(); + this.state.mbMap.doubleClickZoom.enable(); + this.state.mbMap.dragPan.enable(); + this.state.mbMap.addControl(this._navigationControl, 'top-left'); + } + } + let zoomRangeChanged = false; if (this.props.settings.minZoom !== this.state.mbMap.getMinZoom()) { this.state.mbMap.setMinZoom(this.props.settings.minZoom); diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index 89eef907b22593..9e5a6080c830d8 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -8,7 +8,7 @@ import React from 'react'; import classNames from 'classnames'; -import { EuiIcon, EuiOverlayMask, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; +import { EuiIcon, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; import { i18n } from '@kbn/i18n'; @@ -100,20 +100,18 @@ export class TOCEntry extends React.Component { }; return ( - - -

    There are unsaved changes to your layer.

    -

    Are you sure you want to proceed?

    -
    -
    + +

    There are unsaved changes to your layer.

    +

    Are you sure you want to proceed?

    +
    ); } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index b769ac489f565e..f42a055b24d0a2 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -29,6 +29,7 @@ import { } from '../../../../../src/plugins/data/public'; import { replaceLayerList, + setMapSettings, setQuery, setRefreshConfig, disableScrollZoom, @@ -60,6 +61,7 @@ import { getCoreI18n, getHttp, getChartsPaletteServiceGetColor, + getSearchService, } from '../kibana_services'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapContainer } from '../connected_components/map_container'; @@ -77,6 +79,14 @@ import { } from './types'; export { MapEmbeddableInput, MapEmbeddableOutput }; +function getIsRestore(searchSessionId?: string) { + if (!searchSessionId) { + return false; + } + const searchSessionOptions = getSearchService().session.getSearchOptions(searchSessionId); + return searchSessionOptions ? searchSessionOptions.isRestore : false; +} + export class MapEmbeddable extends Embeddable implements ReferenceOrValueEmbeddable { @@ -85,6 +95,7 @@ export class MapEmbeddable private _savedMap: SavedMap; private _renderTooltipContent?: RenderToolTipContent; private _subscription: Subscription; + private _prevIsRestore: boolean = false; private _prevTimeRange?: TimeRange; private _prevQuery?: Query; private _prevRefreshConfig?: RefreshInterval; @@ -234,6 +245,17 @@ export class MapEmbeddable if (this.input.syncColors !== this._prevSyncColors) { this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors); } + + const isRestore = getIsRestore(this.input.searchSessionId); + if (isRestore !== this._prevIsRestore) { + this._prevIsRestore = isRestore; + this._savedMap.getStore().dispatch( + setMapSettings({ + disableInteractive: isRestore, + hideToolbarOverlay: isRestore, + }) + ); + } } _dispatchSetQuery({ diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index c2f5fc02c5df20..89cd80f4daab50 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -26,10 +26,12 @@ jest.mock('../kibana_services', () => ({ })); import { DEFAULT_MAP_STORE_STATE } from '../reducers/store'; -import { getTimeFilters } from './map_selectors'; +import { areLayersLoaded, getTimeFilters } from './map_selectors'; +import { LayerDescriptor } from '../../common/descriptor_types'; +import { ILayer } from '../classes/layers/layer'; describe('getTimeFilters', () => { - it('should return timeFilters when contained in state', () => { + test('should return timeFilters when contained in state', () => { const state = { ...DEFAULT_MAP_STORE_STATE, map: { @@ -46,7 +48,7 @@ describe('getTimeFilters', () => { expect(getTimeFilters(state)).toEqual({ to: '2001-01-01', from: '2001-12-31' }); }); - it('should return kibana time filters when not contained in state', () => { + test('should return kibana time filters when not contained in state', () => { const state = { ...DEFAULT_MAP_STORE_STATE, map: { @@ -60,3 +62,74 @@ describe('getTimeFilters', () => { expect(getTimeFilters(state)).toEqual({ to: 'now', from: 'now-15m' }); }); }); + +describe('areLayersLoaded', () => { + function createLayerMock({ + hasErrors = false, + isDataLoaded = false, + isVisible = true, + showAtZoomLevel = true, + }: { + hasErrors?: boolean; + isDataLoaded?: boolean; + isVisible?: boolean; + showAtZoomLevel?: boolean; + }) { + return ({ + hasErrors: () => { + return hasErrors; + }, + isDataLoaded: () => { + return isDataLoaded; + }, + isVisible: () => { + return isVisible; + }, + showAtZoomLevel: () => { + return showAtZoomLevel; + }, + } as unknown) as ILayer; + } + + test('layers waiting for map to load should not be counted loaded', () => { + const layerList: ILayer[] = []; + const waitingForMapReadyLayerList: LayerDescriptor[] = [({} as unknown) as LayerDescriptor]; + const zoom = 4; + expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(false); + }); + + test('layer should not be counted as loaded if it has not loaded', () => { + const layerList = [createLayerMock({ isDataLoaded: false })]; + const waitingForMapReadyLayerList: LayerDescriptor[] = []; + const zoom = 4; + expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(false); + }); + + test('layer should be counted as loaded if its not visible', () => { + const layerList = [createLayerMock({ isVisible: false, isDataLoaded: false })]; + const waitingForMapReadyLayerList: LayerDescriptor[] = []; + const zoom = 4; + expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(true); + }); + + test('layer should be counted as loaded if its not shown at zoom level', () => { + const layerList = [createLayerMock({ showAtZoomLevel: false, isDataLoaded: false })]; + const waitingForMapReadyLayerList: LayerDescriptor[] = []; + const zoom = 4; + expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(true); + }); + + test('layer should be counted as loaded if it has a loading error', () => { + const layerList = [createLayerMock({ hasErrors: true, isDataLoaded: false })]; + const waitingForMapReadyLayerList: LayerDescriptor[] = []; + const zoom = 4; + expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(true); + }); + + test('layer should be counted as loaded if its loaded', () => { + const layerList = [createLayerMock({ isDataLoaded: true })]; + const waitingForMapReadyLayerList: LayerDescriptor[] = []; + const zoom = 4; + expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(true); + }); +}); diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index f53f39ad2fc0cc..b16ac704c3715b 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -428,7 +428,12 @@ export const areLayersLoaded = createSelector( for (let i = 0; i < layerList.length; i++) { const layer = layerList[i]; - if (layer.isVisible() && layer.showAtZoomLevel(zoom) && !layer.isDataLoaded()) { + if ( + layer.isVisible() && + layer.showAtZoomLevel(zoom) && + !layer.hasErrors() && + !layer.isDataLoaded() + ) { return false; } } diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index bb6b331c10fc18..09f5c37ac9aeaf 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -49,6 +49,7 @@ export type MlSummaryJobs = MlSummaryJob[]; export interface MlJobWithTimeRange extends CombinedJobWithStats { id: string; + isRunning?: boolean; isNotSingleMetricViewerJobMessage?: string; timeRange: { from: number; diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index cacc5acb9768f4..95d82932a1212e 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -34,9 +34,10 @@ interface Regression { } interface Classification { + class_assignment_objective?: string; dependent_variable: string; training_percent?: number; - num_top_classes?: string; + num_top_classes?: number; num_top_feature_importance_values?: number; prediction_field_name?: string; } diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts index 2e45c3cd4d8c44..964ce8c3257838 100644 --- a/x-pack/plugins/ml/common/types/feature_importance.ts +++ b/x-pack/plugins/ml/common/types/feature_importance.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { isPopulatedObject } from '../util/object_utils'; + export type FeatureImportanceClassName = string | number | boolean; export interface ClassFeatureImportance { @@ -87,7 +89,7 @@ export function isClassificationFeatureImportanceBaseline( baselineData: any ): baselineData is ClassificationFeatureImportanceBaseline { return ( - typeof baselineData === 'object' && + isPopulatedObject(baselineData) && baselineData.hasOwnProperty('classes') && Array.isArray(baselineData.classes) ); @@ -96,5 +98,5 @@ export function isClassificationFeatureImportanceBaseline( export function isRegressionFeatureImportanceBaseline( baselineData: any ): baselineData is RegressionFeatureImportanceBaseline { - return typeof baselineData === 'object' && baselineData.hasOwnProperty('baseline'); + return isPopulatedObject(baselineData) && baselineData.hasOwnProperty('baseline'); } diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index ae157cef5735fc..581ce861e8331c 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ES_FIELD_TYPES, RuntimeField } from '../../../../../src/plugins/data/common'; +import { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, @@ -106,4 +106,18 @@ export interface AggCardinality { } export type RollupFields = Record]>; + +// Replace this with import once #88995 is merged +const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export interface RuntimeField { + type: RuntimeType; + script: + | string + | { + source: string; + }; +} + export type RuntimeMappings = Record; diff --git a/x-pack/plugins/ml/common/util/datafeed_utils.ts b/x-pack/plugins/ml/common/util/datafeed_utils.ts index fa1a940ba5492c..c0579ce947992a 100644 --- a/x-pack/plugins/ml/common/util/datafeed_utils.ts +++ b/x-pack/plugins/ml/common/util/datafeed_utils.ts @@ -20,7 +20,7 @@ export const getDatafeedAggregations = ( }; export const getAggregationBucketsName = (aggregations: any): string | undefined => { - if (typeof aggregations === 'object') { + if (aggregations !== null && typeof aggregations === 'object') { const keys = Object.keys(aggregations); return keys.length > 0 ? keys[0] : undefined; } diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 711103b499ec90..ab56726e160f7c 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -28,6 +28,7 @@ import { getDatafeedAggregations, } from './datafeed_utils'; import { findAggField } from './validation_utils'; +import { isPopulatedObject } from './object_utils'; export interface ValidationResults { valid: boolean; @@ -51,17 +52,9 @@ export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: numb } export function hasRuntimeMappings(job: CombinedJob): boolean { - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + const hasDatafeed = isPopulatedObject(job.datafeed_config); if (hasDatafeed) { - const runtimeMappings = - typeof job.datafeed_config.runtime_mappings === 'object' - ? Object.keys(job.datafeed_config.runtime_mappings) - : undefined; - - if (Array.isArray(runtimeMappings) && runtimeMappings.length > 0) { - return true; - } + return isPopulatedObject(job.datafeed_config.runtime_mappings); } return false; } @@ -114,7 +107,11 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex // If the datafeed uses script fields, we can only plot the time series if // model plot is enabled. Without model plot it will be very difficult or impossible // to invert to a reverse search of the underlying metric data. - if (isSourceDataChartable === true && typeof job.datafeed_config?.script_fields === 'object') { + if ( + isSourceDataChartable === true && + job.datafeed_config?.script_fields !== null && + typeof job.datafeed_config?.script_fields === 'object' + ) { // Perform extra check to see if the detector is using a scripted field. const scriptFields = Object.keys(job.datafeed_config.script_fields); isSourceDataChartable = @@ -123,8 +120,7 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex scriptFields.indexOf(dtr.over_field_name!) === -1; } - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + const hasDatafeed = isPopulatedObject(job.datafeed_config); if (hasDatafeed) { // We cannot plot the source data for some specific aggregation configurations const aggs = getDatafeedAggregations(job.datafeed_config); diff --git a/x-pack/plugins/ml/common/util/object_utils.ts b/x-pack/plugins/ml/common/util/object_utils.ts new file mode 100644 index 00000000000000..4bbd0c1c2810fe --- /dev/null +++ b/x-pack/plugins/ml/common/util/object_utils.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isPopulatedObject = >(arg: any): arg is T => { + return typeof arg === 'object' && arg !== null && Object.keys(arg).length > 0; +}; diff --git a/x-pack/plugins/ml/common/util/validation_utils.ts b/x-pack/plugins/ml/common/util/validation_utils.ts index 7f0208e726ab0b..66084f83ea87d1 100644 --- a/x-pack/plugins/ml/common/util/validation_utils.ts +++ b/x-pack/plugins/ml/common/util/validation_utils.ts @@ -45,7 +45,7 @@ export function findAggField( value = returnParent === true ? aggs : aggs[k]; return true; } - if (aggs.hasOwnProperty(k) && typeof aggs[k] === 'object') { + if (aggs.hasOwnProperty(k) && aggs[k] !== null && typeof aggs[k] === 'object') { value = findAggField(aggs[k], fieldName, returnParent); return value !== undefined; } diff --git a/x-pack/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx b/x-pack/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx index 8469d42c16c519..9999fad89d0e1c 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import React, { Fragment } from 'react'; -import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -26,33 +26,31 @@ export const DeleteAnnotationModal: React.FC = ({ return ( {isVisible === true && ( - - - } - onCancel={cancelAction} - onConfirm={deleteAction} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - className="eui-textBreakWord" - /> - + + } + onCancel={cancelAction} + onConfirm={deleteAction} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + className="eui-textBreakWord" + /> )} ); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index f169c56205e08e..069c13df2470f7 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -27,7 +27,11 @@ import { import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; import { extractErrorMessage } from '../../../../common/util/errors'; -import { FeatureImportance, TopClasses } from '../../../../common/types/feature_importance'; +import { + FeatureImportance, + FeatureImportanceClassName, + TopClasses, +} from '../../../../common/types/feature_importance'; import { BASIC_NUMERICAL_TYPES, @@ -44,6 +48,9 @@ import { getNestedProperty } from '../../util/object_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; +import type { RuntimeField } from '../../../../../../../src/plugins/data/common/index_patterns'; +import { RuntimeMappings } from '../../../../common/types/fields'; +import { isPopulatedObject } from '../../../../common/util/object_utils'; export const INIT_MAX_COLUMNS = 10; @@ -82,6 +89,37 @@ export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): str return indexPatternFields; }; +/** + * Return a map of runtime_mappings for each of the index pattern field provided + * to provide in ES search queries + * @param indexPatternFields + * @param indexPattern + * @param clonedRuntimeMappings + */ +export const getRuntimeFieldsMapping = ( + indexPatternFields: string[] | undefined, + indexPattern: IndexPattern | undefined, + clonedRuntimeMappings?: RuntimeMappings +) => { + if (!Array.isArray(indexPatternFields) || indexPattern === undefined) return {}; + const ipRuntimeMappings = indexPattern.getComputedFields().runtimeFields; + let combinedRuntimeMappings: RuntimeMappings = {}; + + if (isPopulatedObject(ipRuntimeMappings)) { + indexPatternFields.forEach((ipField) => { + if (ipRuntimeMappings.hasOwnProperty(ipField)) { + combinedRuntimeMappings[ipField] = ipRuntimeMappings[ipField]; + } + }); + } + if (isPopulatedObject(clonedRuntimeMappings)) { + combinedRuntimeMappings = { ...combinedRuntimeMappings, ...clonedRuntimeMappings }; + } + return Object.keys(combinedRuntimeMappings).length > 0 + ? { runtime_mappings: combinedRuntimeMappings } + : {}; +}; + export interface FieldTypes { [key: string]: ES_FIELD_TYPES; } @@ -131,6 +169,45 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results }; export const NON_AGGREGATABLE = 'non-aggregatable'; + +export const getDataGridSchemaFromESFieldType = ( + fieldType: ES_FIELD_TYPES | undefined | RuntimeField['type'] +): string | undefined => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (fieldType) { + case ES_FIELD_TYPES.GEO_POINT: + case ES_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case ES_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case ES_FIELD_TYPES.DATE: + case ES_FIELD_TYPES.DATE_NANOS: + schema = 'datetime'; + break; + case ES_FIELD_TYPES.BYTE: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.SHORT: + schema = 'numeric'; + break; + // keep schema undefined for text based columns + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.TEXT: + break; + } + + return schema; +}; + export const getDataGridSchemaFromKibanaFieldType = ( field: IFieldType | undefined ): string | undefined => { @@ -168,8 +245,9 @@ const getClassName = (className: string, isClassTypeBoolean: boolean) => { return className; }; + /** - * Helper to transform feature importance flattened fields with arrays back to object structure + * Helper to transform feature importance fields with arrays back to primitive value * * @param row - EUI data grid data row * @param mlResultsField - Data frame analytics results field @@ -180,69 +258,44 @@ export const getFeatureImportance = ( mlResultsField: string, isClassTypeBoolean = false ): FeatureImportance[] => { - const featureNames: string[] | undefined = - row[`${mlResultsField}.feature_importance.feature_name`]; - const classNames: string[] | undefined = - row[`${mlResultsField}.feature_importance.classes.class_name`]; - const classImportance: number[] | undefined = - row[`${mlResultsField}.feature_importance.classes.importance`]; - - if (featureNames === undefined) { - return []; - } - - // return object structure for classification job - if (classNames !== undefined && classImportance !== undefined) { - const overallClassNames = classNames?.slice(0, classNames.length / featureNames.length); - - return featureNames.map((fName, index) => { - const offset = overallClassNames.length * index; - const featureClassImportance = classImportance.slice( - offset, - offset + overallClassNames.length - ); - return { - feature_name: fName, - classes: overallClassNames.map((fClassName, fIndex) => { + const featureImportance: Array<{ + feature_name: string[]; + classes?: Array<{ class_name: FeatureImportanceClassName[]; importance: number[] }>; + importance?: number | number[]; + }> = row[`${mlResultsField}.feature_importance`]; + if (featureImportance === undefined) return []; + + return featureImportance.map((fi) => ({ + feature_name: Array.isArray(fi.feature_name) ? fi.feature_name[0] : fi.feature_name, + classes: Array.isArray(fi.classes) + ? fi.classes.map((c) => { + const processedClass = getProcessedFields(c); return { - class_name: getClassName(fClassName, isClassTypeBoolean), - importance: featureClassImportance[fIndex], + importance: processedClass.importance, + class_name: getClassName(processedClass.class_name, isClassTypeBoolean), }; - }), - }; - }); - } - - // return object structure for regression job - const importance: number[] = row[`${mlResultsField}.feature_importance.importance`]; - return featureNames.map((fName, index) => ({ - feature_name: fName, - importance: importance[index], + }) + : fi.classes, + importance: Array.isArray(fi.importance) ? fi.importance[0] : fi.importance, })); }; /** - * Helper to transforms top classes flattened fields with arrays back to object structure + * Helper to transforms top classes fields with arrays back to original primitive value * * @param row - EUI data grid data row * @param mlResultsField - Data frame analytics results field * @returns nested object structure of feature importance values */ export const getTopClasses = (row: Record, mlResultsField: string): TopClasses => { - const classNames: string[] | undefined = row[`${mlResultsField}.top_classes.class_name`]; - const classProbabilities: number[] | undefined = - row[`${mlResultsField}.top_classes.class_probability`]; - const classScores: number[] | undefined = row[`${mlResultsField}.top_classes.class_score`]; - - if (classNames === undefined || classProbabilities === undefined || classScores === undefined) { - return []; - } - - return classNames.map((className, index) => ({ - class_name: className, - class_probability: classProbabilities[index], - class_score: classScores[index], - })); + const topClasses: Array<{ + class_name: FeatureImportanceClassName[]; + class_probability: number[]; + class_score: number[]; + }> = row[`${mlResultsField}.top_classes`]; + + if (topClasses === undefined) return []; + return topClasses.map((tc) => getProcessedFields(tc)) as TopClasses; }; export const useRenderCellValue = ( diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index da34e0f1bc9fb9..5dad9801eb644b 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -35,7 +35,7 @@ import { getTopClasses, } from './common'; import { UseIndexDataReturnType } from './types'; -import { DecisionPathPopover } from './feature_importance/decision_path_popover'; +import { DecisionPathPopover } from '../../data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_popover'; import { FeatureImportanceBaseline, FeatureImportance, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index ccd2f3f56e45df..79a8d65f9905a2 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -7,8 +7,10 @@ export { getDataGridSchemasFromFieldTypes, + getDataGridSchemaFromESFieldType, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, + getRuntimeFieldsMapping, multiColumnSortFactory, showDataGridColumnChartErrorMessageToast, useRenderCellValue, diff --git a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx index eda0509d417ca7..972ed06ba13859 100644 --- a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx +++ b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx @@ -13,7 +13,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiModal, - EuiOverlayMask, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody, @@ -230,69 +229,67 @@ export const DeleteJobCheckModal: FC = ({ }; return ( - - - {isLoading === true && ( - <> - - - - - - - - - )} - {isLoading === false && ( - <> - - - - - + + {isLoading === true && ( + <> + + + + + + + + + )} + {isLoading === false && ( + <> + + + + + - {modalContent} + {modalContent} - - - - {!hasUntagged && + + + + {!hasUntagged && + jobCheckRespSummary?.canTakeAnyAction && + jobCheckRespSummary?.canRemoveFromSpace && + jobCheckRespSummary?.canDelete && ( + + {shouldUnTagLabel} + + )} + + + - {shouldUnTagLabel} - - )} - - - - {buttonContent} - - - - - - )} - - + !jobCheckRespSummary?.canDelete + ? onUntagClick + : onClick + } + fill + > + {buttonContent} + + + + + + )} + ); }; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx index 1d2bda90516b99..8fc4a0d636bce7 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { COMBINED_JOB_STATE } from '../model_snapshots_table'; @@ -25,56 +25,51 @@ export const CloseJobConfirm: FC = ({ forceCloseJob, }) => { return ( - - +

    + {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING && ( + )} - confirmButtonText={ - combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING - ? i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.stopAndClose.button', { - defaultMessage: 'Force stop and close', - }) - : i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.close.button', { - defaultMessage: 'Force close', - }) - } - defaultFocusedButton="confirm" - > -

    - {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING && ( - - )} - {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_STOPPED && ( - - )} -
    + {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_STOPPED && ( -

    -
    -
    + )} +
    + +

    + ); }; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx index 833e70fc86f4c4..20c98255930b4d 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -22,7 +22,6 @@ import { EuiFormRow, EuiSwitch, EuiConfirmModal, - EuiOverlayMask, EuiCallOut, } from '@elastic/eui'; @@ -190,23 +189,21 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout {deleteModalVisible && ( - - - + )} ); 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 1929cddaca6b56..6dd4e6c14589b2 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 @@ -22,7 +22,6 @@ import { EuiFormRow, EuiSwitch, EuiConfirmModal, - EuiOverlayMask, EuiCallOut, EuiHorizontalRule, EuiSuperSelect, @@ -368,34 +367,32 @@ export const RevertModelSnapshotFlyout: FC = ({ {revertModalVisible && ( - - - - - + + + )} ); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap index a132e6682ee250..3a11531f6c4bc1 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap @@ -42,34 +42,32 @@ exports[`DeleteRuleModal renders modal after clicking delete rule link 1`] = ` values={Object {}} /> - - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - /> - + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } + /> `; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js index 809bb780c33239..6caa6592e96c1e 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js @@ -12,7 +12,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { EuiConfirmModal, EuiLink, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EuiLink, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export class DeleteRuleModal extends Component { @@ -43,32 +43,30 @@ export class DeleteRuleModal extends Component { if (this.state.isModalVisible) { modal = ( - - - } - onCancel={this.closeModal} - onConfirm={this.deleteRule} - buttonColor="danger" - cancelButtonText={ - - } - confirmButtonText={ - - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - /> - + + } + onCancel={this.closeModal} + onConfirm={this.deleteRule} + buttonColor="danger" + cancelButtonText={ + + } + confirmButtonText={ + + } + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + /> ); } diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index a93264c852dd15..2b7c89db15e2e1 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiText, EuiFlexGroup, @@ -161,24 +160,19 @@ const LoadingSpinner = () => ( ); const Modal = ({ close, title, children }) => ( - - - - {title} - - - {children} - - - - - - - - + + + {title} + + + {children} + + + + + + + ); Modal.propType = { close: PropTypes.func.isRequired, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx index f92d391ecd4a95..ef88c363e3e279 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx @@ -69,9 +69,16 @@ export const ConfigurationStepDetails: FC = ({ setCurrentStep, state }) = }), description: includes.length > MAX_INCLUDES_LENGTH - ? `${includes.slice(0, MAX_INCLUDES_LENGTH).join(', ')} ... (and ${ - includes.length - MAX_INCLUDES_LENGTH - } more)` + ? i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.includedFieldsAndMoreDescription', + { + defaultMessage: '{includedFields} ... (and {extraCount} more)', + values: { + extraCount: includes.length - MAX_INCLUDES_LENGTH, + includedFields: includes.slice(0, MAX_INCLUDES_LENGTH).join(', '), + }, + } + ) : includes.join(', '), }, ]; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 6ad874d3abd6c9..0432094c30c500 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -8,6 +8,7 @@ import React, { FC, Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { EuiBadge, + EuiCallOut, EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, @@ -19,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { debounce } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; @@ -62,6 +64,8 @@ const requiredFieldsErrorText = i18n.translate( } ); +const maxRuntimeFieldsDisplayCount = 5; + export const ConfigurationStepForm: FC = ({ actions, state, @@ -314,6 +318,15 @@ export const ConfigurationStepForm: FC = ({ }; }, [jobType, dependentVariable, trainingPercent, JSON.stringify(includes), jobConfigQueryString]); + const unsupportedRuntimeFields = useMemo( + () => + currentIndexPattern.fields + .getAll() + .filter((f) => f.runtimeField) + .map((f) => `'${f.displayName}'`), + [currentIndexPattern.fields] + ); + return ( @@ -445,6 +458,36 @@ export const ConfigurationStepForm: FC = ({ > + {Array.isArray(unsupportedRuntimeFields) && unsupportedRuntimeFields.length > 0 && ( + <> + + 0 ? ( + + ) : ( + '' + ), + unsupportedRuntimeFields: unsupportedRuntimeFields + .slice(0, maxRuntimeFieldsDisplayCount) + .join(', '), + }} + /> + + + + )} + { - const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); + const indexPatternFields = useMemo(() => getFieldsFromKibanaIndexPattern(indexPattern), [ + indexPattern, + ]); // EuiDataGrid State const columns: EuiDataGridColumn[] = [ @@ -75,7 +78,6 @@ export const useIndexData = ( s[column.id] = { order: column.direction }; return s; }, {} as EsSorting); - const esSearchRequest = { index: indexPattern.title, body: { @@ -86,6 +88,7 @@ export const useIndexData = ( fields: ['*'], _source: false, ...(Object.keys(sort).length > 0 ? { sort } : {}), + ...getRuntimeFieldsMapping(indexPatternFields, indexPattern), }, }; @@ -105,7 +108,7 @@ export const useIndexData = ( useEffect(() => { getIndexData(); // custom comparison - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + }, [indexPattern.title, indexPatternFields, JSON.stringify([query, pagination, sortingColumns])]); const dataLoader = useMemo(() => new DataLoader(indexPattern, toastNotifications), [ indexPattern, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section.scss index c1c80e8dbd2c42..3a548b40d3a912 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section.scss @@ -5,3 +5,9 @@ .mlExpandableSection-contentPadding { padding: $euiSizeS; } + +// Make sure the charts tooltip in popover +// have higher zIndex than Eui popover cells +[id^='echTooltipPortal'] { + z-index: $euiZLevel9 !important; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx similarity index 95% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx index a711d672975aae..5e508df7c6ae5e 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx @@ -25,12 +25,12 @@ import { EuiIcon } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import euiVars from '@elastic/eui/dist/eui_theme_light.json'; -import { DecisionPathPlotData } from './use_classification_path_data'; -import { formatSingleValue } from '../../../formatters/format_value'; +import type { DecisionPathPlotData } from './use_classification_path_data'; +import { formatSingleValue } from '../../../../../formatters/format_value'; import { FeatureImportanceBaseline, isRegressionFeatureImportanceBaseline, -} from '../../../../../common/types/feature_importance'; +} from '../../../../../../../common/types/feature_importance'; const { euiColorFullShade, euiColorMediumShade } = euiVars; const axisColor = euiColorMediumShade; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_classification.tsx similarity index 97% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_classification.tsx index 48a0c0871f6865..d10755b32d7a75 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_classification.tsx @@ -14,11 +14,11 @@ import { useDecisionPathData, getStringBasedClassName, } from './use_classification_path_data'; -import { +import type { FeatureImportance, FeatureImportanceBaseline, TopClasses, -} from '../../../../../common/types/feature_importance'; +} from '../../../../../../../common/types/feature_importance'; import { DecisionPathChart } from './decision_path_chart'; import { MissingDecisionPathCallout } from './missing_decision_path_callout'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_json_viewer.tsx similarity index 86% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_json_viewer.tsx index 93b7bd6bd012fb..1110ef8171b96a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_json_viewer.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiCodeBlock } from '@elastic/eui'; -import { FeatureImportance } from '../../../../../common/types/feature_importance'; +import type { FeatureImportance } from '../../../../../../../common/types/feature_importance'; interface DecisionPathJSONViewerProps { featureImportance: FeatureImportance[]; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_popover.tsx similarity index 94% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_popover.tsx index 3aed0f56d5a76d..e1ad6a68639081 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_popover.tsx @@ -16,11 +16,11 @@ import { isClassificationFeatureImportanceBaseline, isRegressionFeatureImportanceBaseline, TopClasses, -} from '../../../../../common/types/feature_importance'; -import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common'; +} from '../../../../../../../common/types/feature_importance'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common'; import { ClassificationDecisionPath } from './decision_path_classification'; -import { useMlKibana } from '../../../contexts/kibana'; -import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; +import { useMlKibana } from '../../../../../contexts/kibana'; +import type { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; import { getStringBasedClassName } from './use_classification_path_data'; interface DecisionPathPopoverProps { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_regression.tsx similarity index 97% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_regression.tsx index ccb7870fd79dc2..bb9cdd861788c3 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_regression.tsx @@ -9,11 +9,11 @@ import React, { FC, useMemo } from 'react'; import { EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import d3 from 'd3'; -import { +import type { FeatureImportance, FeatureImportanceBaseline, TopClasses, -} from '../../../../../common/types/feature_importance'; +} from '../../../../../../../common/types/feature_importance'; import { useDecisionPathData, isDecisionPathData } from './use_classification_path_data'; import { DecisionPathChart } from './decision_path_chart'; import { MissingDecisionPathCallout } from './missing_decision_path_callout'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/missing_decision_path_callout.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/missing_decision_path_callout.tsx diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.test.tsx similarity index 98% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.test.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.test.tsx index 18bc02ae638473..70c62294cae009 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.test.tsx @@ -9,7 +9,7 @@ import { buildClassificationDecisionPathData, buildRegressionDecisionPathData, } from './use_classification_path_data'; -import { FeatureImportance } from '../../../../../common/types/feature_importance'; +import type { FeatureImportance } from '../../../../../../../common/types/feature_importance'; describe('buildClassificationDecisionPathData()', () => { test('should return correct prediction probability for binary classification', () => { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx similarity index 98% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx index ccee43a8c971d1..5d61d8b3ef0c49 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx @@ -14,8 +14,8 @@ import { isClassificationFeatureImportanceBaseline, isRegressionFeatureImportanceBaseline, TopClasses, -} from '../../../../../common/types/feature_importance'; -import { ExtendedFeatureImportance } from './decision_path_popover'; +} from '../../../../../../../common/types/feature_importance'; +import type { ExtendedFeatureImportance } from './decision_path_popover'; export type DecisionPathPlotData = Array<[string, number, number]>; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx index bc99330a444ae5..e2e1ec852d1a95 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx @@ -9,7 +9,6 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiConfirmModal, - EuiOverlayMask, EuiSwitch, EuiFlexGroup, EuiFlexItem, @@ -37,67 +36,62 @@ export const DeleteActionModal: FC = ({ const indexName = item.config.dest.index; return ( - - - - - {userCanDeleteIndex && ( - - )} - - - {userCanDeleteIndex && indexPatternExists && ( - - )} - - - - + + + + {userCanDeleteIndex && ( + + )} + + + {userCanDeleteIndex && indexPatternExists && ( + + )} + + + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx index 2a19068ca6f4e1..d63e60e43e9094 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { StartAction } from './use_start_action'; @@ -15,37 +15,35 @@ export const StartActionModal: FC = ({ closeModal, item, startAndCl return ( <> {item !== undefined && ( - - +

    + {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { + defaultMessage: + 'A data frame analytics job increases search and indexing load in your cluster. If excessive load occurs, stop the job.', })} - onCancel={closeModal} - onConfirm={startAndCloseModal} - cancelButtonText={i18n.translate( - 'xpack.ml.dataframe.analyticsList.startModalCancelButton', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.ml.dataframe.analyticsList.startModalStartButton', - { - defaultMessage: 'Start', - } - )} - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - buttonColor="primary" - > -

    - {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { - defaultMessage: - 'A data frame analytics job increases search and indexing load in your cluster. If excessive load occurs, stop the job.', - })} -

    -
    -
    +

    +
    )} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx index a10c0c59abd973..8ee7350245be43 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { StopAction } from './use_stop_action'; @@ -16,37 +16,35 @@ export const StopActionModal: FC = ({ closeModal, item, forceStopAnd return ( <> {item !== undefined && ( - - -

    - -

    -
    -
    + +

    + +

    +
    )} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx index 7ff77f21a8623e..d93baee97c5330 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx @@ -8,7 +8,6 @@ import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -31,57 +30,55 @@ export const DeleteModelsModal: FC = ({ models, onClose .map((model) => model.model_id); return ( - - - - + + + + + + + + + {modelsWithPipelines.length > 0 && ( + - - - - - {modelsWithPipelines.length > 0 && ( - - - - )} - + + )} + - - - - + + + + - - - - - - + + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index c9f78e9b0dab1a..40f97690d7790b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -9,13 +9,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; @@ -41,66 +35,62 @@ export const SourceSelection: FC = ({ onClose }) => { }; return ( - <> - - - - - {' '} - /{' '} - - - - - + + + {' '} + /{' '} + + + + + 'search', + name: i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search', { - defaultMessage: 'No matching indices or saved searches found.', + defaultMessage: 'Saved search', } - )} - savedObjectMetaData={[ + ), + }, + { + type: 'index-pattern', + getIconForSavedObject: () => 'indexPatternApp', + name: i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern', { - type: 'search', - getIconForSavedObject: () => 'search', - name: i18n.translate( - 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search', - { - defaultMessage: 'Saved search', - } - ), - }, - { - type: 'index-pattern', - getIconForSavedObject: () => 'indexPatternApp', - name: i18n.translate( - 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern', - { - defaultMessage: 'Index pattern', - } - ), - }, - ]} - fixedPageSize={fixedPageSize} - uiSettings={uiSettings} - savedObjects={savedObjects} - /> - - - - + defaultMessage: 'Index pattern', + } + ), + }, + ]} + fixedPageSize={fixedPageSize} + uiSettings={uiSettings} + savedObjects={savedObjects} + /> + + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 131da93a2328a0..40e13ea0e6867e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -16,6 +16,7 @@ import { DataFrameAnalyticsId, DataFrameAnalysisConfigType, } from '../../../../../../../common/types/data_frame_analytics'; +import { isClassificationAnalysis } from '../../../../../../../common/util/analytics_utils'; import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; export enum DEFAULT_MODEL_MEMORY_LIMIT { regression = '100mb', @@ -50,6 +51,7 @@ export interface State { alpha: undefined | number; computeFeatureInfluence: string; createIndexPattern: boolean; + classAssignmentObjective: undefined | string; dependentVariable: DependentVariable; description: string; destinationIndex: EsIndexName; @@ -126,6 +128,7 @@ export const getInitialState = (): State => ({ alpha: undefined, computeFeatureInfluence: 'true', createIndexPattern: true, + classAssignmentObjective: undefined, dependentVariable: '', description: '', destinationIndex: '', @@ -278,13 +281,14 @@ export const getJobConfigFromFormState = ( }; } - if ( - formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && - jobConfig?.analysis?.classification !== undefined && - formState.numTopClasses !== undefined - ) { - // @ts-ignore - jobConfig.analysis.classification.num_top_classes = formState.numTopClasses; + if (jobConfig?.analysis !== undefined && isClassificationAnalysis(jobConfig?.analysis)) { + if (formState.numTopClasses !== undefined) { + jobConfig.analysis.classification.num_top_classes = formState.numTopClasses; + } + if (formState.classAssignmentObjective !== undefined) { + jobConfig.analysis.classification.class_assignment_objective = + formState.classAssignmentObjective; + } } if (formState.jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) { diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx index 3401c72a3b8549..2330eafd87825e 100644 --- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx +++ b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx @@ -14,7 +14,6 @@ import { EuiModal, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiButtonEmpty, EuiButton, @@ -215,103 +214,99 @@ export const AddToDashboardControl: FC = ({ const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected); return ( - - - - + + + + + + + + - - - - - } - > - { - const newSelection = { - ...selectedSwimlanes, - [optionId]: !selectedSwimlanes[optionId as SwimlaneType], - }; - setSelectedSwimlanes(newSelection); - }} - data-test-subj="mlAddToDashboardSwimlaneTypeSelector" - /> - + } + > + { + const newSelection = { + ...selectedSwimlanes, + [optionId]: !selectedSwimlanes[optionId as SwimlaneType], + }; + setSelectedSwimlanes(newSelection); + }} + data-test-subj="mlAddToDashboardSwimlaneTypeSelector" + /> + - + - - } - data-test-subj="mlDashboardSelectionContainer" - > - - - - - - - - { - onClose(async () => { - const selectedDashboardId = selectedItems[0].id; - await addSwimlaneToDashboardCallback(); - await navigateToUrl( - await dashboardService.getDashboardEditUrl(selectedDashboardId) - ); - }); - }} - data-test-subj="mlAddAndEditDashboardButton" - > - - - + - - - - + } + data-test-subj="mlDashboardSelectionContainer" + > + + + + + + + + { + onClose(async () => { + const selectedDashboardId = selectedItems[0].id; + await addSwimlaneToDashboardCallback(); + await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId)); + }); + }} + data-test-subj="mlAddAndEditDashboardButton" + > + + + + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap index dc7e567380fdf9..388e2f590edf28 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap @@ -14,14 +14,6 @@ exports[`ExplorerNoInfluencersFound snapshot 1`] = ` } iconType="iInCircle" - title={ -

    - -

    - } + title={

    } /> `; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js index 6e058a8fc8c610..799437e1799f00 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js @@ -14,26 +14,48 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt } from '@elastic/eui'; -export const ExplorerNoResultsFound = () => ( - - -

    - } - body={ - -

    - -

    -
    - } - /> -); +export const ExplorerNoResultsFound = ({ hasResults, selectedJobsRunning }) => { + const resultsHaveNoAnomalies = hasResults === true; + const noResults = hasResults === false; + return ( + + {resultsHaveNoAnomalies && ( + + )} + {noResults && ( + + )} + + } + body={ + + {selectedJobsRunning && noResults && ( +

    + +

    + )} + {!selectedJobsRunning && ( +

    + +

    + )} +
    + } + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx index fe77fdf235b58d..65935050ee218a 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx @@ -12,7 +12,7 @@ export const NoOverallData: FC = () => { return ( ); }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 9f77260ab3320f..abf8197f51634d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -142,6 +142,7 @@ export class Explorer extends React.Component { setSelectedCells: PropTypes.func.isRequired, severity: PropTypes.number.isRequired, showCharts: PropTypes.bool.isRequired, + selectedJobsRunning: PropTypes.bool.isRequired, }; state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG }; @@ -223,7 +224,7 @@ export class Explorer extends React.Component { updateLanguage = (language) => this.setState({ language }); render() { - const { showCharts, severity, stoppedPartitions } = this.props; + const { showCharts, severity, stoppedPartitions, selectedJobsRunning } = this.props; const { annotations, @@ -248,6 +249,9 @@ export class Explorer extends React.Component { const noJobsFound = selectedJobs === null || selectedJobs.length === 0; const hasResults = overallSwimlaneData.points && overallSwimlaneData.points.length > 0; + const hasResultsWithAnomalies = + (hasResults && overallSwimlaneData.points.some((v) => v.value > 0)) || + tableData.anomalies?.length > 0; if (noJobsFound && !loading) { return ( @@ -257,10 +261,13 @@ export class Explorer extends React.Component { ); } - if (noJobsFound && hasResults === false && !loading) { + if (hasResultsWithAnomalies === false && !loading) { return ( - + ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx index 3bec404276ca26..a67863ea5f803b 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx @@ -10,7 +10,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiModal, - EuiOverlayMask, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody, @@ -77,74 +76,72 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, if (canDelete) { return ( - - - - - - - - -

    - {deleting === true ? ( -

    - - -
    - -
    + + + + + + + +

    + {deleting === true ? ( +

    + + +
    +
    - ) : ( - - - - )} -

    - - <> - - - - - + + )} +

    + + <> + + + + + - - - - - - - + + + +
    + + ); } else { return ( diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 8769c2c3cca20e..b23bbedb7413a2 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -21,7 +21,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiTabbedContent, - EuiOverlayMask, EuiConfirmModal, } from '@elastic/eui'; @@ -443,38 +442,36 @@ export class EditJobFlyoutUI extends Component { if (this.state.isConfirmationModalVisible) { confirmationModal = ( - - - } - onCancel={() => this.closeFlyout(true)} - onConfirm={() => this.save()} - cancelButtonText={ - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - > -

    - -

    -
    -
    + + } + onCancel={() => this.closeFlyout(true)} + onConfirm={() => this.save()} + cancelButtonText={ + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + > +

    + +

    +
    ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index a1bac4b6a35979..da4c9b0b0cc004 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -14,7 +14,6 @@ import { EuiFlexItem, EuiPanel, EuiSpacer, - EuiOverlayMask, EuiModal, EuiModalBody, EuiModalHeader, @@ -282,30 +281,28 @@ class CustomUrlsUI extends Component { ) : ( - - - - - - - + + + + + + - {editor} + {editor} - - {testButton} - {addButton} - - - + + {testButton} + {addButton} + + ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js index 5f5759e49208cb..361e8956c714e3 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js @@ -16,7 +16,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiHorizontalRule, EuiCheckbox, } from '@elastic/eui'; @@ -138,78 +137,76 @@ export class StartDatafeedModal extends Component { if (this.state.isModalVisible) { modal = ( - - - - - - - - - - + + + - {this.state.endTime === undefined && ( -
    - - - } - checked={createAlert} - onChange={this.setCreateAlert} - /> -
    - )} -
    - - - - - - - - + + + + + {this.state.endTime === undefined && ( +
    + + + } + checked={createAlert} + onChange={this.setCreateAlert} /> - - - - +
    + )} +
    + + + + + + + + + + +
    ); } return
    {modal}
    ; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 98d8b5eaf912a7..5b8fa5c672c6e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -19,6 +19,7 @@ import { stringMatch } from '../../../util/string_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { mlCalendarService } from '../../../services/calendar_service'; +import { isPopulatedObject } from '../../../../../common/util/object_utils'; export function loadFullJob(jobId) { return new Promise((resolve, reject) => { @@ -379,7 +380,7 @@ export function checkForAutoStartDatafeed() { mlJobService.tempJobCloningObjects.datafeed = undefined; mlJobService.tempJobCloningObjects.createdBy = undefined; - const hasDatafeed = typeof datafeed === 'object' && Object.keys(datafeed).length > 0; + const hasDatafeed = isPopulatedObject(datafeed); const datafeedId = hasDatafeed ? datafeed.datafeed_id : ''; return { id: job.job_id, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx index 6afc1122fcdab1..916a25271c63b8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -21,6 +21,7 @@ import { CombinedJob } from '../../../../../../../../common/types/anomaly_detect import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { mlJobService } from '../../../../../../services/job_service'; import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; +import { isPopulatedObject } from '../../../../../../../../common/util/object_utils'; export const DatafeedPreview: FC<{ combinedJob: CombinedJob | null; @@ -64,7 +65,7 @@ export const DatafeedPreview: FC<{ const resp = await mlJobService.searchPreview(combinedJob); let data = resp.hits.hits; // the first item under aggregations can be any name - if (typeof resp.aggregations === 'object' && Object.keys(resp.aggregations).length > 0) { + if (isPopulatedObject(resp.aggregations)) { const accessor = Object.keys(resp.aggregations)[0]; data = resp.aggregations[accessor].buckets.slice(0, ML_DATA_PREVIEW_COUNT); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx index 02f53c77c088c0..e42ec414e9641f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx @@ -8,13 +8,7 @@ import React, { FC, useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiCodeBlock, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { getDefaultDatafeedQuery } from '../../../../../utils/new_job_utils'; @@ -34,35 +28,33 @@ export const ResetQueryButton: FC = () => { return ( <> {confirmModalVisible && ( - - - + + - + - - {defaultQueryString} - - - + + {defaultQueryString} + + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx index 3f4a0f6ea6b3d2..aaed47cc7a02bb 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx @@ -9,7 +9,6 @@ import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -28,44 +27,42 @@ interface Props { export const ModalWrapper: FC = ({ onCreateClick, closeModal, saveEnabled, children }) => { return ( - - - - - - - + + + + + + - {children} + {children} - - - - + + + + - - - - - - + + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 052be41ca1eb70..e65ca22effd768 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -87,6 +87,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }); const { jobIds } = useJobSelection(jobsWithTimeRange); + const selectedJobsRunning = jobsWithTimeRange.some( + (job) => jobIds.includes(job.id) && job.isRunning === true + ); const explorerAppState = useObservable(explorerService.appState$); const explorerState = useObservable(explorerService.state$); @@ -261,6 +264,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim severity: tableSeverity.val, stoppedPartitions, invalidTimeRangeError, + selectedJobsRunning, }} />
    diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index ec1d36a1ced4c2..a8ae42658f3689 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -24,6 +24,7 @@ import { findAggField } from '../../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils'; import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; +import { isPopulatedObject } from '../../../../common/util/object_utils'; interface ResultResponse { success: boolean; @@ -175,7 +176,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { // when the field is an aggregation field, because the field doesn't actually exist in the indices // we need to pass all the sub aggs from the original datafeed config // so that we can access the aggregated field - if (typeof aggFields === 'object' && Object.keys(aggFields).length > 0) { + if (isPopulatedObject(aggFields)) { // first item under aggregations can be any name, not necessarily 'buckets' const accessor = Object.keys(aggFields)[0]; const tempAggs = { ...(aggFields[accessor].aggs ?? aggFields[accessor].aggregations) }; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 7f3c8ce9a1a6e5..42d8b32691c205 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -10,7 +10,7 @@ import { PropTypes } from 'prop-types'; import { i18n } from '@kbn/i18n'; -import { EuiPage, EuiPageBody, EuiPageContent, EuiOverlayMask } from '@elastic/eui'; +import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; import { NavigationMenu } from '../../../components/navigation_menu'; @@ -336,19 +336,13 @@ class NewCalendarUI extends Component { let modal = ''; if (isNewEventModalVisible) { - modal = ( - - - - ); + modal = ; } else if (isImportModalVisible) { modal = ( - - - + ); } diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js index afd1433b7ae698..bba28ab481ea11 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js @@ -10,7 +10,6 @@ import { PropTypes } from 'prop-types'; import { EuiConfirmModal, - EuiOverlayMask, EuiPage, EuiPageBody, EuiPageContent, @@ -111,37 +110,35 @@ export class CalendarsListUI extends Component { if (this.state.isDestroyModalVisible) { destroyModal = ( - - c.calendar_id).join(', '), - }} - /> - } - onCancel={this.closeDestroyModal} - onConfirm={this.deleteCalendars} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - /> - + c.calendar_id).join(', '), + }} + /> + } + onCancel={this.closeDestroyModal} + onConfirm={this.deleteCalendars} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + /> ); } diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap index 93ca044cb0c830..8cadb8270f680a 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap @@ -92,41 +92,39 @@ exports[`DeleteFilterListModal renders modal after clicking delete button 1`] = values={Object {}} /> - - - } - className="eui-textBreakWord" - confirmButtonText={ - - } - data-test-subj="mlFilterListDeleteConfirmation" - defaultFocusedButton="confirm" - onCancel={[Function]} - onConfirm={[Function]} - title={ - + } + className="eui-textBreakWord" + confirmButtonText={ + + } + data-test-subj="mlFilterListDeleteConfirmation" + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + - } - /> - + } + /> + } + />
    `; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js index bed0e7ca281e5b..20b716586b97d1 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiButton, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { deleteFilterLists } from './delete_filter_lists'; @@ -67,29 +67,27 @@ export class DeleteFilterListModal extends Component { /> ); modal = ( - - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - data-test-subj={'mlFilterListDeleteConfirmation'} - /> - + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + data-test-subj={'mlFilterListDeleteConfirmation'} + /> ); } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js index 613bd51bc16c3c..3261846a5fdd5b 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, } from '@elastic/eui'; @@ -31,48 +30,42 @@ import { FormattedMessage } from '@kbn/i18n/react'; export function Modal(props) { return ( - - - - - - - + + + + + + - - {props.messages.map((message, i) => ( - - - - - ))} + + {props.messages.map((message, i) => ( + + + + + ))} - {props.forecasts.length > 0 && ( - - - - - )} - - + {props.forecasts.length > 0 && ( + + + + + )} + + - - - - - - - + + + + + +
    ); } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 33e5183fa79493..06a0f7e17e1649 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1000,7 +1000,6 @@ export class TimeSeriesExplorer extends React.Component { }} /> } - color="warning" iconType="help" size="s" /> diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts index a0107ce8e049c5..7fb27f889c4173 100644 --- a/x-pack/plugins/ml/public/shared.ts +++ b/x-pack/plugins/ml/public/shared.ts @@ -17,8 +17,8 @@ export * from '../common/types/audit_message'; export * from '../common/util/anomaly_utils'; export * from '../common/util/errors'; export * from '../common/util/validators'; +export * from '../common/util/date_utils'; export * from './application/formatters/metric_change_description'; export * from './application/components/data_grid'; export * from './application/data_frame_analytics/common'; -export * from '../common/util/date_utils'; diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 0af8f1e1ec1cab..dc2c04540ef21d 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -39,6 +39,7 @@ import { } from '../../../common/util/job_utils'; import { groupsProvider } from './groups'; import type { MlClient } from '../../lib/ml_client'; +import { isPopulatedObject } from '../../../common/util/object_utils'; interface Results { [id: string]: { @@ -172,8 +173,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { }); const jobs = fullJobsList.map((job) => { - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + const hasDatafeed = isPopulatedObject(job.datafeed_config); const dataCounts = job.data_counts; const errorMessage = getSingleMetricViewerJobErrorMessage(job); @@ -233,8 +233,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { const jobs = fullJobsList.map((job) => { jobsMap[job.job_id] = job.groups || []; - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + const hasDatafeed = isPopulatedObject(job.datafeed_config); const timeRange: { to?: number; from?: number } = {}; const dataCounts = job.data_counts; diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json new file mode 100644 index 00000000000000..760ff188aacfc1 --- /dev/null +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*"], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_legacy/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../actions/tsconfig.json" }, + { "path": "../alerts/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, + { "path": "../encrypted_saved_objects/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../infra/tsconfig.json" }, + { "path": "../license_management/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../observability/tsconfig.json" }, + { "path": "../telemetry_collection_xpack/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/remove_cluster_button_provider/remove_cluster_button_provider.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/remove_cluster_button_provider/remove_cluster_button_provider.js index fd9f4e3503d106..4db5d1b333b7cf 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/remove_cluster_button_provider/remove_cluster_button_provider.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/remove_cluster_button_provider/remove_cluster_button_provider.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; export class RemoveClusterButtonProvider extends Component { static propTypes = { @@ -84,7 +84,7 @@ export class RemoveClusterButtonProvider extends Component { ); modal = ( - + <> {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} {!isSingleCluster && content} - + ); } diff --git a/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap index 09e487591c164a..692b410bd7e5f3 100644 --- a/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap @@ -58,7 +58,7 @@ Array [ >
    ,
    ,
    { }); return ( - - this.hideConfirm()} - onConfirm={() => this.props.performDelete()} - confirmButtonText={confirmButtonText} - cancelButtonText={cancelButtonText} - defaultFocusedButton="confirm" - buttonColor="danger" - > - {message} - - + this.hideConfirm()} + onConfirm={() => this.props.performDelete()} + confirmButtonText={confirmButtonText} + cancelButtonText={cancelButtonText} + defaultFocusedButton="confirm" + buttonColor="danger" + > + {message} + ); } diff --git a/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js index 650d63f38eeb52..ec7473c69dec19 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; export class ConfirmDeleteModal extends Component { static propTypes = { @@ -91,28 +91,26 @@ export class ConfirmDeleteModal extends Component { } return ( - - - {content} - - + + {content} + ); } } diff --git a/x-pack/plugins/security/public/components/confirm_modal.tsx b/x-pack/plugins/security/public/components/confirm_modal.tsx index d0ca1de07314e2..3802ee368d735b 100644 --- a/x-pack/plugins/security/public/components/confirm_modal.tsx +++ b/x-pack/plugins/security/public/components/confirm_modal.tsx @@ -18,7 +18,6 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiModalProps, - EuiOverlayMask, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -29,13 +28,11 @@ export interface ConfirmModalProps extends Omit = ({ children, @@ -45,51 +42,42 @@ export const ConfirmModal: FunctionComponent = ({ isDisabled, onCancel, onConfirm, - ownFocus = true, title, ...rest -}) => { - const modal = ( - - - {title} - - {children} - - - - - - - - - - {confirmButtonText} - - - - - - ); - - return ownFocus ? ( - {modal} - ) : ( - modal - ); -}; +}) => ( + + + {title} + + {children} + + + + + + + + + + {confirmButtonText} + + + + + +); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx index ae142e76877cef..232847b63cb1ab 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'src/core/public'; @@ -127,58 +127,56 @@ export const InvalidateProvider: React.FunctionComponent = ({ const isSingle = apiKeys.length === 1; return ( - - - {!isSingle ? ( - -

    - {i18n.translate( - 'xpack.security.management.apiKeys.invalidateApiKey.confirmModal.invalidateMultipleListDescription', - { defaultMessage: 'You are about to invalidate these API keys:' } - )} -

    -
      - {apiKeys.map(({ name, id }) => ( -
    • {name}
    • - ))} -
    -
    - ) : null} -
    -
    + )} + buttonColor="danger" + data-test-subj="invalidateApiKeyConfirmationModal" + > + {!isSingle ? ( + +

    + {i18n.translate( + 'xpack.security.management.apiKeys.invalidateApiKey.confirmModal.invalidateMultipleListDescription', + { defaultMessage: 'You are about to invalidate these API keys:' } + )} +

    +
      + {apiKeys.map(({ name, id }) => ( +
    • {name}
    • + ))} +
    +
    + ) : null} + ); }; diff --git a/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx index c5e6e3cb9860d0..680a4a40a7d9a1 100644 --- a/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useRef, useState, ReactElement } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart } from 'src/core/public'; @@ -140,59 +140,57 @@ export const DeleteProvider: React.FunctionComponent = ({ const isSingle = roleMappings.length === 1; return ( - - - {!isSingle ? ( - -

    - {i18n.translate( - 'xpack.security.management.roleMappings.deleteRoleMapping.confirmModal.deleteMultipleListDescription', - { defaultMessage: 'You are about to delete these role mappings:' } - )} -

    -
      - {roleMappings.map(({ name }) => ( -
    • {name}
    • - ))} -
    -
    - ) : null} -
    -
    + )} + confirmButtonDisabled={isDeleteInProgress} + buttonColor="danger" + data-test-subj="deleteRoleMappingConfirmationModal" + > + {!isSingle ? ( + +

    + {i18n.translate( + 'xpack.security.management.roleMappings.deleteRoleMapping.confirmModal.deleteMultipleListDescription', + { defaultMessage: 'You are about to delete these role mappings:' } + )} +

    +
      + {roleMappings.map(({ name }) => ( +
    • {name}
    • + ))} +
    +
    + ) : null} + ); }; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx index b094f78a53e778..d027a1aeb7e1fc 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx @@ -9,7 +9,6 @@ import React, { Component, Fragment } from 'react'; import { EuiSpacer, EuiConfirmModal, - EuiOverlayMask, EuiCallOut, EuiErrorBoundary, EuiIcon, @@ -228,40 +227,38 @@ export class RuleEditorPanel extends Component { return null; } return ( - - - } - onCancel={() => this.setState({ showConfirmModeChange: false })} - onConfirm={() => { - this.setState({ mode: 'visual', showConfirmModeChange: false }); - this.onValidityChange(true); - }} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -

    - -

    -
    -
    + + } + onCancel={() => this.setState({ showConfirmModeChange: false })} + onConfirm={() => { + this.setState({ mode: 'visual', showConfirmModeChange: false }); + this.onValidityChange(true); + }} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

    + +

    +
    ); }; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx index 6e94abfb3f4a20..478e8d87abf95c 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx @@ -12,7 +12,6 @@ import { EuiContextMenuItem, EuiLink, EuiIcon, - EuiOverlayMask, EuiConfirmModal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -87,45 +86,43 @@ export const RuleGroupTitle = (props: Props) => { ); const confirmChangeModal = showConfirmChangeModal ? ( - - - } - onCancel={() => { - setShowConfirmChangeModal(false); - setPendingNewRule(null); - }} - onConfirm={() => { - setShowConfirmChangeModal(false); - changeRuleDiscardingSubRules(pendingNewRule!); - setPendingNewRule(null); - }} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -

    - -

    -
    -
    + + } + onCancel={() => { + setShowConfirmChangeModal(false); + setPendingNewRule(null); + }} + onConfirm={() => { + setShowConfirmChangeModal(false); + changeRuleDiscardingSubRules(pendingNewRule!); + setPendingNewRule(null); + }} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

    + +

    +
    ) : null; return ( diff --git a/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx index bd3c86575c61a4..1b3a7fa024dd16 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButtonEmpty, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; @@ -46,44 +46,42 @@ export class DeleteRoleButton extends Component { return null; } return ( - - - } - onCancel={this.closeModal} - onConfirm={this.onConfirmDelete} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor={'danger'} - > -

    - -

    -

    - -

    -
    -
    + + } + onCancel={this.closeModal} + onConfirm={this.onConfirmDelete} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor={'danger'} + > +

    + +

    +

    + +

    +
    ); }; diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx index dbbb09f1598b63..81302465bb3732 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx @@ -14,7 +14,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -55,65 +54,61 @@ export class ConfirmDelete extends Component { // to disable the buttons since this could be a long-running operation return ( - - - - - {title} - - - - - {moreThanOne ? ( - -

    - -

    -
      - {rolesToDelete.map((roleName) => ( -
    • {roleName}
    • - ))} -
    -
    - ) : null} -

    - -

    -
    -
    - - + + + {title} + + + + {moreThanOne ? ( + +

    + +

    +
      + {rolesToDelete.map((roleName) => ( +
    • {roleName}
    • + ))} +
    +
    + ) : null} +

    - +

    +
    +
    + + + + - - - - -
    -
    + + + + + ); } diff --git a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index c670a9ce99f5bb..38adca145dfc5d 100644 --- a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -6,7 +6,7 @@ */ import React, { Component, Fragment } from 'react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -35,46 +35,44 @@ export class ConfirmDeleteUsers extends Component { values: { userLength: usersToDelete[0] }, }); return ( - - -
    - {moreThanOne ? ( - -

    - -

    -
      - {usersToDelete.map((username) => ( -
    • {username}
    • - ))} -
    -
    - ) : null} -

    - -

    -
    -
    -
    + +
    + {moreThanOne ? ( + +

    + +

    +
      + {usersToDelete.map((username) => ( +
    • {username}
    • + ))} +
    +
    + ) : null} +

    + +

    +
    +
    ); } diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx index 9e8745538e0ed3..189f0c3845d635 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx @@ -68,7 +68,6 @@ export const ConfirmDeleteUsers: FunctionComponent = ({ )} confirmButtonColor="danger" isLoading={state.loading} - ownFocus >

    diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx index 793f0e6c2a420b..e0fb4e554ee3c4 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx @@ -80,7 +80,6 @@ export const ConfirmDisableUsers: FunctionComponent = } confirmButtonColor={isSystemUser ? 'danger' : undefined} isLoading={state.loading} - ownFocus > {isSystemUser ? ( diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx index a1aac5bc0a8cb4..2cb4cf8b4a9e2c 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx @@ -67,7 +67,6 @@ export const ConfirmEnableUsers: FunctionComponent = ({ } )} isLoading={state.loading} - ownFocus >

    diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index 3b07d766d7cb46..f59fd6ecdec919 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -76,9 +76,9 @@ describe('#setup', () => { config: { enabled: true, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }, @@ -102,9 +102,9 @@ describe('#setup', () => { config: { enabled: true, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }, @@ -251,9 +251,9 @@ describe('#createLoggingConfig', () => { createLoggingConfig({ enabled: true, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }) @@ -264,10 +264,10 @@ describe('#createLoggingConfig', () => { Object { "appenders": Object { "auditTrailAppender": Object { - "kind": "console", "layout": Object { - "kind": "pattern", + "type": "pattern", }, + "type": "console", }, }, "loggers": Array [ @@ -275,8 +275,8 @@ describe('#createLoggingConfig', () => { "appenders": Array [ "auditTrailAppender", ], - "context": "audit.ecs", "level": "info", + "name": "audit.ecs", }, ], } @@ -293,9 +293,9 @@ describe('#createLoggingConfig', () => { createLoggingConfig({ enabled: false, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }) @@ -331,9 +331,9 @@ describe('#createLoggingConfig', () => { createLoggingConfig({ enabled: true, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }) diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 42e36e50d6d42d..99dd2c82ec9fe5 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -224,16 +224,16 @@ export const createLoggingConfig = (config: ConfigType['audit']) => map, LoggerContextConfigInput>((features) => ({ appenders: { auditTrailAppender: config.appender ?? { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', highlight: true, }, }, }, loggers: [ { - context: 'audit.ecs', + name: 'audit.ecs', level: config.enabled && config.appender && features.allowAuditLogging ? 'info' : 'off', appenders: ['auditTrailAppender'], }, diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap index 76d284a21984e9..04190fbf5eacdd 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action3]: expected value of type [boolean] but got [string]"`; +exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: expected value of type [boolean] but got [string]"`; -exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action2]: expected value of type [boolean] but got [undefined]"`; +exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`; exports[`validateEsPrivilegeResponse fails validation when an expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; -exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action4]: definition for this key is missing"`; +exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`; exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.otherApplication]: definition for this key is missing"`; diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index 93f5efed58fb8d..5bca46f22a5123 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -316,7 +316,7 @@ describe('#atSpace', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:bar-type/get]: definition for this key is missing]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` ); }); @@ -338,7 +338,7 @@ describe('#atSpace', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:foo-type/get]: expected value of type [boolean] but got [undefined]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` ); }); }); @@ -1092,7 +1092,7 @@ describe('#atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [mock-action:version]: expected value of type [boolean] but got [undefined]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` ); }); @@ -2266,7 +2266,7 @@ describe('#globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [mock-action:version]: expected value of type [boolean] but got [undefined]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` ); }); @@ -2384,7 +2384,7 @@ describe('#globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:bar-type/get]: definition for this key is missing]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` ); }); @@ -2405,7 +2405,7 @@ describe('#globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:foo-type/get]: expected value of type [boolean] but got [undefined]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` ); }); }); diff --git a/x-pack/plugins/security/server/authorization/validate_es_response.ts b/x-pack/plugins/security/server/authorization/validate_es_response.ts index 19afaaf035c15e..270ff26716e3f2 100644 --- a/x-pack/plugins/security/server/authorization/validate_es_response.ts +++ b/x-pack/plugins/security/server/authorization/validate_es_response.ts @@ -8,6 +8,11 @@ import { schema } from '@kbn/config-schema'; import { HasPrivilegesResponse } from './types'; +/** + * Validates an Elasticsearch "Has privileges" response against the expected application, actions, and resources. + * + * Note: the `actions` and `resources` parameters must be unique string arrays; any duplicates will cause validation to fail. + */ export function validateEsPrivilegeResponse( response: HasPrivilegesResponse, application: string, @@ -24,21 +29,29 @@ export function validateEsPrivilegeResponse( return response; } -function buildActionsValidationSchema(actions: string[]) { - return schema.object({ - ...actions.reduce>((acc, action) => { - return { - ...acc, - [action]: schema.boolean(), - }; - }, {}), - }); -} - function buildValidationSchema(application: string, actions: string[], resources: string[]) { - const actionValidationSchema = buildActionsValidationSchema(actions); + const actionValidationSchema = schema.boolean(); + const actionsValidationSchema = schema.object( + {}, + { + unknowns: 'allow', + validate: (value) => { + const actualActions = Object.keys(value).sort(); + if ( + actions.length !== actualActions.length || + ![...actions].sort().every((x, i) => x === actualActions[i]) + ) { + throw new Error('Payload did not match expected actions'); + } + + Object.values(value).forEach((actionResult) => { + actionValidationSchema.validate(actionResult); + }); + }, + } + ); - const resourceValidationSchema = schema.object( + const resourcesValidationSchema = schema.object( {}, { unknowns: 'allow', @@ -46,13 +59,13 @@ function buildValidationSchema(application: string, actions: string[], resources const actualResources = Object.keys(value).sort(); if ( resources.length !== actualResources.length || - !resources.sort().every((x, i) => x === actualResources[i]) + ![...resources].sort().every((x, i) => x === actualResources[i]) ) { throw new Error('Payload did not match expected resources'); } Object.values(value).forEach((actionResult) => { - actionValidationSchema.validate(actionResult); + actionsValidationSchema.validate(actionResult); }); }, } @@ -63,7 +76,7 @@ function buildValidationSchema(application: string, actions: string[], resources has_all_requested: schema.boolean(), cluster: schema.object({}, { unknowns: 'allow' }), application: schema.object({ - [application]: resourceValidationSchema, + [application]: resourcesValidationSchema, }), index: schema.object({}, { unknowns: 'allow' }), }); diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index d4dcca8bebb0c1..53e4152b3c8fbf 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -1558,21 +1558,21 @@ describe('createConfig()', () => { ConfigSchema.validate({ audit: { appender: { - kind: 'file', - path: '/path/to/file.txt', + type: 'file', + fileName: '/path/to/file.txt', layout: { - kind: 'json', + type: 'json', }, }, }, }).audit.appender ).toMatchInlineSnapshot(` Object { - "kind": "file", + "fileName": "/path/to/file.txt", "layout": Object { - "kind": "json", + "type": "json", }, - "path": "/path/to/file.txt", + "type": "file", } `); }); @@ -1583,12 +1583,12 @@ describe('createConfig()', () => { audit: { // no layout configured appender: { - kind: 'file', + type: 'file', path: '/path/to/file.txt', }, }, }) - ).toThrow('[audit.appender.2.kind]: expected value to equal [legacy-appender]'); + ).toThrow('[audit.appender.2.type]: expected value to equal [legacy-appender]'); }); it('rejects an ignore_filter when no appender is configured', () => { diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index bdb02d8ed99750..c4c7f399e7b5d3 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -52,6 +52,117 @@ describe('Config Deprecations', () => { `); }); + it('renames audit.appender.kind to audit.appender.type', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + kind: 'console', + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.kind).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.type).toEqual('console'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.type\\"", + ] + `); + }); + + it('renames audit.appender.layout.kind to audit.appender.layout.type', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + layout: { kind: 'pattern' }, + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.layout.kind).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.layout.type).toEqual('pattern'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.layout.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.layout.type\\"", + ] + `); + }); + + it('renames audit.appender.policy.kind to audit.appender.policy.type', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + policy: { kind: 'time-interval' }, + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.policy.kind).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.policy.type).toEqual('time-interval'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.policy.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.policy.type\\"", + ] + `); + }); + + it('renames audit.appender.strategy.kind to audit.appender.strategy.type', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + strategy: { kind: 'numeric' }, + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.strategy.kind).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.strategy.type).toEqual('numeric'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.strategy.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.strategy.type\\"", + ] + `); + }); + + it('renames audit.appender.path to audit.appender.fileName', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + type: 'file', + path: './audit.log', + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.path).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.fileName).toEqual('./audit.log'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.path\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.fileName\\"", + ] + `); + }); + it(`warns that 'authorization.legacyFallback.enabled' is unused`, () => { const config = { xpack: { diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index 65d18f0a4e7eb3..a7bb5e09fb919d 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -12,6 +12,13 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ unused, }) => [ rename('sessionTimeout', 'session.idleTimeout'), + + rename('audit.appender.kind', 'audit.appender.type'), + rename('audit.appender.layout.kind', 'audit.appender.layout.type'), + rename('audit.appender.policy.kind', 'audit.appender.policy.type'), + rename('audit.appender.strategy.kind', 'audit.appender.strategy.type'), + rename('audit.appender.path', 'audit.appender.fileName'), + unused('authorization.legacyFallback.enabled'), unused('authc.saml.maxRedirectURLSize'), // Deprecation warning for the old array-based format of `xpack.security.authc.providers`. diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 5d1f7572672990..aade8be4f503fb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -325,7 +325,7 @@ export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null, - 'partial failure': null, + warning: null, }); export type JobStatus = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index b76a762ca6cbf0..981a5422a05949 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -55,6 +55,7 @@ import { threat_filters, threat_mapping, threat_language, + threat_indicator_path, } from '../types/threat_mapping'; import { @@ -133,6 +134,7 @@ export const addPrepackagedRulesSchema = t.intersection([ threat_query, // defaults to "undefined" if not set during decode threat_index, // defaults to "undefined" if not set during decode threat_language, // defaults "undefined" if not set during decode + threat_indicator_path, // defaults "undefined" if not set during decode concurrent_searches, // defaults to "undefined" if not set during decode items_per_search, // defaults to "undefined" if not set during decode }) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index 0a7b8b120ba7ef..8fa5809abe68b4 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -62,6 +62,7 @@ import { threat_filters, threat_mapping, threat_language, + threat_indicator_path, } from '../types/threat_mapping'; import { @@ -152,6 +153,7 @@ export const importRulesSchema = t.intersection([ threat_query, // defaults to "undefined" if not set during decode threat_index, // defaults to "undefined" if not set during decode threat_language, // defaults "undefined" if not set during decode + threat_indicator_path, // defaults to "undefined" if not set during decode concurrent_searches, // defaults to "undefined" if not set during decode items_per_search, // defaults to "undefined" if not set during decode }) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 9d5331aeab8e4e..920fbaf4915c5c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -57,6 +57,7 @@ import { threat_filters, threat_mapping, threat_language, + threat_indicator_path, } from '../types/threat_mapping'; import { listArrayOrUndefined } from '../types/lists'; @@ -112,6 +113,7 @@ export const patchRulesSchema = t.exact( threat_filters, threat_mapping, threat_language, + threat_indicator_path, concurrent_searches, items_per_search, }) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index 87e5acb5428df7..fb29e37a53fdbe 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -56,6 +56,7 @@ export const getCreateThreatMatchRulesSchemaMock = ( rule_id: ruleId, threat_query: '*:*', threat_index: ['list-index'], + threat_indicator_path: 'threat.indicator', threat_mapping: [ { entries: [ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 14b47c8b2b3280..6b8211b23088ca 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -1152,7 +1152,7 @@ describe('create rules schema', () => { }); }); - describe('threat_mapping', () => { + describe('threat_match', () => { test('You can set a threat query, index, mapping, filters when creating a rule', () => { const payload = getCreateThreatMatchRulesSchemaMock(); const decoded = createRulesSchema.decode(payload); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 1c9ebe00333157..5cf2b6242b2f89 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -13,6 +13,7 @@ import { threat_query, threat_mapping, threat_index, + threat_indicator_path, concurrent_searches, items_per_search, } from '../types/threat_mapping'; @@ -213,6 +214,7 @@ const threatMatchRuleParams = { filters, saved_id, threat_filters, + threat_indicator_path, threat_language: t.keyof({ kuery: null, lucene: null }), concurrent_searches, items_per_search, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index b14c646e862d36..cf07389e207b34 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -150,6 +150,7 @@ export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial { expect(fields).toEqual(expected); }); - test('should return 8 fields for a rule of type "threat_match"', () => { + test('should return nine (9) fields for a rule of type "threat_match"', () => { const fields = addThreatMatchFields({ type: 'threat_match' }); - expect(fields.length).toEqual(8); + expect(fields.length).toEqual(9); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index bcdb0fa9b085d6..6bd54973e064f1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -70,6 +70,7 @@ import { threat_filters, threat_mapping, threat_language, + threat_indicator_path, } from '../types/threat_mapping'; import { DefaultListArray } from '../types/lists_default_array'; @@ -151,6 +152,7 @@ export const dependentRulesSchema = t.partial({ items_per_search, threat_mapping, threat_language, + threat_indicator_path, }); /** @@ -286,6 +288,9 @@ export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.exact(t.type({ threat_mapping: dependentRulesSchema.props.threat_mapping })), t.exact(t.partial({ threat_language: dependentRulesSchema.props.threat_language })), t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })), + t.exact( + t.partial({ threat_indicator_path: dependentRulesSchema.props.threat_indicator_path }) + ), t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), t.exact(t.partial({ concurrent_searches: dependentRulesSchema.props.concurrent_searches })), t.exact( diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts index d3975df488de9e..aab06941686c26 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts @@ -18,6 +18,11 @@ export type ThreatQuery = t.TypeOf; export const threatQueryOrUndefined = t.union([threat_query, t.undefined]); export type ThreatQueryOrUndefined = t.TypeOf; +export const threat_indicator_path = t.string; +export type ThreatIndicatorPath = t.TypeOf; +export const threatIndicatorPathOrUndefined = t.union([threat_indicator_path, t.undefined]); +export type ThreatIndicatorPathOrUndefined = t.TypeOf; + export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet export type ThreatFilters = t.TypeOf; export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 080b704e9c193b..725a2eb9fea7bb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -30,4 +30,5 @@ export const isEqlRule = (ruleType: Type | undefined): boolean => ruleType === ' export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType === 'threshold'; export const isQueryRule = (ruleType: Type | undefined): boolean => ruleType === 'query' || ruleType === 'saved_query'; -export const isThreatMatchRule = (ruleType: Type): boolean => ruleType === 'threat_match'; +export const isThreatMatchRule = (ruleType: Type | undefined): boolean => + ruleType === 'threat_match'; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index ffeaf853828f13..8aec9768dd50d2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -101,6 +101,7 @@ const POLICY_RESPONSE_STATUSES: HostPolicyResponseActionStatus[] = [ HostPolicyResponseActionStatus.success, HostPolicyResponseActionStatus.failure, HostPolicyResponseActionStatus.warning, + HostPolicyResponseActionStatus.unsupported, ]; const APPLIED_POLICIES: Array<{ @@ -1492,7 +1493,7 @@ export class EndpointDocGenerator { { name: 'workflow', message: 'Failed to apply a portion of the configuration (kernel)', - status: HostPolicyResponseActionStatus.success, + status: HostPolicyResponseActionStatus.unsupported, }, { name: 'download_model', @@ -1637,6 +1638,7 @@ export class EndpointDocGenerator { HostPolicyResponseActionStatus.failure, HostPolicyResponseActionStatus.success, HostPolicyResponseActionStatus.warning, + HostPolicyResponseActionStatus.unsupported, ]); } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index d361c0d6282a34..94a09b385a08c8 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -933,6 +933,7 @@ export enum HostPolicyResponseActionStatus { success = 'success', failure = 'failure', warning = 'warning', + unsupported = 'unsupported', } /** diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index 40e353263bcc8c..7e19944ea5856c 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -13,6 +13,7 @@ export enum HostPolicyResponseActionStatus { success = 'success', failure = 'failure', warning = 'warning', + unsupported = 'unsupported', } export enum HostsFields { diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index d6ec668e1b0f9f..988f0ad0c125d4 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -43,6 +43,7 @@ export { ExceptionListType, Type, ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, osTypeArray, OsTypeArray, } from '../../lists/common'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index cee8ccdea3e9e1..58e3b9824d8fdb 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -281,7 +281,7 @@ export enum TimelineId { active = 'timeline-1', casePage = 'timeline-case', test = 'test', // Reserved for testing purposes - test2 = 'test2', + alternateTest = 'alternateTest', } export const TimelineIdLiteralRt = runtimeTypes.union([ diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index ca4c869e0f2d38..c001f1fc2bc47d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -99,7 +99,7 @@ export const goToQueryTab = () => { export const addNotesToTimeline = (notes: string) => { goToNotesTab(); cy.get(NOTES_TEXT_AREA).type(notes); - cy.get(ADD_NOTE_BUTTON).click(); + cy.get(ADD_NOTE_BUTTON).click({ force: true }); cy.get(QUERY_TAB_BUTTON).click(); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx index 1b67aaeb795dd1..eb75d896ae7788 100644 --- a/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import * as i18n from './translations'; interface ConfirmDeleteCaseModalProps { @@ -28,20 +28,18 @@ const ConfirmDeleteCaseModalComp: React.FC = ({ return null; } return ( - - - {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION} - - + + {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION} + ); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index 1dfabda8068f17..eda8ed8cdfbcd5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -6,13 +6,7 @@ */ import React, { memo } from 'react'; -import { - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; import { Case } from '../../containers/types'; @@ -34,16 +28,14 @@ const AllCasesModalComponent: React.FC = ({ const userCanCrud = userPermissions?.crud ?? false; return isModalOpen ? ( - - - - {i18n.SELECT_CASE_TITLE} - - - - - - + + + {i18n.SELECT_CASE_TITLE} + + + + + ) : null; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 3595f2c916af71..8dd5080666cb38 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -7,13 +7,7 @@ import React, { memo } from 'react'; import styled from 'styled-components'; -import { - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { FormContext } from '../create/form_context'; import { CreateCaseForm } from '../create/form'; @@ -40,21 +34,19 @@ const CreateModalComponent: React.FC = ({ onSuccess, }) => { return isModalOpen ? ( - - - - {i18n.CREATE_TITLE} - - - - - - - - - - - + + + {i18n.CREATE_TITLE} + + + + + + + + + + ) : null; }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index a37528fcb24d7c..3ecc17589fe084 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -201,7 +201,7 @@ describe('EventsViewer', () => { testProps = { ...testProps, // Update with a new id, to force columns back to default. - id: TimelineId.test2, + id: TimelineId.alternateTest, }; const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index dc7388438c012a..5ea11f61f9a7e5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -14,7 +14,6 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiModalFooter, - EuiOverlayMask, EuiButton, EuiButtonEmpty, EuiHorizontalRule, @@ -348,133 +347,129 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [maybeRule]); return ( - - - - {addExceptionMessage} - - {ruleName} - - - - {fetchOrCreateListError != null && ( - - - + + + {addExceptionMessage} + + {ruleName} + + + + {fetchOrCreateListError != null && ( + + + + )} + {fetchOrCreateListError == null && + (isLoadingExceptionList || + isIndexPatternLoading || + isSignalIndexLoading || + isSignalIndexPatternLoading) && ( + )} - {fetchOrCreateListError == null && - (isLoadingExceptionList || - isIndexPatternLoading || - isSignalIndexLoading || - isSignalIndexPatternLoading) && ( - - )} - {fetchOrCreateListError == null && - !isSignalIndexLoading && - !isSignalIndexPatternLoading && - !isLoadingExceptionList && - !isIndexPatternLoading && - !isRuleLoading && - !mlJobLoading && - ruleExceptionList && ( - <> - - {isRuleEQLSequenceStatement && ( - <> - - - - )} - {i18n.EXCEPTION_BUILDER_INFO} - - - - - - - - - - {alertData !== undefined && alertStatus !== 'closed' && ( - - - - )} + {fetchOrCreateListError == null && + !isSignalIndexLoading && + !isSignalIndexPatternLoading && + !isLoadingExceptionList && + !isIndexPatternLoading && + !isRuleLoading && + !mlJobLoading && + ruleExceptionList && ( + <> + + {isRuleEQLSequenceStatement && ( + <> + + + + )} + {i18n.EXCEPTION_BUILDER_INFO} + + + + + + + + + + {alertData !== undefined && alertStatus !== 'closed' && ( - {exceptionListType === 'endpoint' && ( - <> - - - {i18n.ENDPOINT_QUARANTINE_TEXT} - - - )} - - - )} - {fetchOrCreateListError == null && ( - - {i18n.CANCEL} - - - {addExceptionMessage} - - + )} + + + + {exceptionListType === 'endpoint' && ( + <> + + + {i18n.ENDPOINT_QUARANTINE_TEXT} + + + )} + + )} - - + {fetchOrCreateListError == null && ( + + {i18n.CANCEL} + + + {addExceptionMessage} + + + )} + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 75b7bf2aabd7fd..336732016e9369 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -12,7 +12,6 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiModalFooter, - EuiOverlayMask, EuiButton, EuiButtonEmpty, EuiHorizontalRule, @@ -281,125 +280,121 @@ export const EditExceptionModal = memo(function EditExceptionModal({ }, [maybeRule]); return ( - - - - - {exceptionListType === 'endpoint' - ? i18n.EDIT_ENDPOINT_EXCEPTION_TITLE - : i18n.EDIT_EXCEPTION_TITLE} - - - {ruleName} - - - {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( - - )} - {!isSignalIndexLoading && - !addExceptionIsLoading && - !isIndexPatternLoading && - !isRuleLoading && - !mlJobLoading && ( - <> - - {isRuleEQLSequenceStatement && ( - <> - - - - )} - {i18n.EXCEPTION_BUILDER_INFO} - - - - - - - - - - - + + + {exceptionListType === 'endpoint' + ? i18n.EDIT_ENDPOINT_EXCEPTION_TITLE + : i18n.EDIT_EXCEPTION_TITLE} + + + {ruleName} + + + {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( + + )} + {!isSignalIndexLoading && + !addExceptionIsLoading && + !isIndexPatternLoading && + !isRuleLoading && + !mlJobLoading && ( + <> + + {isRuleEQLSequenceStatement && ( + <> + - - {exceptionListType === 'endpoint' && ( - <> - - - {i18n.ENDPOINT_QUARANTINE_TEXT} - - - )} - - - )} - {updateError != null && ( - - - - )} - {hasVersionConflict && ( - - -

    {i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

    - - - )} - {updateError == null && ( - - {i18n.CANCEL} - - - {i18n.EDIT_EXCEPTION_SAVE_BUTTON} - - + + + )} + {i18n.EXCEPTION_BUILDER_INFO} + + + + + + + + + + + + + {exceptionListType === 'endpoint' && ( + <> + + + {i18n.ENDPOINT_QUARANTINE_TEXT} + + + )} + + )} - - + {updateError != null && ( + + + + )} + {hasVersionConflict && ( + + +

    {i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

    +
    +
    + )} + {updateError == null && ( + + {i18n.CANCEL} + + + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} + + + )} + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap index 6503dd8dfb5086..d1a41b1c32c102 100644 --- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap @@ -2,64 +2,62 @@ exports[`ImportDataModal renders correctly against snapshot 1`] = ` - - - - - title - - - - -

    - description -

    -
    - - - - -
    - - - Cancel - - - submitBtnText - - -
    -
    + + + + title + + + + +

    + description +

    +
    + + + + +
    + + + Cancel + + + submitBtnText + + +
    `; diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx index 8a29ce3799321f..4c3dc2a249b4ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx @@ -15,7 +15,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -132,51 +131,49 @@ export const ImportDataModalComponent = ({ return ( <> {showModal && ( - - - - {title} - - - - -

    {description}

    -
    - - - { - setSelectedFiles(files && files.length > 0 ? files : null); - }} - display={'large'} - fullWidth={true} - isLoading={isImporting} + + + {title} + + + + +

    {description}

    +
    + + + { + setSelectedFiles(files && files.length > 0 ? files : null); + }} + display={'large'} + fullWidth={true} + isLoading={isImporting} + /> + + {showCheckBox && ( + setOverwrite(!overwrite)} /> - - {showCheckBox && ( - setOverwrite(!overwrite)} - /> - )} -
    - - - {i18n.CANCEL_BUTTON} - - {submitBtnText} - - -
    -
    + )} + + + + {i18n.CANCEL_BUTTON} + + {submitBtnText} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx index ece29cd360ce71..a5c0144531110a 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx @@ -15,7 +15,6 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiModalFooter, - EuiOverlayMask, EuiSpacer, EuiTabbedContent, } from '@elastic/eui'; @@ -211,24 +210,22 @@ export const ModalInspectQuery = ({ ]; return ( - - - - - {i18n.INSPECT} {title} - - - - - - - - - - {i18n.CLOSE} - - - - + + + + {i18n.INSPECT} {title} + + + + + + + + + + {i18n.CLOSE} + + + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index 778916ad2d07ac..be5702550a44c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -246,6 +246,12 @@ exports[`Paginated Table Component rendering it renders the default load more ta }, "euiFilePickerTallHeight": "128px", "euiFlyoutBorder": "1px solid #343741", + "euiFlyoutPaddingModifiers": Object { + "paddingLarge": "24px", + "paddingMedium": "16px", + "paddingNone": 0, + "paddingSmall": "8px", + }, "euiFocusBackgroundColor": "#08334a", "euiFocusRingAnimStartColor": "rgba(27, 169, 245, 0)", "euiFocusRingAnimStartSize": "6px", @@ -357,6 +363,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta }, "euiMarkdownEditorMinHeight": "150px", "euiPageBackgroundColor": "#1a1b20", + "euiPageDefaultMaxWidth": "1000px", "euiPaletteColorBlind": Object { "euiColorVis0": Object { "behindText": "#6dccb1", @@ -534,6 +541,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta "euiSwitchWidthCompressed": "28px", "euiSwitchWidthMini": "22px", "euiTabFontSize": "16px", + "euiTabFontSizeL": "18px", "euiTabFontSizeS": "14px", "euiTableActionsAreaWidth": "40px", "euiTableActionsBorderColor": "rgba(83, 89, 102, 0.09999999999999998)", diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap index f7924f37d2c173..5e008e28073de1 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -1,50 +1,48 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = ` - - - - - Your visualization has error(s) - - - - - - - - Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - - - - - - Close - - - - + + + + Your visualization has error(s) + + + + + + + + Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + + + + Close + + + `; diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx index 873ebe97317f4f..0a78139f5fe3a1 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx +++ b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx @@ -7,7 +7,6 @@ import { EuiButton, - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -36,36 +35,34 @@ const ModalAllErrorsComponent: React.FC = ({ isShowing, toast, t if (!isShowing || toast == null) return null; return ( - - - - {i18n.TITLE_ERROR_MODAL} - + + + {i18n.TITLE_ERROR_MODAL} + - - - - {toast.errors != null && - toast.errors.map((error, index) => ( - 100 ? `${error.substring(0, 100)} ...` : error} - data-test-subj="modal-all-errors-accordion" - > - {error} - - ))} - + + + + {toast.errors != null && + toast.errors.map((error, index) => ( + 100 ? `${error.substring(0, 100)} ...` : error} + data-test-subj="modal-all-errors-accordion" + > + {error} + + ))} + - - - {i18n.CLOSE_ERROR_MODAL} - - - - + + + {i18n.CLOSE_ERROR_MODAL} + + + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts index 4ed971ea6a936e..cca745659d2ccf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts @@ -14,6 +14,9 @@ export const getStatusColor = (status: RuleStatusType | string | null) => ? 'success' : status === 'failed' ? 'danger' - : status === 'executing' || status === 'going to run' || status === 'partial failure' + : status === 'executing' || + status === 'going to run' || + status === 'partial failure' || + status === 'warning' ? 'warning' : 'subdued'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx index 6292cc5b530b04..677e6de0ff485d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx @@ -16,7 +16,11 @@ import { import React, { memo, useCallback, useEffect, useState } from 'react'; import deepEqual from 'fast-deep-equal'; -import { useRuleStatus, RuleInfoStatus } from '../../../containers/detection_engine/rules'; +import { + useRuleStatus, + RuleInfoStatus, + RuleStatusType, +} from '../../../containers/detection_engine/rules'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { getStatusColor } from './helpers'; @@ -55,6 +59,19 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) } }, [fetchRuleStatus, ruleId]); + const getStatus = useCallback((status: RuleStatusType | null | undefined) => { + if (status == null) { + return getEmptyTagValue(); + } else if (status != null && status === 'partial failure') { + // Temporary fix if on upgrade a rule has a status of 'partial failure' we want to display that text as 'warning' + // On the next subsequent rule run, that 'partial failure' status will be re-written as a 'warning' status + // and this code will no longer be necessary + // TODO: remove this code in 8.0.0 + return 'warning'; + } + return status; + }, []); + return ( @@ -71,7 +88,7 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) - {currentStatus?.status ?? getEmptyTagValue()} + {getStatus(currentStatus?.status)} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts index 08feb5f2e51660..f73b2ccfb02ae6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts @@ -29,6 +29,7 @@ export const stepAboutDefaultValue: AboutStepRule = { license: '', ruleNameOverride: '', tags: [], + threatIndicatorPath: '', timestampOverride: '', threat: threatDefault, note: '', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 209071d27536d5..25295a823ea66e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -40,6 +40,7 @@ import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; import { AutocompleteField } from '../autocomplete_field'; import { useFetchIndex } from '../../../../common/containers/source'; +import { isThreatMatchRule } from '../../../../../common/detection_engine/utils'; const CommonUseField = getUseField({ component: Field }); @@ -298,6 +299,23 @@ const StepAboutRuleComponent: FC = ({ /> + {isThreatMatchRule(defineRuleData?.ruleType) && ( + <> + + + )} + = { ), labelAppend: OptionalFieldLabel, }, + threatIndicatorPath: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathLabel', + { + defaultMessage: 'Threat Indicator Path', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathHelpText', + { + defaultMessage: + 'Specify the document path containing your threat indicator fields. Used for enrichment of indicator match alerts. Defaults to threat.indicator unless otherwise specified.', + } + ), + labelAppend: OptionalFieldLabel, + }, timestampOverride: { type: FIELD_TYPES.TEXT, label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index adc46f08272d7e..aefa447269f46c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -15,7 +15,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiPanel, EuiSpacer, EuiText, @@ -211,7 +210,7 @@ export const ValueListsModalComponent: React.FC = ({ const columns = buildColumns(handleExport, handleDelete); return ( - + <> {i18n.MODAL_TITLE} @@ -255,7 +254,7 @@ export const ValueListsModalComponent: React.FC = ({ name={exportDownload.name} onDownload={() => setExportDownload({})} /> - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx index 20744c3a22515f..e4d8e2cee32685 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EuiListGroup, EuiListGroupItem, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EuiListGroup, EuiListGroupItem } from '@elastic/eui'; import styled from 'styled-components'; import { rgba } from 'polished'; @@ -59,28 +59,26 @@ export const ReferenceErrorModalComponent: React.FC = } return ( - - -

    {contentText}

    - - - {references.map((r, index) => ( - - ))} - - -
    -
    + +

    {contentText}

    + + + {references.map((r, index) => ( + + ))} + + +
    ); }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 591432829d90a0..b8f6c4bde3e8f7 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -24,6 +24,7 @@ import { listArray, threat_query, threat_index, + threat_indicator_path, threat_mapping, threat_language, threat_filters, @@ -77,6 +78,7 @@ const StatusTypes = t.union([ t.literal('failed'), t.literal('going to run'), t.literal('partial failure'), + t.literal('warning'), ]); // TODO: make a ticket @@ -132,6 +134,7 @@ export const RuleSchema = t.intersection([ threat_query, threat_filters, threat_index, + threat_indicator_path, threat_mapping, threat_language, timeline_id: t.string, @@ -252,7 +255,13 @@ export interface RuleStatus { failures: RuleInfoStatus[]; } -export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; +export type RuleStatusType = + | 'executing' + | 'failed' + | 'going to run' + | 'succeeded' + | 'partial failure' + | 'warning'; export interface RuleInfoStatus { alert_id: string; status_date: string; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index 425848cd09af0d..d110f2d52b3c5e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -313,7 +313,11 @@ export const getMonitoringColumns = ( }} href={formatUrl(getRuleDetailsUrl(item.id))} > - {value} + {/* Temporary fix if on upgrade a rule has a status of 'partial failure' we want to display that text as 'warning' */} + {/* On the next subsequent rule run, that 'partial failure' status will be re-written as a 'warning' status */} + {/* and this code will no longer be necessary */} + {/* TODO: remove this code in 8.0.0 */} + {value === 'partial failure' ? 'warning' : value} ); }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index 89785efbb5047a..04bf3c544030a4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -9,7 +9,6 @@ import { EuiBasicTable, EuiLoadingContent, EuiProgress, - EuiOverlayMask, EuiConfirmModal, EuiWindowEvent, } from '@elastic/eui'; @@ -490,18 +489,16 @@ export const RulesTables = React.memo( )} {showIdleModal && ( - - -

    {i18n.REFRESH_PROMPT_BODY}

    -
    -
    + +

    {i18n.REFRESH_PROMPT_BODY}

    +
    )} {shouldShowRulesTable && ( <> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index 5e2aeb4ead9340..fdb0513d7b7082 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -493,6 +493,15 @@ describe('helpers', () => { expect(result.exceptions_list).toEqual([getListMock()]); }); + test('returns a threat indicator path', () => { + mockData = { + ...mockData, + threatIndicatorPath: 'my_custom.path', + }; + const result = formatAboutStepData(mockData); + expect(result.threat_indicator_path).toEqual('my_custom.path'); + }); + test('returns formatted object with both exceptions_lists', () => { const result = formatAboutStepData( { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index c09f85ce7edccc..7c447214cfdebb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -288,6 +288,7 @@ export const formatAboutStepData = ( isBuildingBlock, note, ruleNameOverride, + threatIndicatorPath, timestampOverride, ...rest } = aboutStepData; @@ -330,6 +331,7 @@ export const formatAboutStepData = ( ...singleThreat, framework: 'MITRE ATT&CK', })), + threat_indicator_path: threatIndicatorPath, timestamp_override: timestampOverride !== '' ? timestampOverride : undefined, ...(!isEmpty(note) ? { note } : {}), ...rest, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 5836cac09e9b81..ed88ca41146f18 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -314,7 +314,7 @@ const RuleDetailsPageComponent = () => { /> ); } else if ( - rule?.status === 'partial failure' && + (rule?.status === 'warning' || rule?.status === 'partial failure') && ruleDetailTab === RuleDetailTabs.alerts && rule?.last_success_at != null ) { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index 4e6d8f4d567b18..1d100fb9109d07 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -52,7 +52,7 @@ export const ERROR_CALLOUT_TITLE = i18n.translate( export const PARTIAL_FAILURE_CALLOUT_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.partialErrorCalloutTitle', { - defaultMessage: 'Partial rule failure at', + defaultMessage: 'Warning at', } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index f0511602bd67f7..111eb8a5594a8d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -116,6 +116,7 @@ describe('rule helpers', () => { severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, tags: ['tag1', 'tag2'], threat: getThreatMock(), + threatIndicatorPath: '', timestampOverride: 'event.ingested', }; const scheduleRuleStepData = { from: '0s', interval: '5m' }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 35f9f0c658a6af..d37c2d9141f5d4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -153,6 +153,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu risk_score: riskScore, tags, threat, + threat_indicator_path: threatIndicatorPath, } = rule; return { @@ -179,6 +180,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu }, falsePositives, threat: threat as Threats, + threatIndicatorPath: threatIndicatorPath ?? '', }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 218d8c0178a2b0..94fdcc4069fc23 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -101,6 +101,7 @@ export interface AboutStepRule { ruleNameOverride: string; tags: string[]; timestampOverride: string; + threatIndicatorPath?: string; threat: Threats; note: string; } @@ -186,6 +187,7 @@ export interface AboutStepRuleJson { rule_name_override?: RuleNameOverride; tags: string[]; threat: Threats; + threat_indicator_path?: string; timestamp_override?: TimestampOverride; note?: string; } diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 4397573217312d..f70cd37b8da94e 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -273,6 +273,7 @@ export enum HostPolicyResponseActionStatus { success = 'success', failure = 'failure', warning = 'warning', + unsupported = 'unsupported', } export enum TimelineType { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts index 37a26d88053521..4745cd9de249db 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts @@ -25,6 +25,7 @@ export const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze< success: 'success', warning: 'warning', failure: 'danger', + unsupported: 'subdued', }); export const POLICY_STATUS_TO_TEXT = Object.freeze< @@ -39,4 +40,7 @@ export const POLICY_STATUS_TO_TEXT = Object.freeze< failure: i18n.translate('xpack.securitySolution.policyStatusText.failure', { defaultMessage: 'Failure', }), + unsupported: i18n.translate('xpack.securitySolution.policyStatusText.unsupported', { + defaultMessage: 'Unsupported', + }), }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index ec0198de585589..e14f56881d6733 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -12,7 +12,6 @@ import { EuiButton, EuiButtonEmpty, EuiSpacer, - EuiOverlayMask, EuiConfirmModal, EuiCallOut, EuiLoadingSpinner, @@ -234,59 +233,54 @@ const ConfirmUpdate = React.memo<{ onCancel: () => void; }>(({ hostCount, onCancel, onConfirm }) => { return ( - - - {hostCount > 0 && ( - <> - - - - - - )} -

    - -

    -
    -
    + + {hostCount > 0 && ( + <> + + + + + + )} +

    + +

    +
    ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx index 4e3dc953b539e2..bffd9806103721 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiText, } from '@elastic/eui'; @@ -100,36 +99,34 @@ export const TrustedAppDeletionDialog = memo(() => { if (useTrustedAppsSelector(isDeletionDialogOpen)) { return ( - - - - {translations.title} - + + + {translations.title} + - - -

    {translations.mainMessage}

    -

    {translations.subMessage}

    -
    -
    + + +

    {translations.mainMessage}

    +

    {translations.subMessage}

    +
    +
    - - - {translations.cancelButton} - + + + {translations.cancelButton} + - - {translations.confirmButton} - - -
    -
    + + {translations.confirmButton} + + + ); } else { return <>; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx index 35dcd88b77e04e..6713be176586cc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { useKibana } from '../../../../common/lib/kibana'; import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; +import { TimelineId } from '../../../../../common/types/timeline'; import { useTimelineKpis } from '../../../containers/kpis'; import { FlyoutHeader } from '.'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; @@ -33,6 +34,14 @@ const mockUseTimelineKpiResponse = { hostCount: 1, destinationIpCount: 1, }; + +const mockUseTimelineLargeKpiResponse = { + processCount: 1000, + userCount: 1000000, + sourceIpCount: 1000000000, + hostCount: 999, + destinationIpCount: 1, +}; const defaultMocks = { browserFields: mockBrowserFields, docValueFields: mockDocValueFields, @@ -65,7 +74,7 @@ describe('Timeline KPIs', () => { it('renders the component, labels and values succesfully', async () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true); @@ -87,7 +96,7 @@ describe('Timeline KPIs', () => { it('renders a loading indicator for values', async () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( @@ -103,7 +112,7 @@ describe('Timeline KPIs', () => { it('renders labels and the default empty string', async () => { const wrapper = mount( - + ); @@ -115,4 +124,29 @@ describe('Timeline KPIs', () => { ); }); }); + + describe('when the response contains numbers larger than one thousand', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]); + }); + it('formats the numbers correctly', async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( + expect.stringContaining('1k') + ); + expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual( + expect.stringContaining('1m') + ); + expect(wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text()).toEqual( + expect.stringContaining('1b') + ); + expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual( + expect.stringContaining('999') + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx index 3b0a86432aa969..e487fe70fdc94c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx @@ -5,61 +5,95 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; -import { EuiStat, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiStat, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; import { TimelineKpiStrategyResponse } from '../../../../../common/search_strategy'; import { getEmptyValue } from '../../../../common/components/empty_value'; import * as i18n from './translations'; export const TimelineKPIs = React.memo( ({ kpis, isLoading }: { kpis: TimelineKpiStrategyResponse | null; isLoading: boolean }) => { + const kpiFormat = '0,0.[000]a'; + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const formattedKpis = useMemo(() => { + return { + process: kpis === null ? getEmptyValue() : numeral(kpis.processCount).format(kpiFormat), + user: kpis === null ? getEmptyValue() : numeral(kpis.userCount).format(kpiFormat), + host: kpis === null ? getEmptyValue() : numeral(kpis.hostCount).format(kpiFormat), + sourceIp: kpis === null ? getEmptyValue() : numeral(kpis.sourceIpCount).format(kpiFormat), + destinationIp: + kpis === null ? getEmptyValue() : numeral(kpis.destinationIpCount).format(kpiFormat), + }; + }, [kpis]); + const formattedKpiToolTips = useMemo(() => { + return { + process: numeral(kpis?.processCount).format(defaultNumberFormat), + user: numeral(kpis?.userCount).format(defaultNumberFormat), + host: numeral(kpis?.hostCount).format(defaultNumberFormat), + sourceIp: numeral(kpis?.sourceIpCount).format(defaultNumberFormat), + destinationIp: numeral(kpis?.destinationIpCount).format(defaultNumberFormat), + }; + }, [kpis, defaultNumberFormat]); return ( - + + + - + + + - + + + - + + + - + + + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx index a87f486a9d5d1f..7dde3fbe4cd2a6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiModal } from '@elastic/eui'; import React, { useCallback } from 'react'; import { createGlobalStyle } from 'styled-components'; @@ -46,16 +46,14 @@ export const DeleteTimelineModalOverlay = React.memo( <> {isModalOpen && } {isModalOpen ? ( - - - - - + + + ) : null} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.tsx index 5b7fbcffd14ad7..c23cffa854514f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiModal } from '@elastic/eui'; import React from 'react'; import { TimelineModel } from '../../../../timelines/store/timeline/model'; @@ -26,22 +26,20 @@ const OPEN_TIMELINE_MODAL_WIDTH = 1100; // px export const OpenTimelineModal = React.memo( ({ hideActions = [], modalTitle, onClose, onOpen }) => ( - - - - - + + + ) ); 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 124c8012fd533c..aece377ee4f2dc 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 @@ -287,7 +287,7 @@ Array [ data-eui="EuiFocusTrap" >
    - - {isSaving && ( - + + {isSaving && ( + + )} + {modalHeader} + + + {showWarning && ( + + + + )} - {modalHeader} - - - {showWarning && ( - - - - - )} -
    - - - - - - - - - - - - - {closeModalText} - - - - - {saveButtonTitle} - - - - -
    -
    -
    - +
    + + + + + + + + + + + + + {closeModalText} + + + + + {saveButtonTitle} + + + + +
    + +
    ); } ); 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 2833a5ad24f2a3..88bf7941c84643 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,12 +10,17 @@ 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 { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types'; -import { buildArtifact, getFullEndpointExceptionList } from './lists'; +import { + buildArtifact, + getEndpointExceptionList, + getEndpointTrustedAppsList, + getFilteredEndpointExceptionList, +} from './lists'; import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; import { ArtifactConstants } from './common'; -import { ENDPOINT_LIST_ID } from '../../../../../lists/common'; +import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common'; -describe('buildEventTypeSignal', () => { +describe('artifacts lists', () => { let mockExceptionClient: ExceptionListClient; beforeEach(() => { @@ -23,214 +28,384 @@ describe('buildEventTypeSignal', () => { mockExceptionClient = listMock.getExceptionListClient(); }); - test('it should convert the exception lists response to the proper endpoint format', async () => { - const expectedEndpointExceptions = { - type: 'simple', - entries: [ - { - entries: [ - { - field: 'nested.field', - operator: 'included', - type: 'exact_cased', - value: 'some value', - }, - ], - field: 'some.parentField', - type: 'nested', - }, - { - field: 'some.not.nested.field', - operator: 'included', - type: 'exact_cased', - value: 'some value', - }, - ], - }; - - const first = getFoundExceptionListItemSchemaMock(); - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - expect(resp).toEqual({ - entries: [expectedEndpointExceptions], + describe('getFilteredEndpointExceptionList', () => { + const TEST_FILTER = 'exception-list-agnostic.attributes.os_types:"linux"'; + + test('it should convert the exception lists response to the proper endpoint format', async () => { + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + { + field: 'some.not.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); }); - }); - test('it should convert simple fields', async () => { - const testEntries: EntriesArray = [ - { field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' }, - { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, - { field: 'host.hostname', operator: 'included', type: 'match', value: 'estc' }, - ]; + test('it should convert simple fields', async () => { + const testEntries: EntriesArray = [ + { field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' }, + { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, + { field: 'host.hostname', operator: 'included', type: 'match', value: 'estc' }, + ]; - const expectedEndpointExceptions = { - type: 'simple', - entries: [ - { - field: 'host.os.full', - operator: 'included', - type: 'exact_cased', - value: 'windows', - }, + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'host.os.full', + operator: 'included', + type: 'exact_cased', + value: 'windows', + }, + { + field: 'server.ip', + operator: 'included', + type: 'exact_cased', + value: '192.168.1.1', + }, + { + field: 'host.hostname', + operator: 'included', + type: 'exact_cased', + value: 'estc', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should convert fields case sensitive', async () => { + const testEntries: EntriesArray = [ + { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, + { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, { - field: 'server.ip', + field: 'host.hostname.caseless', operator: 'included', - type: 'exact_cased', - value: '192.168.1.1', + type: 'match_any', + value: ['estc', 'kibana'], }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'host.os.full', + operator: 'included', + type: 'exact_caseless', + value: 'windows', + }, + { + field: 'server.ip', + operator: 'included', + type: 'exact_cased', + value: '192.168.1.1', + }, + { + field: 'host.hostname', + operator: 'included', + type: 'exact_caseless_any', + value: ['estc', 'kibana'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should deduplicate exception entries', async () => { + const testEntries: EntriesArray = [ + { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, + { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, + { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, + { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, { field: 'host.hostname', operator: 'included', - type: 'exact_cased', - value: 'estc', + type: 'match_any', + value: ['estc', 'kibana'], }, - ], - }; - - const first = getFoundExceptionListItemSchemaMock(); - first.data[0].entries = testEntries; - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - expect(resp).toEqual({ - entries: [expectedEndpointExceptions], - }); - }); + ]; - test('it should convert fields case sensitive', async () => { - const testEntries: EntriesArray = [ - { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, - { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, - { - field: 'host.hostname.caseless', - operator: 'included', - type: 'match_any', - value: ['estc', 'kibana'], - }, - ]; + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'host.os.full', + operator: 'included', + type: 'exact_caseless', + value: 'windows', + }, + { + field: 'server.ip', + operator: 'included', + type: 'exact_cased', + value: '192.168.1.1', + }, + { + field: 'host.hostname', + operator: 'included', + type: 'exact_cased_any', + value: ['estc', 'kibana'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); - const expectedEndpointExceptions = { - type: 'simple', - entries: [ - { - field: 'host.os.full', - operator: 'included', - type: 'exact_caseless', - value: 'windows', - }, - { - field: 'server.ip', - operator: 'included', - type: 'exact_cased', - value: '192.168.1.1', - }, + test('it should not deduplicate exception entries across nested boundaries', async () => { + const testEntries: EntriesArray = [ { - field: 'host.hostname', - operator: 'included', - type: 'exact_caseless_any', - value: ['estc', 'kibana'], + entries: [ + { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' }, + ], + field: 'some.parentField', + type: 'nested', }, - ], - }; - - const first = getFoundExceptionListItemSchemaMock(); - first.data[0].entries = testEntries; - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - expect(resp).toEqual({ - entries: [expectedEndpointExceptions], + // Same as above but not inside the nest + { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); }); - }); - test('it should deduplicate exception entries', async () => { - const testEntries: EntriesArray = [ - { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, - { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, - { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, - { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, - { - field: 'host.hostname', - operator: 'included', - type: 'match_any', - value: ['estc', 'kibana'], - }, - ]; + test('it should deduplicate exception items', async () => { + const testEntries: EntriesArray = [ + { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, + { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, + ]; - const expectedEndpointExceptions = { - type: 'simple', - entries: [ + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'host.os.full', + operator: 'included', + type: 'exact_caseless', + value: 'windows', + }, + { + field: 'server.ip', + operator: 'included', + type: 'exact_cased', + value: '192.168.1.1', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + + // Create a second exception item with the same entries + first.data[1] = getExceptionListItemSchemaMock(); + first.data[1].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should ignore unsupported entries', async () => { + // Lists and exists are not supported by the Endpoint + const testEntries: EntriesArray = [ + { field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' }, { field: 'host.os.full', operator: 'included', - type: 'exact_caseless', - value: 'windows', - }, - { - field: 'server.ip', - operator: 'included', - type: 'exact_cased', - value: '192.168.1.1', - }, - { - field: 'host.hostname', - operator: 'included', - type: 'exact_cased_any', - value: ['estc', 'kibana'], - }, - ], - }; - - const first = getFoundExceptionListItemSchemaMock(); - first.data[0].entries = testEntries; - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - expect(resp).toEqual({ - entries: [expectedEndpointExceptions], - }); - }); + type: 'list', + list: { + id: 'lists_not_supported', + type: 'keyword', + }, + } as EntryList, + { field: 'server.ip', operator: 'included', type: 'exists' }, + ]; - test('it should not deduplicate exception entries across nested boundaries', async () => { - const testEntries: EntriesArray = [ - { + const expectedEndpointExceptions = { + type: 'simple', entries: [ - { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' }, + { + field: 'host.os.full', + operator: 'included', + type: 'exact_cased', + value: 'windows', + }, ], - field: 'some.parentField', - type: 'nested', - }, - // Same as above but not inside the nest - { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' }, - ]; + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should convert the exception lists response to the proper endpoint format while paging', async () => { + // The first call returns two exceptions + const first = getFoundExceptionListItemSchemaMock(); + first.per_page = 2; + first.total = 4; + first.data.push(getExceptionListItemSchemaMock()); + + // The second call returns two exceptions + const second = getFoundExceptionListItemSchemaMock(); + second.per_page = 2; + second.total = 4; + second.data.push(getExceptionListItemSchemaMock()); + + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(first) + .mockReturnValueOnce(second); + + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + + // Expect 2 exceptions, the first two calls returned the same exception list items + expect(resp.entries.length).toEqual(2); + }); + + test('it should handle no exceptions', async () => { + const exceptionsResponse = getFoundExceptionListItemSchemaMock(); + exceptionsResponse.data = []; + exceptionsResponse.total = 0; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse); + const resp = await getFilteredEndpointExceptionList( + mockExceptionClient, + 'v1', + TEST_FILTER, + ENDPOINT_LIST_ID + ); + expect(resp.entries.length).toEqual(0); + }); - const expectedEndpointExceptions = { - type: 'simple', - entries: [ + test('it should return a stable hash regardless of order of entries', async () => { + const translatedEntries: TranslatedEntry[] = [ { entries: [ { - field: 'nested.field', + field: 'some.nested.field', operator: 'included', type: 'exact_cased', value: 'some value', @@ -245,218 +420,107 @@ describe('buildEventTypeSignal', () => { type: 'exact_cased', value: 'some value', }, - ], - }; - - const first = getFoundExceptionListItemSchemaMock(); - first.data[0].entries = testEntries; - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - expect(resp).toEqual({ - entries: [expectedEndpointExceptions], - }); - }); - - test('it should deduplicate exception items', async () => { - const testEntries: EntriesArray = [ - { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' }, - { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, - ]; - - const expectedEndpointExceptions = { - type: 'simple', - entries: [ - { - field: 'host.os.full', - operator: 'included', - type: 'exact_caseless', - value: 'windows', - }, - { - field: 'server.ip', - operator: 'included', - type: 'exact_cased', - value: '192.168.1.1', - }, - ], - }; - - const first = getFoundExceptionListItemSchemaMock(); - first.data[0].entries = testEntries; - - // Create a second exception item with the same entries - first.data[1] = getExceptionListItemSchemaMock(); - first.data[1].entries = testEntries; - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - expect(resp).toEqual({ - entries: [expectedEndpointExceptions], - }); - }); - - test('it should ignore unsupported entries', async () => { - // Lists and exists are not supported by the Endpoint - const testEntries: EntriesArray = [ - { field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' }, - { - field: 'host.os.full', - operator: 'included', - type: 'list', - list: { - id: 'lists_not_supported', - type: 'keyword', - }, - } as EntryList, - { field: 'server.ip', operator: 'included', type: 'exists' }, - ]; - - const expectedEndpointExceptions = { - type: 'simple', - entries: [ - { - field: 'host.os.full', - operator: 'included', - type: 'exact_cased', - value: 'windows', - }, - ], - }; - - const first = getFoundExceptionListItemSchemaMock(); - first.data[0].entries = testEntries; - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - expect(resp).toEqual({ - entries: [expectedEndpointExceptions], - }); - }); + ]; + const translatedEntriesReversed = translatedEntries.reverse(); - test('it should convert the exception lists response to the proper endpoint format while paging', async () => { - // The first call returns two exceptions - const first = getFoundExceptionListItemSchemaMock(); - first.per_page = 2; - first.total = 4; - first.data.push(getExceptionListItemSchemaMock()); - - // The second call returns two exceptions - const second = getFoundExceptionListItemSchemaMock(); - second.per_page = 2; - second.total = 4; - second.data.push(getExceptionListItemSchemaMock()); - - mockExceptionClient.findExceptionListItem = jest - .fn() - .mockReturnValueOnce(first) - .mockReturnValueOnce(second); - - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - - // Expect 2 exceptions, the first two calls returned the same exception list items - expect(resp.entries.length).toEqual(2); - }); - - test('it should handle no exceptions', async () => { - const exceptionsResponse = getFoundExceptionListItemSchemaMock(); - exceptionsResponse.data = []; - exceptionsResponse.total = 0; - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse); - const resp = await getFullEndpointExceptionList( - mockExceptionClient, - 'linux', - 'v1', - ENDPOINT_LIST_ID - ); - expect(resp.entries.length).toEqual(0); - }); + const translatedExceptionList = { + entries: [ + { + type: 'simple', + entries: translatedEntries, + }, + ], + }; - test('it should return a stable hash regardless of order of entries', async () => { - const translatedEntries: TranslatedEntry[] = [ - { + const translatedExceptionListReversed = { entries: [ { - field: 'some.nested.field', - operator: 'included', - type: 'exact_cased', - value: 'some value', + type: 'simple', + entries: translatedEntriesReversed, }, ], - field: 'some.parentField', - type: 'nested', - }, - { - field: 'nested.field', - operator: 'included', - type: 'exact_cased', - value: 'some value', - }, - ]; - const translatedEntriesReversed = translatedEntries.reverse(); + }; + + const artifact1 = await buildArtifact( + translatedExceptionList, + 'v1', + 'linux', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + const artifact2 = await buildArtifact( + translatedExceptionListReversed, + 'v1', + 'linux', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); + }); - const translatedExceptionList = { - entries: [ + test('it should return a stable hash regardless of order of items', async () => { + const translatedItems: TranslatedExceptionListItem[] = [ { type: 'simple', - entries: translatedEntries, + entries: [ + { + entries: [ + { + field: 'some.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + ], }, - ], - }; - - const translatedExceptionListReversed = { - entries: [ { type: 'simple', - entries: translatedEntriesReversed, + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], }, - ], - }; - - const artifact1 = await buildArtifact( - translatedExceptionList, - 'linux', - 'v1', - ArtifactConstants.GLOBAL_ALLOWLIST_NAME - ); - const artifact2 = await buildArtifact( - translatedExceptionListReversed, - 'linux', - 'v1', - ArtifactConstants.GLOBAL_ALLOWLIST_NAME - ); - expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); + ]; + + const translatedExceptionList = { + entries: translatedItems, + }; + + const translatedExceptionListReversed = { + entries: translatedItems.reverse(), + }; + + const artifact1 = await buildArtifact( + translatedExceptionList, + 'v1', + 'linux', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + const artifact2 = await buildArtifact( + translatedExceptionListReversed, + 'v1', + 'linux', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); + }); }); - test('it should return a stable hash regardless of order of items', async () => { - const translatedItems: TranslatedExceptionListItem[] = [ + const TEST_EXCEPTION_LIST_ITEM = { + entries: [ { type: 'simple', entries: [ { entries: [ { - field: 'some.nested.field', + field: 'nested.field', operator: 'included', type: 'exact_cased', value: 'some value', @@ -465,41 +529,87 @@ describe('buildEventTypeSignal', () => { field: 'some.parentField', type: 'nested', }, - ], - }, - { - type: 'simple', - entries: [ { - field: 'nested.field', + field: 'some.not.nested.field', operator: 'included', type: 'exact_cased', value: 'some value', }, ], }, - ]; - - const translatedExceptionList = { - entries: translatedItems, - }; - - const translatedExceptionListReversed = { - entries: translatedItems.reverse(), - }; - - const artifact1 = await buildArtifact( - translatedExceptionList, - 'linux', - 'v1', - ArtifactConstants.GLOBAL_ALLOWLIST_NAME - ); - const artifact2 = await buildArtifact( - translatedExceptionListReversed, - 'linux', - 'v1', - ArtifactConstants.GLOBAL_ALLOWLIST_NAME - ); - expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); + ], + }; + + describe('getEndpointExceptionList', () => { + test('it should build proper kuery', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointExceptionList(mockExceptionClient, 'v1', 'windows'); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_LIST_ID, + namespaceType: 'agnostic', + filter: 'exception-list-agnostic.attributes.os_types:"windows"', + perPage: 100, + page: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + }); + }); + + describe('getEndpointTrustedAppsList', () => { + test('it should build proper kuery without policy', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointTrustedAppsList(mockExceptionClient, 'v1', 'macos'); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_TRUSTED_APPS_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('it should build proper kuery with policy', async () => { + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(getFoundExceptionListItemSchemaMock()); + + const resp = await getEndpointTrustedAppsList( + mockExceptionClient, + 'v1', + 'macos', + 'c6d16e42-c32d-4dce-8a88-113cfe276ad1' + ); + + expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM); + + expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({ + listId: ENDPOINT_TRUSTED_APPS_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 6cc6a821eba334..322bb2ca47a45c 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 @@ -12,7 +12,7 @@ import { validate } from '../../../../common/validate'; import { Entry, EntryNested } from '../../../../../lists/common/schemas/types'; import { ExceptionListClient } from '../../../../../lists/server'; -import { ENDPOINT_LIST_ID } from '../../../../common/shared_imports'; +import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../common/shared_imports'; import { InternalArtifactSchema, TranslatedEntry, @@ -28,12 +28,11 @@ import { internalArtifactCompleteSchema, InternalArtifactCompleteSchema, } from '../../schemas'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, - os: string, schemaVersion: string, + os: string, name: string ): Promise { const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions)); @@ -74,10 +73,10 @@ export function isCompressed(artifact: InternalArtifactSchema) { return artifact.compressionAlgorithm === 'zlib'; } -export async function getFullEndpointExceptionList( +export async function getFilteredEndpointExceptionList( eClient: ExceptionListClient, - os: string, schemaVersion: string, + filter: string, listId: typeof ENDPOINT_LIST_ID | typeof ENDPOINT_TRUSTED_APPS_LIST_ID ): Promise { const exceptions: WrappedTranslatedExceptionList = { entries: [] }; @@ -88,7 +87,7 @@ export async function getFullEndpointExceptionList( const response = await eClient.findExceptionListItem({ listId, namespaceType: 'agnostic', - filter: `exception-list-agnostic.attributes.os_types:\"${os}\"`, + filter, perPage: 100, page, sortField: 'created_at', @@ -114,6 +113,35 @@ export async function getFullEndpointExceptionList( 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 + ); +} + /** * Translates Exception list items to Exceptions the endpoint can understand * @param exceptions diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts index 972c4f3153a1c8..b8b1e13f2052b5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts @@ -35,7 +35,7 @@ const createExceptionListItemOptions = ( name: '', namespaceType: 'agnostic', osTypes: [], - tags: [], + tags: ['policy:all'], type: 'simple', ...options, }); @@ -56,7 +56,7 @@ const exceptionListItemSchema = ( name: '', namespace_type: 'agnostic', os_types: [], - tags: [], + tags: ['policy:all'], type: 'simple', tie_breaker_id: '123', updated_at: '11/11/2011T11:11:11.111', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index 4d2238ea96ee15..41b4b7b1d55fdd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -15,7 +15,7 @@ import { ExceptionListItemSchema, NestedEntriesArray, } from '../../../../../lists/common/shared_exports'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common'; import { CreateExceptionListItemOptions } from '../../../../../lists/server'; import { ConditionEntry, @@ -184,7 +184,7 @@ export const newTrustedAppToCreateExceptionListItemOptions = ({ name, namespaceType: 'agnostic', osTypes: [OPERATING_SYSTEM_TO_OS_TYPE[os]], - tags: [], + tags: ['policy:all'], type: 'simple', }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts index dc3c369494d4e2..97a8451bf25d83 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts @@ -6,7 +6,7 @@ */ import { ExceptionListClient } from '../../../../../lists/server'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common'; import { DeleteTrustedAppsRequestParams, diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts index dedbcc25e2373e..1975c2a92cc16f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -36,8 +36,8 @@ export const getInternalArtifactMock = async ( ): Promise => { const artifact = await buildArtifact( getTranslatedExceptionListMock(), - os, schemaVersion, + os, artifactName ); return opts?.compress ? compressArtifact(artifact) : artifact; @@ -49,7 +49,7 @@ export const getEmptyInternalArtifactMock = async ( opts?: { compress: boolean }, artifactName: string = ArtifactConstants.GLOBAL_ALLOWLIST_NAME ): Promise => { - const artifact = await buildArtifact({ entries: [] }, os, schemaVersion, artifactName); + const artifact = await buildArtifact({ entries: [] }, schemaVersion, os, artifactName); return opts?.compress ? compressArtifact(artifact) : artifact; }; @@ -62,8 +62,8 @@ export const getInternalArtifactMockWithDiffs = async ( mock.entries.pop(); const artifact = await buildArtifact( mock, - os, schemaVersion, + os, ArtifactConstants.GLOBAL_ALLOWLIST_NAME ); return opts?.compress ? compressArtifact(artifact) : artifact; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index a8bbfca0d41e58..b0e0d5d8ebfbea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -33,17 +33,24 @@ export const createExceptionListResponse = (data: ExceptionListItemSchema[], tot type FindExceptionListItemOptions = Parameters[0]; -const FILTER_REGEXP = /^exception-list-agnostic\.attributes\.os_types:"(\w+)"$/; +const FILTER_PROPERTY_PREFIX = 'exception-list-agnostic\\.attributes'; +const FILTER_REGEXP = new RegExp( + `^${FILTER_PROPERTY_PREFIX}\.os_types:"([^"]+)"( and \\(${FILTER_PROPERTY_PREFIX}\.tags:"policy:all"( or ${FILTER_PROPERTY_PREFIX}\.tags:"policy:([^"]+)")?\\))?$` +); export const mockFindExceptionListItemResponses = ( responses: Record> ) => { return jest.fn().mockImplementation((options: FindExceptionListItemOptions) => { - const os = FILTER_REGEXP.test(options.filter || '') - ? options.filter!.match(FILTER_REGEXP)![1] - : ''; - - return createExceptionListResponse(responses[options.listId]?.[os] || []); + const matches = options.filter!.match(FILTER_REGEXP) || []; + + if (matches[4] && responses[options.listId]?.[`${matches![1]}-${matches[4]}`]) { + return createExceptionListResponse( + responses[options.listId]?.[`${matches![1]}-${matches[4]}`] || [] + ); + } else { + return createExceptionListResponse(responses[options.listId]?.[matches![1] || ''] || []); + } }); }; @@ -118,7 +125,7 @@ export const getManifestManagerMock = ( context.exceptionListClient.findExceptionListItem = jest .fn() .mockRejectedValue(new Error('unexpected thing happened')); - return super.buildExceptionListArtifacts('v1'); + return super.buildExceptionListArtifacts(); case ManifestManagerMockType.NormalFlow: return getMockArtifactsWithDiff(); } 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 52897f473189fb..26db49be459fa2 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 @@ -8,8 +8,7 @@ import { inflateSync } from 'zlib'; import { SavedObjectsErrorHelpers } from 'src/core/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { ENDPOINT_LIST_ID } from '../../../../../../lists/common'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants'; +import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { PackagePolicy } from '../../../../../../fleet/common/types/models'; import { getEmptyInternalArtifactMock } from '../../../schemas/artifacts/saved_objects.mock'; @@ -211,10 +210,19 @@ describe('ManifestManager', () => { ARTIFACT_NAME_TRUSTED_APPS_LINUX, ]; - const getArtifactIds = (artifacts: InternalArtifactSchema[]) => - artifacts.map((artifact) => artifact.identifier); + const getArtifactIds = (artifacts: InternalArtifactSchema[]) => [ + ...new Set(artifacts.map((artifact) => artifact.identifier)).values(), + ]; + + const mockPolicyListIdsResponse = (items: string[]) => + jest.fn().mockResolvedValue({ + items, + page: 1, + per_page: 100, + total: items.length, + }); - test('Fails when exception list list client fails', async () => { + test('Fails when exception list client fails', async () => { const context = buildManifestManagerContextMock({}); const manifestManager = new ManifestManager(context); @@ -228,6 +236,7 @@ describe('ManifestManager', () => { const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({}); + context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); const manifest = await manifestManager.buildNewManifest(); @@ -237,11 +246,16 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); + expect(artifacts.length).toBe(5); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts.every(isCompressed)).toBe(true); for (const artifact of artifacts) { expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] }); + expect(manifest.isDefaultArtifact(artifact)).toBe(true); + expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); } }); @@ -255,6 +269,7 @@ describe('ManifestManager', () => { [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, [ENDPOINT_TRUSTED_APPS_LIST_ID]: { linux: [trustedAppListItem] }, }); + context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); const manifest = await manifestManager.buildNewManifest(); @@ -264,21 +279,25 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); + expect(artifacts.length).toBe(5); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts.every(isCompressed)).toBe(true); + expect(await uncompressArtifact(artifacts[0])).toStrictEqual({ + entries: translateToEndpointExceptions([exceptionListItem], 'v1'), + }); + expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[4])).toStrictEqual({ + entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), + }); + for (const artifact of artifacts) { - if (artifact.identifier === ARTIFACT_NAME_EXCEPTIONS_MACOS) { - expect(await uncompressArtifact(artifact)).toStrictEqual({ - entries: translateToEndpointExceptions([exceptionListItem], 'v1'), - }); - } else if (artifact.identifier === 'endpoint-trustlist-linux-v1') { - expect(await uncompressArtifact(artifact)).toStrictEqual({ - entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), - }); - } else { - expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] }); - } + expect(manifest.isDefaultArtifact(artifact)).toBe(true); + expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); } }); @@ -291,6 +310,7 @@ describe('ManifestManager', () => { context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, }); + context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); const oldManifest = await manifestManager.buildNewManifest(); @@ -307,20 +327,89 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); + expect(artifacts.length).toBe(5); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts.every(isCompressed)).toBe(true); + expect(artifacts[0]).toStrictEqual(oldManifest.getAllArtifacts()[0]); + expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[4])).toStrictEqual({ + entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), + }); + for (const artifact of artifacts) { - if (artifact.identifier === ARTIFACT_NAME_EXCEPTIONS_MACOS) { - expect(artifact).toStrictEqual(oldManifest.getAllArtifacts()[0]); - } else if (artifact.identifier === 'endpoint-trustlist-linux-v1') { - expect(await uncompressArtifact(artifact)).toStrictEqual({ - entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), - }); - } else { - expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] }); - } + expect(manifest.isDefaultArtifact(artifact)).toBe(true); + expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + } + }); + + test('Builds manifest with policy specific exception list items for trusted apps', async () => { + const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); + const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); + const trustedAppListItemPolicy2 = getExceptionListItemSchemaMock({ + os_types: ['linux'], + entries: [ + { field: 'other.field', operator: 'included', type: 'match', value: 'other value' }, + ], + }); + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ + [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, + [ENDPOINT_TRUSTED_APPS_LIST_ID]: { + linux: [trustedAppListItem], + [`linux-${TEST_POLICY_ID_2}`]: [trustedAppListItem, trustedAppListItemPolicy2], + }, + }); + context.packagePolicyService.listIds = mockPolicyListIdsResponse([ + TEST_POLICY_ID_1, + TEST_POLICY_ID_2, + ]); + + const manifest = await manifestManager.buildNewManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toBeUndefined(); + + const artifacts = manifest.getAllArtifacts(); + + expect(artifacts.length).toBe(6); + expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); + expect(artifacts.every(isCompressed)).toBe(true); + + expect(await uncompressArtifact(artifacts[0])).toStrictEqual({ + entries: translateToEndpointExceptions([exceptionListItem], 'v1'), + }); + expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[4])).toStrictEqual({ + entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), + }); + expect(await uncompressArtifact(artifacts[5])).toStrictEqual({ + entries: translateToEndpointExceptions( + [trustedAppListItem, trustedAppListItemPolicy2], + 'v1' + ), + }); + + for (const artifact of artifacts.slice(0, 4)) { + expect(manifest.isDefaultArtifact(artifact)).toBe(true); + expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual( + new Set([TEST_POLICY_ID_1, TEST_POLICY_ID_2]) + ); } + + expect(manifest.isDefaultArtifact(artifacts[5])).toBe(false); + expect(manifest.getArtifactTargetPolicies(artifacts[5])).toStrictEqual( + new Set([TEST_POLICY_ID_2]) + ); }); }); 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 6b9cbb55415a01..f49f2a3e226eef 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 @@ -9,6 +9,7 @@ import semver from 'semver'; import LRU from 'lru-cache'; import { isEqual } from 'lodash'; import { Logger, SavedObjectsClientContract } from 'src/core/server'; +import { ListResult } from '../../../../../../fleet/common'; import { PackagePolicyServiceInterface } from '../../../../../../fleet/server'; import { ExceptionListClient } from '../../../../../../lists/server'; import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common'; @@ -21,7 +22,8 @@ import { ArtifactConstants, buildArtifact, getArtifactId, - getFullEndpointExceptionList, + getEndpointExceptionList, + getEndpointTrustedAppsList, isCompressed, Manifest, maybeCompressArtifact, @@ -32,9 +34,45 @@ import { } from '../../../schemas/artifacts'; import { ArtifactClient } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; -import { ENDPOINT_LIST_ID } from '../../../../../../lists/common'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants'; -import { PackagePolicy } from '../../../../../../fleet/common/types/models'; + +interface ArtifactsBuildResult { + defaultArtifacts: InternalArtifactCompleteSchema[]; + policySpecificArtifacts: Record; +} + +const iterateArtifactsBuildResult = async ( + result: ArtifactsBuildResult, + callback: (artifact: InternalArtifactCompleteSchema, policyId?: string) => Promise +) => { + for (const artifact of result.defaultArtifacts) { + await callback(artifact); + } + + for (const policyId of Object.keys(result.policySpecificArtifacts)) { + for (const artifact of result.policySpecificArtifacts[policyId]) { + await callback(artifact, policyId); + } + } +}; + +const iterateAllListItems = async ( + pageSupplier: (page: number) => Promise>, + itemCallback: (item: T) => void +) => { + let paging = true; + let page = 1; + + while (paging) { + const { items, total } = await pageSupplier(page); + + for (const item of items) { + await itemCallback(item); + } + + paging = (page - 1) * 20 + items.length < total; + page++; + } +}; export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; @@ -81,6 +119,19 @@ export class ManifestManager { return new ManifestClient(this.savedObjectsClient, this.schemaVersion); } + /** + * Builds an artifact (one per supported OS) based on the current + * state of exception-list-agnostic SOs. + */ + protected async buildExceptionListArtifact(os: string): Promise { + return buildArtifact( + await getEndpointExceptionList(this.exceptionListClient, this.schemaVersion, os), + this.schemaVersion, + os, + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + } + /** * Builds an array of artifacts (one per supported OS) based on the current * state of exception-list-agnostic SOs. @@ -88,54 +139,60 @@ export class ManifestManager { * @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs. * @throws Throws/rejects if there are errors building the list. */ - protected async buildExceptionListArtifacts( - artifactSchemaVersion?: string - ): Promise { - const artifacts: InternalArtifactCompleteSchema[] = []; + protected async buildExceptionListArtifacts(): Promise { + const defaultArtifacts: InternalArtifactCompleteSchema[] = []; + const policySpecificArtifacts: Record = {}; + for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) { - const exceptionList = await getFullEndpointExceptionList( - this.exceptionListClient, - os, - artifactSchemaVersion ?? 'v1', - ENDPOINT_LIST_ID - ); - const artifact = await buildArtifact( - exceptionList, - os, - artifactSchemaVersion ?? 'v1', - ArtifactConstants.GLOBAL_ALLOWLIST_NAME - ); - artifacts.push(artifact); + defaultArtifacts.push(await this.buildExceptionListArtifact(os)); } - return artifacts; + + await iterateAllListItems( + (page) => this.listEndpointPolicyIds(page), + async (policyId) => { + policySpecificArtifacts[policyId] = defaultArtifacts; + } + ); + + return { defaultArtifacts, policySpecificArtifacts }; + } + + /** + * Builds an artifact (one per supported OS) based on the current state of the + * Trusted Apps list (which uses the `exception-list-agnostic` SO type) + */ + protected async buildTrustedAppsArtifact(os: string, policyId?: string) { + return buildArtifact( + await getEndpointTrustedAppsList(this.exceptionListClient, this.schemaVersion, os, policyId), + this.schemaVersion, + os, + ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME + ); } /** * Builds an array of artifacts (one per supported OS) based on the current state of the * Trusted Apps list (which uses the `exception-list-agnostic` SO type) - * @param artifactSchemaVersion */ - protected async buildTrustedAppsArtifacts( - artifactSchemaVersion?: string - ): Promise { - const artifacts: InternalArtifactCompleteSchema[] = []; + protected async buildTrustedAppsArtifacts(): Promise { + const defaultArtifacts: InternalArtifactCompleteSchema[] = []; + const policySpecificArtifacts: Record = {}; for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { - const trustedApps = await getFullEndpointExceptionList( - this.exceptionListClient, - os, - artifactSchemaVersion ?? 'v1', - ENDPOINT_TRUSTED_APPS_LIST_ID - ); - const artifact = await buildArtifact( - trustedApps, - os, - 'v1', - ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME - ); - artifacts.push(artifact); + defaultArtifacts.push(await this.buildTrustedAppsArtifact(os)); } - return artifacts; + + await iterateAllListItems( + (page) => this.listEndpointPolicyIds(page), + async (policyId) => { + for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { + policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; + policySpecificArtifacts[policyId].push(await this.buildTrustedAppsArtifact(os, policyId)); + } + } + ); + + return { defaultArtifacts, policySpecificArtifacts }; } /** @@ -251,32 +308,33 @@ export class ManifestManager { public async buildNewManifest( baselineManifest: Manifest = Manifest.getDefault(this.schemaVersion) ): Promise { - // Build new exception list artifacts - const artifacts = ( - await Promise.all([this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts()]) - ).flat(); + const results = await Promise.all([ + this.buildExceptionListArtifacts(), + this.buildTrustedAppsArtifacts(), + ]); - // Build new manifest const manifest = new Manifest({ schemaVersion: this.schemaVersion, semanticVersion: baselineManifest.getSemanticVersion(), soVersion: baselineManifest.getSavedObjectVersion(), }); - for (const artifact of artifacts) { - let artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact; - - if (!isCompressed(artifactToAdd)) { - artifactToAdd = await maybeCompressArtifact(artifactToAdd); + for (const result of results) { + await iterateArtifactsBuildResult(result, async (artifact, policyId) => { + let artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact; if (!isCompressed(artifactToAdd)) { - throw new Error(`Unable to compress artifact: ${getArtifactId(artifactToAdd)}`); - } else if (!internalArtifactCompleteSchema.is(artifactToAdd)) { - throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`); + artifactToAdd = await maybeCompressArtifact(artifactToAdd); + + if (!isCompressed(artifactToAdd)) { + throw new Error(`Unable to compress artifact: ${getArtifactId(artifactToAdd)}`); + } else if (!internalArtifactCompleteSchema.is(artifactToAdd)) { + throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`); + } } - } - manifest.addEntry(artifactToAdd); + manifest.addEntry(artifactToAdd, policyId); + }); } return manifest; @@ -292,49 +350,52 @@ export class ManifestManager { public async tryDispatch(manifest: Manifest): Promise { const errors: Error[] = []; - await this.forEachPolicy(async (packagePolicy) => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { id, revision, updated_at, updated_by, ...newPackagePolicy } = packagePolicy; - if (newPackagePolicy.inputs.length > 0 && newPackagePolicy.inputs[0].config !== undefined) { - const oldManifest = newPackagePolicy.inputs[0].config.artifact_manifest ?? { - value: {}, - }; - - const newManifestVersion = manifest.getSemanticVersion(); - if (semver.gt(newManifestVersion, oldManifest.value.manifest_version)) { - const serializedManifest = manifest.toPackagePolicyManifest(packagePolicy.id); - - if (!manifestDispatchSchema.is(serializedManifest)) { - errors.push(new Error(`Invalid manifest for policy ${packagePolicy.id}`)); - } else if (!manifestsEqual(serializedManifest, oldManifest.value)) { - newPackagePolicy.inputs[0].config.artifact_manifest = { value: serializedManifest }; - - try { - await this.packagePolicyService.update( - this.savedObjectsClient, - // @ts-ignore - undefined, - id, - newPackagePolicy - ); + await iterateAllListItems( + (page) => this.listEndpointPolicies(page), + async (packagePolicy) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { id, revision, updated_at, updated_by, ...newPackagePolicy } = packagePolicy; + if (newPackagePolicy.inputs.length > 0 && newPackagePolicy.inputs[0].config !== undefined) { + const oldManifest = newPackagePolicy.inputs[0].config.artifact_manifest ?? { + value: {}, + }; + + const newManifestVersion = manifest.getSemanticVersion(); + if (semver.gt(newManifestVersion, oldManifest.value.manifest_version)) { + const serializedManifest = manifest.toPackagePolicyManifest(packagePolicy.id); + + if (!manifestDispatchSchema.is(serializedManifest)) { + errors.push(new Error(`Invalid manifest for policy ${packagePolicy.id}`)); + } else if (!manifestsEqual(serializedManifest, oldManifest.value)) { + newPackagePolicy.inputs[0].config.artifact_manifest = { value: serializedManifest }; + + try { + await this.packagePolicyService.update( + this.savedObjectsClient, + // @ts-ignore + undefined, + id, + newPackagePolicy + ); + this.logger.debug( + `Updated package policy ${id} with manifest version ${manifest.getSemanticVersion()}` + ); + } catch (err) { + errors.push(err); + } + } else { this.logger.debug( - `Updated package policy ${id} with manifest version ${manifest.getSemanticVersion()}` + `No change in manifest content for package policy: ${id}. Staying on old version` ); - } catch (err) { - errors.push(err); } } else { - this.logger.debug( - `No change in manifest content for package policy: ${id}. Staying on old version` - ); + this.logger.debug(`No change in manifest version for package policy: ${id}`); } } else { - this.logger.debug(`No change in manifest version for package policy: ${id}`); + errors.push(new Error(`Package Policy ${id} has no config.`)); } - } else { - errors.push(new Error(`Package Policy ${id} has no config.`)); } - }); + ); return errors; } @@ -363,23 +424,19 @@ export class ManifestManager { this.logger.info(`Committed manifest ${manifest.getSemanticVersion()}`); } - private async forEachPolicy(callback: (policy: PackagePolicy) => Promise) { - let paging = true; - let page = 1; - - while (paging) { - const { items, total } = await this.packagePolicyService.list(this.savedObjectsClient, { - page, - perPage: 20, - kuery: 'ingest-package-policies.package.name:endpoint', - }); - - for (const packagePolicy of items) { - await callback(packagePolicy); - } + private async listEndpointPolicies(page: number) { + return this.packagePolicyService.list(this.savedObjectsClient, { + page, + perPage: 20, + kuery: 'ingest-package-policies.package.name:endpoint', + }); + } - paging = (page - 1) * 20 + items.length < total; - page++; - } + private async listEndpointPolicyIds(page: number) { + return this.packagePolicyService.listIds(this.savedObjectsClient, { + page, + perPage: 20, + kuery: 'ingest-package-policies.package.name:endpoint', + }); } } diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts index f3d553936bac53..c3a5c4e3b23cf1 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts @@ -50,6 +50,7 @@ export const hostsSchema = gql` success failure warning + unsupported } type EndpointFields { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index d1e646e73cf12c..0d6a0e63455b00 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -275,6 +275,7 @@ export enum HostPolicyResponseActionStatus { success = 'success', failure = 'failure', warning = 'warning', + unsupported = 'unsupported', } export enum TimelineType { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index dc6f7d35a6395f..cf6ea572aa8561 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -393,6 +393,7 @@ export const getResult = (): RuleAlertType => ({ threatMapping: undefined, threatLanguage: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatQuery: undefined, references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index fb62a8bc6a14b3..27231ab896b7e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -174,6 +174,7 @@ export const importRulesRoute = ( threat_query: threatQuery, threat_mapping: threatMapping, threat_language: threatLanguage, + threat_indicator_path: threatIndicatorPath, concurrent_searches: concurrentSearches, items_per_search: itemsPerSearch, threshold, @@ -239,6 +240,7 @@ export const importRulesRoute = ( threshold, threatFilters, threatIndex, + threatIndicatorPath, threatQuery, threatMapping, threatLanguage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index defff7235dcba0..45665c61ea3f04 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -150,6 +150,7 @@ export const transformAlertToRule = ( threshold: alert.params.threshold, threat_filters: alert.params.threatFilters, threat_index: alert.params.threatIndex, + threat_indicator_path: alert.params.threatIndicatorPath, threat_query: alert.params.threatQuery, threat_mapping: alert.params.threatMapping, threat_language: alert.params.threatLanguage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts index e36b7b3079eb56..1232971c7baf83 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -48,6 +48,7 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ itemsPerSearch: undefined, threatQuery: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threshold: undefined, timestampOverride: undefined, to: 'now', @@ -94,6 +95,7 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ threat: [], threatFilters: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatMapping: undefined, threatQuery: undefined, threatLanguage: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index 9726df176e93bc..3683cd377e672d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -47,6 +47,7 @@ export const createRules = async ({ threat, threatFilters, threatIndex, + threatIndicatorPath, threatLanguage, concurrentSearches, itemsPerSearch, @@ -102,6 +103,7 @@ export const createRules = async ({ */ threatFilters: threatFilters as PartialFilter[] | undefined, threatIndex, + threatIndicatorPath, threatQuery, concurrentSearches, itemsPerSearch, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index cd1935ef50c10a..0d046bb6ab211e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -56,6 +56,7 @@ export const installPrepackagedRules = ( items_per_search: itemsPerSearch, threat_query: threatQuery, threat_index: threatIndex, + threat_indicator_path: threatIndicatorPath, threshold, timestamp_override: timestampOverride, references, @@ -110,6 +111,7 @@ export const installPrepackagedRules = ( itemsPerSearch, threatQuery, threatIndex, + threatIndicatorPath, threshold, timestampOverride, references, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index 07eb665c8cbd62..22c7dcc3a86169 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -119,6 +119,7 @@ const rule: SanitizedAlert = { threshold: undefined, threatFilters: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatQuery: undefined, threatMapping: undefined, threatLanguage: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 72798c35339820..e8be32111a0e10 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -96,6 +96,7 @@ import { ThreatLanguageOrUndefined, ConcurrentSearchesOrUndefined, ItemsPerSearchOrUndefined, + ThreatIndicatorPathOrUndefined, } from '../../../../common/detection_engine/schemas/types/threat_mapping'; import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; @@ -238,6 +239,7 @@ export interface CreateRulesOptions { threshold: ThresholdOrUndefined; threatFilters: ThreatFiltersOrUndefined; threatIndex: ThreatIndexOrUndefined; + threatIndicatorPath: ThreatIndicatorPathOrUndefined; threatQuery: ThreatQueryOrUndefined; threatMapping: ThreatMappingOrUndefined; concurrentSearches: ConcurrentSearchesOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index 691ac818100a2e..e9a75af14310e9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -53,6 +53,7 @@ export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecif threatMapping: params.threat_mapping, threatLanguage: params.threat_language, threatIndex: params.threat_index, + threatIndicatorPath: params.threat_indicator_path, concurrentSearches: params.concurrent_searches, itemsPerSearch: params.items_per_search, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts index f31d6af1a0d7a3..abbcfcaa791075 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -14,6 +14,7 @@ import { threat_query, concurrentSearchesOrUndefined, itemsPerSearchOrUndefined, + threatIndicatorPathOrUndefined, } from '../../../../common/detection_engine/schemas/types/threat_mapping'; import { authorOrUndefined, @@ -116,6 +117,7 @@ const threatSpecificRuleParams = t.type({ threatMapping: threat_mapping, threatLanguage: t.union([nonEqlLanguages, t.undefined]), threatIndex: threat_index, + threatIndicatorPath: threatIndicatorPathOrUndefined, concurrentSearches: concurrentSearchesOrUndefined, itemsPerSearch: itemsPerSearchOrUndefined, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6011c67376973c..6177fc4cd46614 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -59,6 +59,7 @@ export const sampleRuleAlertParams = ( threatQuery: undefined, threatMapping: undefined, threatIndex: undefined, + threatIndicatorPath: undefined, threatLanguage: undefined, timelineId: undefined, timelineTitle: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index 5586d9e19f7c1b..8f3fda800d7266 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -170,6 +170,7 @@ export const buildRuleWithoutOverrides = ( threat_query: ruleParams.threatQuery, threat_mapping: ruleParams.threatMapping, threat_language: ruleParams.threatLanguage, + threat_indicator_path: ruleParams.threatIndicatorPath, }; return removeInternalTagsFromRule(rule); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts index 04f2b6ff799da9..5893b05a1d811b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts @@ -23,7 +23,7 @@ export const ruleStatusServiceFactoryMock = async ({ success: jest.fn(), - partialFailure: jest.fn(), + warning: jest.fn(), error: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts index f60591422e1ee6..7f2962ae0a6c89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts @@ -54,13 +54,13 @@ describe('buildRuleStatusAttributes', () => { expect(result.statusDate).toEqual(result.lastSuccessAt); }); - it('returns partial failure fields if "partial failure"', () => { + it('returns warning fields if "warning"', () => { const result = buildRuleStatusAttributes( - 'partial failure', + 'warning', 'some indices missing timestamp override field' ); expect(result).toEqual({ - status: 'partial failure', + status: 'warning', statusDate: expectIsoDateString, lastSuccessAt: expectIsoDateString, lastSuccessMessage: 'some indices missing timestamp override field', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts index f4abf9aa5ced8a..6e93ed256321e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts @@ -24,7 +24,7 @@ interface Attributes { export interface RuleStatusService { goingToRun: () => Promise; success: (message: string, attributes?: Attributes) => Promise; - partialFailure: (message: string, attributes?: Attributes) => Promise; + warning: (message: string, attributes?: Attributes) => Promise; error: (message: string, attributes?: Attributes) => Promise; } @@ -48,7 +48,7 @@ export const buildRuleStatusAttributes: ( lastSuccessMessage: message, }; } - case 'partial failure': { + case 'warning': { return { ...baseAttributes, lastSuccessAt: now, @@ -102,7 +102,7 @@ export const ruleStatusServiceFactory = async ({ }); }, - partialFailure: async (message, attributes) => { + warning: async (message, attributes) => { const [currentStatus] = await getOrCreateRuleStatuses({ alertId, ruleStatusClient, @@ -110,7 +110,7 @@ export const ruleStatusServiceFactory = async ({ await ruleStatusClient.update(currentStatus.id, { ...currentStatus.attributes, - ...buildRuleStatusAttributes('partial failure', message, attributes), + ...buildRuleStatusAttributes('warning', message, attributes), }); }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index 2599f7db49f572..da7ee8796afbfc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -52,6 +52,7 @@ export const signalSchema = schema.object({ exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threatFilters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threatIndex: schema.maybe(schema.arrayOf(schema.string())), + threatIndicatorPath: schema.maybe(schema.string()), threatQuery: schema.maybe(schema.string()), threatMapping: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threatLanguage: schema.maybe(schema.string()), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 02a0582e540f45..a79961eb716fdd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -110,7 +110,7 @@ describe('rules_notification_alert_type', () => { find: jest.fn(), goingToRun: jest.fn(), error: jest.fn(), - partialFailure: jest.fn(), + warning: jest.fn(), }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); @@ -207,7 +207,7 @@ describe('rules_notification_alert_type', () => { }); }); - it('should set a partial failure for when rules cannot read ALL provided indices', async () => { + it('should set a warning for when rules cannot read ALL provided indices', async () => { (checkPrivileges as jest.Mock).mockResolvedValueOnce({ username: 'elastic', has_all_requested: false, @@ -227,8 +227,8 @@ describe('rules_notification_alert_type', () => { }); payload.params.index = ['some*', 'myfa*', 'anotherindex*']; await alert.executor(payload); - expect(ruleStatusService.partialFailure).toHaveBeenCalled(); - expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( + expect(ruleStatusService.warning).toHaveBeenCalled(); + expect(ruleStatusService.warning.mock.calls[0][0]).toContain( 'Missing required read privileges on the following indices: ["some*"]' ); }); @@ -250,8 +250,8 @@ describe('rules_notification_alert_type', () => { }); payload.params.index = ['some*', 'myfa*']; await alert.executor(payload); - expect(ruleStatusService.partialFailure).toHaveBeenCalled(); - expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( + expect(ruleStatusService.warning).toHaveBeenCalled(); + expect(ruleStatusService.warning.mock.calls[0][0]).toContain( 'This rule may not have the required read privileges to the following indices: ["myfa*","some*"]' ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index d7fce9d83a490b..ecb36a8b050d98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -137,6 +137,7 @@ export const signalRulesAlertType = ({ threatFilters, threatQuery, threatIndex, + threatIndicatorPath, threatMapping, threatLanguage, timestampOverride, @@ -180,7 +181,7 @@ export const signalRulesAlertType = ({ logger.debug(buildRuleMessage('[+] Starting Signal Rule execution')); logger.debug(buildRuleMessage(`interval: ${interval}`)); - let wrotePartialFailureStatus = false; + let wroteWarningStatus = false; await ruleStatusService.goingToRun(); // check if rule has permissions to access given index pattern @@ -201,7 +202,7 @@ export const signalRulesAlertType = ({ }), ]); - wrotePartialFailureStatus = await flow( + wroteWarningStatus = await flow( () => tryCatch( () => @@ -508,6 +509,7 @@ export const signalRulesAlertType = ({ threatLanguage, buildRuleMessage, threatIndex, + threatIndicatorPath, concurrentSearches: concurrentSearches ?? 1, itemsPerSearch: itemsPerSearch ?? 9000, }); @@ -657,7 +659,7 @@ export const signalRulesAlertType = ({ `[+] Finished indexing ${result.createdSignalsCount} signals into ${outputIndex}` ) ); - if (!hasError && !wrotePartialFailureStatus) { + if (!hasError && !wroteWarningStatus) { await ruleStatusService.success('succeeded', { bulkCreateTimeDurations: result.bulkCreateTimes, searchAfterTimeDurations: result.searchAfterTimes, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts index b14d1482189388..4f38f2db9230a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts @@ -10,6 +10,8 @@ import { enrichSignalThreatMatches } from './enrich_signal_threat_matches'; import { getThreatList } from './get_threat_list'; import { BuildThreatEnrichmentOptions, GetMatchedThreats } from './types'; +const DEFAULT_INDICATOR_PATH = 'threat.indicator'; + export const buildThreatEnrichment = ({ buildRuleMessage, exceptionItems, @@ -18,6 +20,7 @@ export const buildThreatEnrichment = ({ services, threatFilters, threatIndex, + threatIndicatorPath, threatLanguage, threatQuery, }: BuildThreatEnrichmentOptions): SignalsEnrichment => { @@ -50,6 +53,7 @@ export const buildThreatEnrichment = ({ return threatResponse.hits.hits; }; + const defaultedIndicatorPath = threatIndicatorPath ? threatIndicatorPath : DEFAULT_INDICATOR_PATH; return (signals: SignalSearchResponse): Promise => - enrichSignalThreatMatches(signals, getMatchedThreats); + enrichSignalThreatMatches(signals, getMatchedThreats, defaultedIndicatorPath); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 7690eb5eb1d554..e45aea29c423f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -48,6 +48,7 @@ export const createThreatSignals = async ({ threatLanguage, buildRuleMessage, threatIndex, + threatIndicatorPath, name, concurrentSearches, itemsPerSearch, @@ -99,6 +100,7 @@ export const createThreatSignals = async ({ services, threatFilters, threatIndex, + threatIndicatorPath, threatLanguage, threatQuery, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index 3c0765b56ae20b..fada3141168711 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -93,6 +93,7 @@ describe('buildMatchedIndicator', () => { const indicators = buildMatchedIndicator({ queries: [], threats, + indicatorPath: 'threat.indicator', }); expect(indicators).toEqual([]); @@ -102,6 +103,7 @@ describe('buildMatchedIndicator', () => { const [indicator] = buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }); expect(get(indicator, 'matched.atomic')).toEqual('domain_1'); @@ -111,6 +113,7 @@ describe('buildMatchedIndicator', () => { const [indicator] = buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }); expect(get(indicator, 'matched.field')).toEqual('event.field'); @@ -120,6 +123,7 @@ describe('buildMatchedIndicator', () => { const [indicator] = buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }); expect(get(indicator, 'matched.type')).toEqual('type_1'); @@ -148,6 +152,7 @@ describe('buildMatchedIndicator', () => { const indicators = buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }); expect(indicators).toHaveLength(queries.length); @@ -157,6 +162,7 @@ describe('buildMatchedIndicator', () => { const indicators = buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }); expect(indicators).toEqual([ @@ -192,9 +198,9 @@ describe('buildMatchedIndicator', () => { ]; const indicators = buildMatchedIndicator({ - indicatorPath: 'custom.indicator.path', queries, threats, + indicatorPath: 'custom.indicator.path', }); expect(indicators).toEqual([ @@ -221,6 +227,7 @@ describe('buildMatchedIndicator', () => { const indicators = buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }); expect(indicators).toEqual([ @@ -245,6 +252,7 @@ describe('buildMatchedIndicator', () => { const indicators = buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }); expect(indicators).toEqual([ @@ -276,6 +284,7 @@ describe('buildMatchedIndicator', () => { const indicators = buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }); expect(indicators).toEqual([ @@ -307,6 +316,7 @@ describe('buildMatchedIndicator', () => { buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }) ).toThrowError('Expected indicator field to be an object, but found: not an object'); }); @@ -327,6 +337,7 @@ describe('buildMatchedIndicator', () => { buildMatchedIndicator({ queries, threats, + indicatorPath: 'threat.indicator', }) ).toThrowError('Expected indicator field to be an object, but found: not an object'); }); @@ -352,7 +363,11 @@ describe('enrichSignalThreatMatches', () => { it('performs no enrichment if there are no signals', async () => { const signals = getSignalsResponseMock([]); - const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + const enrichedSignals = await enrichSignalThreatMatches( + signals, + getMatchedThreats, + 'threat.indicator' + ); expect(enrichedSignals.hits.hits).toEqual([]); }); @@ -363,7 +378,11 @@ describe('enrichSignalThreatMatches', () => { matched_queries: [matchedQuery], }); const signals = getSignalsResponseMock([signalHit]); - const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + const enrichedSignals = await enrichSignalThreatMatches( + signals, + getMatchedThreats, + 'threat.indicator' + ); const [enrichedHit] = enrichedSignals.hits.hits; const indicators = get(enrichedHit._source, 'threat.indicator'); @@ -384,7 +403,11 @@ describe('enrichSignalThreatMatches', () => { matched_queries: [matchedQuery], }); const signals = getSignalsResponseMock([signalHit]); - const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + const enrichedSignals = await enrichSignalThreatMatches( + signals, + getMatchedThreats, + 'threat.indicator' + ); const [enrichedHit] = enrichedSignals.hits.hits; const indicators = get(enrichedHit._source, 'threat.indicator'); @@ -401,7 +424,11 @@ describe('enrichSignalThreatMatches', () => { matched_queries: [matchedQuery], }); const signals = getSignalsResponseMock([signalHit]); - const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + const enrichedSignals = await enrichSignalThreatMatches( + signals, + getMatchedThreats, + 'threat.indicator' + ); const [enrichedHit] = enrichedSignals.hits.hits; const indicators = get(enrichedHit._source, 'threat.indicator'); @@ -422,9 +449,53 @@ describe('enrichSignalThreatMatches', () => { matched_queries: [matchedQuery], }); const signals = getSignalsResponseMock([signalHit]); - await expect(() => enrichSignalThreatMatches(signals, getMatchedThreats)).rejects.toThrowError( - 'Expected threat field to be an object, but found: whoops' + await expect(() => + enrichSignalThreatMatches(signals, getMatchedThreats, 'threat.indicator') + ).rejects.toThrowError('Expected threat field to be an object, but found: whoops'); + }); + + it('enriches from a configured indicator path, if specified', async () => { + getMatchedThreats = async () => [ + getThreatListItemMock({ + _id: '123', + _source: { + custom_threat: { + custom_indicator: { + domain: 'custom_domain', + other: 'custom_other', + type: 'custom_type', + }, + }, + }, + }), + ]; + matchedQuery = encodeThreatMatchNamedQuery( + getNamedQueryMock({ + id: '123', + field: 'event.field', + value: 'custom_threat.custom_indicator.domain', + }) + ); + const signalHit = getSignalHitMock({ + matched_queries: [matchedQuery], + }); + const signals = getSignalsResponseMock([signalHit]); + const enrichedSignals = await enrichSignalThreatMatches( + signals, + getMatchedThreats, + 'custom_threat.custom_indicator' ); + const [enrichedHit] = enrichedSignals.hits.hits; + const indicators = get(enrichedHit._source, 'threat.indicator'); + + expect(indicators).toEqual([ + { + domain: 'custom_domain', + matched: { atomic: 'custom_domain', field: 'event.field', type: 'custom_type' }, + other: 'custom_other', + type: 'custom_type', + }, + ]); }); it('merges duplicate matched signals into a single signal with multiple indicators', async () => { @@ -455,7 +526,11 @@ describe('enrichSignalThreatMatches', () => { ], }); const signals = getSignalsResponseMock([signalHit, otherSignalHit]); - const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats); + const enrichedSignals = await enrichSignalThreatMatches( + signals, + getMatchedThreats, + 'threat.indicator' + ); expect(enrichedSignals.hits.total).toEqual(expect.objectContaining({ value: 1 })); expect(enrichedSignals.hits.hits).toHaveLength(1); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index c298ef98ebcd53..c5b032207f1c5a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -16,7 +16,6 @@ import type { } from './types'; import { extractNamedQueries } from './utils'; -const DEFAULT_INDICATOR_PATH = 'threat.indicator'; const getSignalId = (signal: SignalSourceHit): string => signal._id; export const groupAndMergeSignalMatches = (signalHits: SignalSourceHit[]): SignalSourceHit[] => { @@ -43,11 +42,11 @@ export const groupAndMergeSignalMatches = (signalHits: SignalSourceHit[]): Signa export const buildMatchedIndicator = ({ queries, threats, - indicatorPath = DEFAULT_INDICATOR_PATH, + indicatorPath, }: { queries: ThreatMatchNamedQuery[]; threats: ThreatListItem[]; - indicatorPath?: string; + indicatorPath: string; }): ThreatIndicator[] => queries.map((query) => { const matchedThreat = threats.find((threat) => threat._id === query.id); @@ -67,7 +66,8 @@ export const buildMatchedIndicator = ({ export const enrichSignalThreatMatches = async ( signals: SignalSearchResponse, - getMatchedThreats: GetMatchedThreats + getMatchedThreats: GetMatchedThreats, + indicatorPath: string ): Promise => { const signalHits = signals.hits.hits; if (signalHits.length === 0) { @@ -79,7 +79,11 @@ export const enrichSignalThreatMatches = async ( const matchedThreatIds = [...new Set(signalMatches.flat().map(({ id }) => id))]; const matchedThreats = await getMatchedThreats(matchedThreatIds); const matchedIndicators = signalMatches.map((queries) => - buildMatchedIndicator({ queries, threats: matchedThreats }) + buildMatchedIndicator({ + indicatorPath, + queries, + threats: matchedThreats, + }) ); const enrichedSignals: SignalSourceHit[] = uniqueHits.map((signalHit, i) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index b80d3faf9b61c7..a022cbbdd40428 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -21,6 +21,7 @@ import { ThreatLanguageOrUndefined, ConcurrentSearches, ItemsPerSearch, + ThreatIndicatorPathOrUndefined, } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; import { PartialFilter, RuleTypeParams } from '../../types'; import { @@ -70,6 +71,7 @@ export interface CreateThreatSignalsOptions { threatQuery: ThreatQuery; buildRuleMessage: BuildRuleMessage; threatIndex: ThreatIndex; + threatIndicatorPath: ThreatIndicatorPathOrUndefined; threatLanguage: ThreatLanguageOrUndefined; name: string; concurrentSearches: ConcurrentSearches; @@ -214,6 +216,7 @@ export interface BuildThreatEnrichmentOptions { services: AlertServices; threatFilters: PartialFilter[]; threatIndex: ThreatIndex; + threatIndicatorPath: ThreatIndicatorPathOrUndefined; threatLanguage: ThreatLanguageOrUndefined; threatQuery: ThreatQuery; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 75bd9f593a6ac1..f7e1eb7622779c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -69,7 +69,7 @@ const ruleStatusServiceMock = { find: jest.fn(), goingToRun: jest.fn(), error: jest.fn(), - partialFailure: jest.fn(), + warning: jest.fn(), }; describe('utils', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 72e5bc0c5b879b..f6bd5c8a325be1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -79,12 +79,12 @@ export const hasReadIndexPrivileges = async ( if (indexesWithReadPrivileges.length > 0 && indexesWithNoReadPrivileges.length > 0) { // some indices have read privileges others do not. - // set a partial failure status + // set a warning status const errorString = `Missing required read privileges on the following indices: ${JSON.stringify( indexesWithNoReadPrivileges )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.partialFailure(errorString); + await ruleStatusService.warning(errorString); return true; } else if ( indexesWithReadPrivileges.length === 0 && @@ -96,7 +96,7 @@ export const hasReadIndexPrivileges = async ( indexesWithNoReadPrivileges )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.partialFailure(errorString); + await ruleStatusService.warning(errorString); return true; } return false; @@ -119,7 +119,7 @@ export const hasTimestampFields = async ( inputIndices )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.error(errorString); + await ruleStatusService.warning(errorString); return true; } else if ( !wroteStatus && @@ -128,7 +128,7 @@ export const hasTimestampFields = async ( timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices != null) ) { // if there is a timestamp override and the unmapped array for the timestamp override key is not empty, - // partial failure + // warning const errorString = `The following indices are missing the ${ timestampField === '@timestamp' ? 'timestamp field "@timestamp"' @@ -139,7 +139,7 @@ export const hasTimestampFields = async ( : timestampFieldCapsResponse.body.fields[timestampField].unmapped.indices )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.partialFailure(errorString); + await ruleStatusService.warning(errorString); return true; } return wroteStatus; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 4b59fcddcb51ff..a8721d82285f25 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -47,6 +47,7 @@ import { ThreatLanguageOrUndefined, ConcurrentSearchesOrUndefined, ItemsPerSearchOrUndefined, + ThreatIndicatorPathOrUndefined, } from '../../../common/detection_engine/schemas/types/threat_mapping'; import { LegacyCallAPIOptions } from '../../../../../../src/core/server'; @@ -88,6 +89,7 @@ export interface RuleTypeParams extends AlertTypeParams { threshold: ThresholdOrUndefined; threatFilters: PartialFilter[] | undefined; threatIndex: ThreatIndexOrUndefined; + threatIndicatorPath: ThreatIndicatorPathOrUndefined; threatQuery: ThreatQueryOrUndefined; threatMapping: ThreatMappingOrUndefined; threatLanguage: ThreatLanguageOrUndefined; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_delete_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_delete_provider.tsx index 0638d3349206d6..792538a730ebe8 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_delete_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_delete_provider.tsx @@ -7,7 +7,7 @@ import React, { Fragment, useRef, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { useServices, useToastNotifications } from '../app_context'; import { deletePolicies } from '../services/http'; @@ -96,58 +96,56 @@ export const PolicyDeleteProvider: React.FunctionComponent = ({ children const isSingle = policyNames.length === 1; return ( - - - ) : ( - - ) - } - onCancel={closeModal} - onConfirm={deletePolicy} - cancelButtonText={ + - } - confirmButtonText={ + ) : ( - } - buttonColor="danger" - data-test-subj="srdeletePolicyConfirmationModal" - > - {!isSingle ? ( - -

    - -

    -
      - {policyNames.map((name) => ( -
    • {name}
    • - ))} -
    -
    - ) : null} -
    -
    + ) + } + onCancel={closeModal} + onConfirm={deletePolicy} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + data-test-subj="srdeletePolicyConfirmationModal" + > + {!isSingle ? ( + +

    + +

    +
      + {policyNames.map((name) => ( +
    • {name}
    • + ))} +
    +
    + ) : null} + ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_execute_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_execute_provider.tsx index 3fcf5a35b34551..5636ca651b6285 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_execute_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_execute_provider.tsx @@ -7,7 +7,7 @@ import React, { Fragment, useRef, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { useServices, useToastNotifications } from '../app_context'; import { executePolicy as executePolicyRequest } from '../services/http'; @@ -81,32 +81,30 @@ export const PolicyExecuteProvider: React.FunctionComponent = ({ children } return ( - - - } - onCancel={closeModal} - onConfirm={executePolicy} - cancelButtonText={ - - } - confirmButtonText={ - - } - data-test-subj="srExecutePolicyConfirmationModal" - /> - + + } + onCancel={closeModal} + onConfirm={executePolicy} + cancelButtonText={ + + } + confirmButtonText={ + + } + data-test-subj="srExecutePolicyConfirmationModal" + /> ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_delete_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_delete_provider.tsx index 3009413541111c..f02f160958a203 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_delete_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_delete_provider.tsx @@ -7,7 +7,7 @@ import React, { Fragment, useRef, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { Repository } from '../../../common/types'; import { useServices, useToastNotifications } from '../app_context'; @@ -109,79 +109,77 @@ export const RepositoryDeleteProvider: React.FunctionComponent = ({ child const isSingle = repositoryNames.length === 1; return ( - - - ) : ( - - ) - } - onCancel={closeModal} - onConfirm={deleteRepository} - cancelButtonText={ + - } - confirmButtonText={ - isSingle ? ( - - ) : ( + ) : ( + + ) + } + onCancel={closeModal} + onConfirm={deleteRepository} + cancelButtonText={ + + } + confirmButtonText={ + isSingle ? ( + + ) : ( + + ) + } + buttonColor="danger" + data-test-subj="deleteRepositoryConfirmation" + > + {isSingle ? ( +

    + +

    + ) : ( + +

    - ) - } - buttonColor="danger" - data-test-subj="deleteRepositoryConfirmation" - > - {isSingle ? ( +

    +
      + {repositoryNames.map((name) => ( +
    • {name}
    • + ))} +

    - ) : ( - -

    - -

    -
      - {repositoryNames.map((name) => ( -
    • {name}
    • - ))} -
    -

    - -

    -
    - )} -
    -
    + + )} +
    ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_execute_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_execute_modal_provider.tsx index 9366815a0256e2..4ce1d93955952f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/retention_execute_modal_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_execute_modal_provider.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { useServices, useToastNotifications } from '../app_context'; import { executeRetention as executeRetentionRequest } from '../services/http'; @@ -58,31 +58,29 @@ export const RetentionExecuteModalProvider: React.FunctionComponent = ({ } return ( - - - } - onCancel={closeModal} - onConfirm={executeRetention} - cancelButtonText={ - - } - confirmButtonText={ - - } - data-test-subj="executeRetentionModal" - /> - + + } + onCancel={closeModal} + onConfirm={executeRetention} + cancelButtonText={ + + } + confirmButtonText={ + + } + data-test-subj="executeRetentionModal" + /> ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx index d8916ce9858f80..73e19eee8bf7a7 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx @@ -8,7 +8,6 @@ import React, { Fragment, useRef, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -129,165 +128,161 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent - - - - {isEditing ? ( - - ) : ( - - )} - - - - - {saveError && ( - - - } - color="danger" - iconType="alert" - > - {saveError.data && saveError.data.message ? ( -

    {saveError.data.message}

    - ) : null} -
    - -
    + + + + {isEditing ? ( + + ) : ( + )} - {isAdvancedCronVisible ? ( - - - } - isInvalid={isInvalid} - error={i18n.translate( - 'xpack.snapshotRestore.policyForm.stepRetention.policyUpdateRetentionScheduleFieldErrorMessage', - { - defaultMessage: 'Retention schedule is required.', - } - )} - helpText={ - - - - ), - }} - /> + + + + + {saveError && ( + + + } + color="danger" + iconType="alert" + > + {saveError.data && saveError.data.message ?

    {saveError.data.message}

    : null} +
    + +
    + )} + {isAdvancedCronVisible ? ( + + + } + isInvalid={isInvalid} + error={i18n.translate( + 'xpack.snapshotRestore.policyForm.stepRetention.policyUpdateRetentionScheduleFieldErrorMessage', + { + defaultMessage: 'Retention schedule is required.', } - fullWidth - > - setRetentionSchedule(e.target.value)} + )} + helpText={ + + + + ), + }} /> - + } + fullWidth + > + setRetentionSchedule(e.target.value)} + /> + - + - - { - setIsAdvancedCronVisible(false); - setRetentionSchedule(simpleCron.expression); - }} - data-test-subj="showBasicCronLink" - > - - - - - ) : ( - - { - setSimpleCron({ - expression, - frequency, - }); - setFieldToPreferredValueMap(newFieldToPreferredValueMap); - setRetentionSchedule(expression); + + { + setIsAdvancedCronVisible(false); + setRetentionSchedule(simpleCron.expression); }} - /> - - + data-test-subj="showBasicCronLink" + > + + + + + ) : ( + + { + setSimpleCron({ + expression, + frequency, + }); + setFieldToPreferredValueMap(newFieldToPreferredValueMap); + setRetentionSchedule(expression); + }} + /> - - { - setIsAdvancedCronVisible(true); - }} - data-test-subj="showAdvancedCronLink" - > - - - - - )} -
    + - - + + { + setIsAdvancedCronVisible(true); + }} + data-test-subj="showAdvancedCronLink" + > + + + + + )} +
    + + + + + + + + {isEditing ? ( - - - - {isEditing ? ( - - ) : ( - - )} - - -
    - + ) : ( + + )} + + + ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/snapshot_delete_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/snapshot_delete_provider.tsx index 40af1b07a50bc1..74614efb314aae 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/snapshot_delete_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/snapshot_delete_provider.tsx @@ -9,7 +9,6 @@ import React, { Fragment, useRef, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, - EuiOverlayMask, EuiCallOut, EuiLoadingSpinner, EuiFlexGroup, @@ -118,95 +117,93 @@ export const SnapshotDeleteProvider: React.FunctionComponent = ({ childre const isSingle = snapshotIds.length === 1; return ( - - - ) : ( - - ) - } - onCancel={closeModal} - onConfirm={deleteSnapshot} - cancelButtonText={ + - } - confirmButtonText={ + ) : ( - } - confirmButtonDisabled={isDeleting} - buttonColor="danger" - data-test-subj="srdeleteSnapshotConfirmationModal" - > - {!isSingle ? ( - + ) + } + onCancel={closeModal} + onConfirm={deleteSnapshot} + cancelButtonText={ + + } + confirmButtonText={ + + } + confirmButtonDisabled={isDeleting} + buttonColor="danger" + data-test-subj="srdeleteSnapshotConfirmationModal" + > + {!isSingle ? ( + +

    + +

    +
      + {snapshotIds.map(({ snapshot, repository }) => ( +
    • {snapshot}
    • + ))} +
    +
    + ) : null} +

    + +

    + {!isSingle && isDeleting ? ( + + + + + + + + + + + + } + >

    -
      - {snapshotIds.map(({ snapshot, repository }) => ( -
    • {snapshot}
    • - ))} -
    -
    - ) : null} -

    - -

    - {!isSingle && isDeleting ? ( - - - - - - - - - - - - } - > -

    - -

    - - - ) : null} -
    -
    + + + ) : null} +
    ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx index e7bdde2984d6f4..823ce3a122ef12 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx @@ -281,7 +281,7 @@ export const RepositoryDetails: React.FunctionComponent = ({ {verification ? ( - + {JSON.stringify( verification.valid ? verification.response : verification.error, null, @@ -350,7 +350,7 @@ export const RepositoryDetails: React.FunctionComponent = ({ /> - + {JSON.stringify(cleanup.response, null, 2)}
    diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap index b0d0933614d125..5bf93a1021c054 100644 --- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap @@ -1,95 +1,93 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ConfirmDeleteModal renders as expected 1`] = ` - - - - + + + + + + + + +

    + + , } } /> - - - - -

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

    - - - -
    -
    - - - - - + - - - -
    -
    + + + + + + + + + + + + `; diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index c57bc1cef8fbee..94a5c082834ad7 100644 --- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx +++ b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -20,7 +20,6 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiModalProps, - EuiOverlayMask, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -97,88 +96,86 @@ class ConfirmDeleteModalUI extends Component { }; return ( - - - - + + + + + + + + +

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

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

    - - - - - - {warning} -
    -
    - - + + - - - - - - - -
    -
    + + + {warning} + + + + + + + + + + + + ); } diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap index 750afcfc44e7e3..3eb92de017927b 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap @@ -1,28 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ConfirmAlterActiveSpaceModal renders as expected 1`] = ` - - - } - > -

    - -

    -
    -
    + + } +> +

    + +

    +
    `; diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx b/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx index 1839fbdfdda7da..c95bb7250a23e1 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; @@ -16,34 +16,32 @@ interface Props { } const ConfirmAlterActiveSpaceModalUI: React.FC = (props) => ( - - - } - defaultFocusedButton={'confirm'} - cancelButtonText={props.intl.formatMessage({ - id: 'xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton', - defaultMessage: 'Cancel', - })} - confirmButtonText={props.intl.formatMessage({ - id: 'xpack.spaces.management.confirmAlterActiveSpaceModal.updateSpaceButton', - defaultMessage: 'Update space', - })} - > -

    - -

    -
    -
    + + } + defaultFocusedButton={'confirm'} + cancelButtonText={props.intl.formatMessage({ + id: 'xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton', + defaultMessage: 'Cancel', + })} + confirmButtonText={props.intl.formatMessage({ + id: 'xpack.spaces.management.confirmAlterActiveSpaceModal.updateSpaceButton', + defaultMessage: 'Update space', + })} + > +

    + +

    +
    ); export const ConfirmAlterActiveSpaceModal = injectI18n(ConfirmAlterActiveSpaceModalUI); diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts index face319f141db5..3d8d7ef4d8ae3a 100644 --- a/x-pack/plugins/transform/common/api_schemas/transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -51,13 +51,43 @@ export type PivotConfig = TypeOf; export type LatestFunctionConfig = TypeOf; +export const retentionPolicySchema = schema.object({ + time: schema.object({ + field: schema.string(), + max_age: schema.string(), + }), +}); + export const settingsSchema = schema.object({ max_page_search_size: schema.maybe(schema.number()), // The default value is null, which disables throttling. docs_per_second: schema.maybe(schema.nullable(schema.number())), }); +export const runtimeMappingsSchema = schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + type: schema.oneOf([ + schema.literal('keyword'), + schema.literal('long'), + schema.literal('double'), + schema.literal('date'), + schema.literal('ip'), + schema.literal('boolean'), + ]), + script: schema.oneOf([ + schema.string(), + schema.object({ + source: schema.string(), + }), + ]), + }) + ) +); + export const sourceSchema = schema.object({ + runtime_mappings: runtimeMappingsSchema, index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), query: schema.maybe(schema.recordOf(schema.string(), schema.any())), }); @@ -94,6 +124,7 @@ export const putTransformsRequestSchema = schema.object( * Latest and pivot are mutually exclusive, i.e. exactly one must be specified in the transform configuration */ latest: schema.maybe(latestFunctionSchema), + retention_policy: schema.maybe(retentionPolicySchema), settings: schema.maybe(settingsSchema), source: sourceSchema, sync: schema.maybe(syncSchema), diff --git a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts index 4ff9780be1f5d9..9bd4df51080490 100644 --- a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts @@ -9,7 +9,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { TransformPivotConfig } from '../types/transform'; -import { settingsSchema, sourceSchema, syncSchema } from './transforms'; +import { retentionPolicySchema, settingsSchema, sourceSchema, syncSchema } from './transforms'; // POST _transform/{transform_id}/_update export const postTransformsUpdateRequestSchema = schema.object({ @@ -22,6 +22,7 @@ export const postTransformsUpdateRequestSchema = schema.object({ }) ), frequency: schema.maybe(schema.string()), + retention_policy: schema.maybe(retentionPolicySchema), settings: schema.maybe(settingsSchema), source: schema.maybe(sourceSchema), sync: schema.maybe(syncSchema), diff --git a/x-pack/plugins/transform/common/types/transform_stats.ts b/x-pack/plugins/transform/common/types/transform_stats.ts index d280f4ce3505c7..f3b7000a424dbb 100644 --- a/x-pack/plugins/transform/common/types/transform_stats.ts +++ b/x-pack/plugins/transform/common/types/transform_stats.ts @@ -33,6 +33,8 @@ export interface TransformStats { attributes: Record; }; stats: { + delete_time_in_ms: number; + documents_deleted: number; documents_indexed: number; documents_processed: number; index_failures: number; diff --git a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts index 18b0c7dde819f2..4f23b8aa4d86fd 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts @@ -10,6 +10,7 @@ import { Dictionary } from '../../../common/types/common'; import { EsFieldName } from '../../../common/types/fields'; import { GenericAgg } from '../../../common/types/pivot_group_by'; import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; +import { PivotAggsConfigWithUiSupport } from './pivot_aggs'; export enum PIVOT_SUPPORTED_GROUP_BY_AGGS { DATE_HISTOGRAM = 'date_histogram', @@ -117,3 +118,7 @@ export function getEsAggFromGroupByConfig(groupByConfig: GroupByConfigBase): Gen [agg]: esAgg, }; } + +export function isPivotAggConfigWithUiSupport(arg: any): arg is PivotAggsConfigWithUiSupport { + return arg.hasOwnProperty('agg') && arg.hasOwnProperty('field'); +} diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index 778b2c24325f62..13e7c0a9feb7a8 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -10,7 +10,7 @@ import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; import { PivotGroupByConfig } from '../common'; import { StepDefineExposedState } from '../sections/create_transform/components/step_define'; -import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; +import { StepDetailsExposedState } from '../sections/create_transform/components/step_details'; import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by'; import { PivotAggsConfig } from './pivot_aggs'; @@ -27,6 +27,7 @@ import { PivotQuery, } from './request'; import { LatestFunctionConfigUI } from '../../../common/types/transform'; +import { RuntimeField } from '../../../../../../src/plugins/data/common/index_patterns'; const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } }; @@ -168,12 +169,18 @@ describe('Transform: Common', () => { validationStatus: { isValid: true, }, + runtimeMappings: undefined, + runtimeMappingsUpdated: false, + isRuntimeMappingsEditorEnabled: false, }; const transformDetailsState: StepDetailsExposedState = { continuousModeDateField: 'the-continuous-mode-date-field', continuousModeDelay: 'the-continuous-mode-delay', createIndexPattern: false, isContinuousModeEnabled: false, + isRetentionPolicyEnabled: false, + retentionPolicyDateField: '', + retentionPolicyMaxAge: '', transformId: 'the-transform-id', transformDescription: 'the-transform-description', transformFrequency: '1m', @@ -209,6 +216,85 @@ describe('Transform: Common', () => { }); }); + test('getCreateTransformRequestBody() with runtime mappings', () => { + const runtimeMappings = { + rt_bytes_bigger: { + type: 'double', + script: { + source: "emit(doc['bytes'].value * 2.0)", + }, + } as RuntimeField, + }; + + const pivotState: StepDefineExposedState = { + aggList: { 'the-agg-name': aggsAvg }, + groupByList: { 'the-group-by-name': groupByTerms }, + isAdvancedPivotEditorEnabled: false, + isAdvancedSourceEditorEnabled: false, + sourceConfigUpdated: false, + searchLanguage: 'kuery', + searchString: 'the-query', + searchQuery: 'the-search-query', + valid: true, + transformFunction: 'pivot', + latestConfig: {} as LatestFunctionConfigUI, + previewRequest: { + pivot: { + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, + }, + }, + validationStatus: { + isValid: true, + }, + runtimeMappings, + runtimeMappingsUpdated: false, + isRuntimeMappingsEditorEnabled: false, + }; + const transformDetailsState: StepDetailsExposedState = { + continuousModeDateField: 'the-continuous-mode-date-field', + continuousModeDelay: 'the-continuous-mode-delay', + createIndexPattern: false, + isContinuousModeEnabled: false, + isRetentionPolicyEnabled: false, + retentionPolicyDateField: '', + retentionPolicyMaxAge: '', + transformId: 'the-transform-id', + transformDescription: 'the-transform-description', + transformFrequency: '1m', + transformSettingsMaxPageSearchSize: 100, + transformSettingsDocsPerSecond: 400, + destinationIndex: 'the-destination-index', + touched: true, + valid: true, + }; + + const request = getCreateTransformRequestBody( + 'the-index-pattern-title', + pivotState, + transformDetailsState + ); + + expect(request).toEqual({ + description: 'the-transform-description', + dest: { index: 'the-destination-index' }, + frequency: '1m', + pivot: { + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, + }, + settings: { + max_page_search_size: 100, + docs_per_second: 400, + }, + source: { + index: ['the-index-pattern-title'], + query: { query_string: { default_operator: 'AND', query: 'the-search-query' } }, + runtime_mappings: runtimeMappings, + }, + }); + }); + test('getCreateTransformSettingsRequestBody() with multiple settings', () => { const transformDetailsState: Partial = { transformSettingsDocsPerSecond: 400, diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 8e535e653a380e..e4cfd0a874f0f5 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -19,7 +19,8 @@ import type { import type { SavedSearchQuery } from '../hooks/use_search_items'; import type { StepDefineExposedState } from '../sections/create_transform/components/step_define'; -import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; +import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details'; +import { isPopulatedObject } from './utils/object_utils'; export interface SimpleQuery { query_string: { @@ -57,10 +58,34 @@ export function isDefaultQuery(query: PivotQuery): boolean { return isSimpleQuery(query) && query.query_string.query === '*'; } +export function getCombinedRuntimeMappings( + indexPattern: IndexPattern | undefined, + runtimeMappings?: StepDefineExposedState['runtimeMappings'] +): StepDefineExposedState['runtimeMappings'] | undefined { + let combinedRuntimeMappings = {}; + + // Use runtime field mappings defined inline from API + if (isPopulatedObject(runtimeMappings)) { + combinedRuntimeMappings = { ...combinedRuntimeMappings, ...runtimeMappings }; + } + + // And runtime field mappings defined by index pattern + if (indexPattern !== undefined) { + const ipRuntimeMappings = indexPattern.getComputedFields().runtimeFields; + combinedRuntimeMappings = { ...combinedRuntimeMappings, ...ipRuntimeMappings }; + } + + if (isPopulatedObject(combinedRuntimeMappings)) { + return combinedRuntimeMappings; + } + return undefined; +} + export function getPreviewTransformRequestBody( indexPatternTitle: IndexPattern['title'], query: PivotQuery, - partialRequest?: StepDefineExposedState['previewRequest'] | undefined + partialRequest?: StepDefineExposedState['previewRequest'] | undefined, + runtimeMappings?: StepDefineExposedState['runtimeMappings'] ): PostTransformsPreviewRequestSchema { const index = indexPatternTitle.split(',').map((name: string) => name.trim()); @@ -68,6 +93,7 @@ export function getPreviewTransformRequestBody( source: { index, ...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}), + ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), }, ...(partialRequest ?? {}), }; @@ -95,7 +121,8 @@ export const getCreateTransformRequestBody = ( ...getPreviewTransformRequestBody( indexPatternTitle, getPivotQuery(pivotState.searchQuery), - pivotState.previewRequest + pivotState.previewRequest, + pivotState.runtimeMappings ), // conditionally add optional description ...(transformDetailsState.transformDescription !== '' @@ -119,6 +146,17 @@ export const getCreateTransformRequestBody = ( }, } : {}), + // conditionally add retention policy settings + ...(transformDetailsState.isRetentionPolicyEnabled + ? { + retention_policy: { + time: { + field: transformDetailsState.retentionPolicyDateField, + max_age: transformDetailsState.retentionPolicyMaxAge, + }, + }, + } + : {}), // conditionally add additional settings ...getCreateTransformSettingsRequestBody(transformDetailsState), }); diff --git a/x-pack/plugins/transform/public/app/common/utils/object_utils.ts b/x-pack/plugins/transform/public/app/common/utils/object_utils.ts new file mode 100644 index 00000000000000..4bbd0c1c2810fe --- /dev/null +++ b/x-pack/plugins/transform/public/app/common/utils/object_utils.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isPopulatedObject = >(arg: any): arg is T => { + return typeof arg === 'object' && arg !== null && Object.keys(arg).length > 0; +}; diff --git a/x-pack/plugins/transform/public/app/common/validators.test.ts b/x-pack/plugins/transform/public/app/common/validators.test.ts index 44126b8f3fa26f..f48039052d2034 100644 --- a/x-pack/plugins/transform/public/app/common/validators.test.ts +++ b/x-pack/plugins/transform/public/app/common/validators.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { continuousModeDelayValidator, transformFrequencyValidator } from './validators'; +import { + continuousModeDelayValidator, + parseDuration, + retentionPolicyMaxAgeValidator, + transformFrequencyValidator, +} from './validators'; describe('continuousModeDelayValidator', () => { it('should allow 0 input without unit', () => { @@ -29,6 +34,73 @@ describe('continuousModeDelayValidator', () => { }); }); +describe('parseDuration', () => { + it('should return undefined when the input is not an integer and valid time unit.', () => { + expect(parseDuration('0')).toBe(undefined); + expect(parseDuration('0.1s')).toBe(undefined); + expect(parseDuration('1.1m')).toBe(undefined); + expect(parseDuration('10.1asdf')).toBe(undefined); + }); + + it('should return parsed data for valid time units nanos|micros|ms|s|m|h|d.', () => { + expect(parseDuration('1a')).toEqual(undefined); + expect(parseDuration('1nanos')).toEqual({ + number: 1, + timeUnit: 'nanos', + }); + expect(parseDuration('1micros')).toEqual({ + number: 1, + timeUnit: 'micros', + }); + expect(parseDuration('1ms')).toEqual({ number: 1, timeUnit: 'ms' }); + expect(parseDuration('1s')).toEqual({ number: 1, timeUnit: 's' }); + expect(parseDuration('1m')).toEqual({ number: 1, timeUnit: 'm' }); + expect(parseDuration('1h')).toEqual({ number: 1, timeUnit: 'h' }); + expect(parseDuration('1d')).toEqual({ number: 1, timeUnit: 'd' }); + }); +}); + +describe('retentionPolicyMaxAgeValidator', () => { + it('should fail when the input is not an integer and valid time unit.', () => { + expect(retentionPolicyMaxAgeValidator('0')).toBe(false); + expect(retentionPolicyMaxAgeValidator('0.1s')).toBe(false); + expect(retentionPolicyMaxAgeValidator('1.1m')).toBe(false); + expect(retentionPolicyMaxAgeValidator('10.1asdf')).toBe(false); + }); + + it('should only allow values equal or above 60s.', () => { + expect(retentionPolicyMaxAgeValidator('0nanos')).toBe(false); + expect(retentionPolicyMaxAgeValidator('59999999999nanos')).toBe(false); + expect(retentionPolicyMaxAgeValidator('60000000000nanos')).toBe(true); + expect(retentionPolicyMaxAgeValidator('60000000001nanos')).toBe(true); + + expect(retentionPolicyMaxAgeValidator('0micros')).toBe(false); + expect(retentionPolicyMaxAgeValidator('59999999micros')).toBe(false); + expect(retentionPolicyMaxAgeValidator('60000000micros')).toBe(true); + expect(retentionPolicyMaxAgeValidator('60000001micros')).toBe(true); + + expect(retentionPolicyMaxAgeValidator('0ms')).toBe(false); + expect(retentionPolicyMaxAgeValidator('59999ms')).toBe(false); + expect(retentionPolicyMaxAgeValidator('60000ms')).toBe(true); + expect(retentionPolicyMaxAgeValidator('60001ms')).toBe(true); + + expect(retentionPolicyMaxAgeValidator('0s')).toBe(false); + expect(retentionPolicyMaxAgeValidator('1s')).toBe(false); + expect(retentionPolicyMaxAgeValidator('59s')).toBe(false); + expect(retentionPolicyMaxAgeValidator('60s')).toBe(true); + expect(retentionPolicyMaxAgeValidator('61s')).toBe(true); + expect(retentionPolicyMaxAgeValidator('10000s')).toBe(true); + + expect(retentionPolicyMaxAgeValidator('0m')).toBe(false); + expect(retentionPolicyMaxAgeValidator('1m')).toBe(true); + expect(retentionPolicyMaxAgeValidator('100m')).toBe(true); + + expect(retentionPolicyMaxAgeValidator('0h')).toBe(false); + expect(retentionPolicyMaxAgeValidator('1h')).toBe(true); + expect(retentionPolicyMaxAgeValidator('2h')).toBe(true); + }); +}); + describe('transformFrequencyValidator', () => { it('should fail when the input is not an integer and valid time unit.', () => { expect(transformFrequencyValidator('0')).toBe(false); diff --git a/x-pack/plugins/transform/public/app/common/validators.ts b/x-pack/plugins/transform/public/app/common/validators.ts index 125a7cd714aa52..065a6b4d1c0ca1 100644 --- a/x-pack/plugins/transform/public/app/common/validators.ts +++ b/x-pack/plugins/transform/public/app/common/validators.ts @@ -5,6 +5,9 @@ * 2.0. */ +const RETENTION_POLICY_MIN_AGE_SECONDS = 60; +const TIME_UNITS = ['nanos', 'micros', 'ms', 's', 'm', 'h', 'd']; + /** * Validates continuous mode time delay input. * Doesn't allow floating intervals. @@ -14,6 +17,78 @@ export function continuousModeDelayValidator(value: string): boolean { return value.match(/^(0|\d*(nanos|micros|ms|s|m|h|d))$/) !== null; } +/** + * Parses a duration uses a string format like `60s`. + * @param value User input value. + */ +export interface ParsedDuration { + number: number; + timeUnit: string; +} +export function parseDuration(value: string): ParsedDuration | undefined { + if (typeof value !== 'string' || value === null) { + return; + } + + // split string by groups of numbers and letters + const regexStr = value.match(/[a-z]+|[^a-z]+/gi); + + // only valid if one group of numbers and one group of letters + if (regexStr === null || (Array.isArray(regexStr) && regexStr.length !== 2)) { + return; + } + + const number = +regexStr[0]; + const timeUnit = regexStr[1]; + + // only valid if number is an integer + if (isNaN(number) || !Number.isInteger(number)) { + return; + } + + if (!TIME_UNITS.includes(timeUnit)) { + return; + } + + return { number, timeUnit }; +} + +export function isValidRetentionPolicyMaxAge({ number, timeUnit }: ParsedDuration): boolean { + // only valid if value is equal or more than 60s + // supported time units: https://www.elastic.co/guide/en/elasticsearch/reference/master/common-options.html#time-units + return ( + (timeUnit === 'nanos' && number >= RETENTION_POLICY_MIN_AGE_SECONDS * 1000000000) || + (timeUnit === 'micros' && number >= RETENTION_POLICY_MIN_AGE_SECONDS * 1000000) || + (timeUnit === 'ms' && number >= RETENTION_POLICY_MIN_AGE_SECONDS * 1000) || + (timeUnit === 's' && number >= RETENTION_POLICY_MIN_AGE_SECONDS) || + ((timeUnit === 'm' || timeUnit === 'h' || timeUnit === 'd') && number >= 1) + ); +} + +/** + * Validates retention policy max age input. + * Doesn't allow floating intervals. + * @param value User input value. Minimum of 60s. + */ +export function retentionPolicyMaxAgeValidator(value: string): boolean { + const parsedValue = parseDuration(value); + + if (parsedValue === undefined) { + return false; + } + + return isValidRetentionPolicyMaxAge(parsedValue); +} + +// only valid if value is up to 1 hour +export function isValidFrequency({ number, timeUnit }: ParsedDuration): boolean { + return ( + (timeUnit === 's' && number <= 3600) || + (timeUnit === 'm' && number <= 60) || + (timeUnit === 'h' && number === 1) + ); +} + /** * Validates transform frequency input. * Allows time units of s/m/h only. @@ -33,20 +108,15 @@ export const transformFrequencyValidator = (value: string): boolean => { return false; } - const valueNumber = +regexStr[0]; - const valueTimeUnit = regexStr[1]; + const number = +regexStr[0]; + const timeUnit = regexStr[1]; // only valid if number is an integer above 0 - if (isNaN(valueNumber) || !Number.isInteger(valueNumber) || valueNumber === 0) { + if (isNaN(number) || !Number.isInteger(number) || number === 0) { return false; } - // only valid if value is up to 1 hour - return ( - (valueTimeUnit === 's' && valueNumber <= 3600) || - (valueTimeUnit === 'm' && valueNumber <= 60) || - (valueTimeUnit === 'h' && valueNumber === 1) - ); + return isValidFrequency({ number, timeUnit }); }; /** diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index d7a760503a00cb..bd361afac2d8d7 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -25,6 +25,7 @@ jest.mock('./use_api'); import { useAppDependencies } from '../__mocks__/app_dependencies'; import { MlSharedContext } from '../__mocks__/shared_context'; +import { RuntimeField } from '../../../../../../src/plugins/data/common/index_patterns'; const query: SimpleQuery = { query_string: { @@ -33,13 +34,21 @@ const query: SimpleQuery = { }, }; +const runtimeMappings = { + rt_bytes_bigger: { + type: 'double', + script: { + source: "emit(doc['bytes'].value * 2.0)", + }, + } as RuntimeField, +}; + describe('Transform: useIndexData()', () => { test('indexPattern set triggers loading', async () => { const mlShared = await getMlSharedImports(); const wrapper: FC = ({ children }) => ( {children} ); - const { result, waitForNextUpdate } = renderHook( () => useIndexData( @@ -48,7 +57,8 @@ describe('Transform: useIndexData()', () => { title: 'the-title', fields: [], } as unknown) as SearchItems['indexPattern'], - query + query, + runtimeMappings ), { wrapper } ); @@ -77,7 +87,7 @@ describe('Transform: with useIndexData()', () => { ml: { DataGrid }, } = useAppDependencies(); const props = { - ...useIndexData(indexPattern, { match_all: {} }), + ...useIndexData(indexPattern, { match_all: {} }, runtimeMappings), copyToClipboard: 'the-copy-to-clipboard-code', copyToClipboardDescription: 'the-copy-to-clipboard-description', dataTestSubj: 'the-data-test-subj', diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index ff2d5d2a8d71c5..abc63d886dbcc3 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; @@ -21,10 +21,12 @@ import { SearchItems } from './use_search_items'; import { useApi } from './use_api'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; +import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common'; export const useIndexData = ( indexPattern: SearchItems['indexPattern'], - query: PivotQuery + query: PivotQuery, + combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'] ): UseIndexDataReturnType => { const api = useApi(); const toastNotifications = useToastNotifications(); @@ -32,6 +34,7 @@ export const useIndexData = ( ml: { getFieldType, getDataGridSchemaFromKibanaFieldType, + getDataGridSchemaFromESFieldType, getFieldsFromKibanaIndexPattern, showDataGridColumnChartErrorMessageToast, useDataGrid, @@ -43,14 +46,37 @@ export const useIndexData = ( const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); - // EuiDataGrid State - const columns: EuiDataGridColumn[] = [ - ...indexPatternFields.map((id) => { + const columns: EuiDataGridColumn[] = useMemo(() => { + let result: Array<{ id: string; schema: string | undefined }> = []; + + // Get the the runtime fields that are defined from API field and index patterns + if (combinedRuntimeMappings !== undefined) { + result = Object.keys(combinedRuntimeMappings).map((fieldName) => { + const field = combinedRuntimeMappings[fieldName]; + const schema = getDataGridSchemaFromESFieldType(field.type); + return { id: fieldName, schema }; + }); + } + + // Combine the runtime field that are defined from API field + indexPatternFields.forEach((id) => { const field = indexPattern.fields.getByName(id); - const schema = getDataGridSchemaFromKibanaFieldType(field); - return { id, schema }; - }), - ]; + if (!field?.runtimeField) { + const schema = getDataGridSchemaFromKibanaFieldType(field); + result.push({ id, schema }); + } + }); + + return result.sort((a, b) => a.id.localeCompare(b.id)); + }, [ + indexPatternFields, + indexPattern.fields, + combinedRuntimeMappings, + getDataGridSchemaFromESFieldType, + getDataGridSchemaFromKibanaFieldType, + ]); + + // EuiDataGrid State const dataGrid = useDataGrid(columns); @@ -92,9 +118,12 @@ export const useIndexData = ( from: pagination.pageIndex * pagination.pageSize, size: pagination.pageSize, ...(Object.keys(sort).length > 0 ? { sort } : {}), + ...(typeof combinedRuntimeMappings === 'object' && + Object.keys(combinedRuntimeMappings).length > 0 + ? { runtime_mappings: combinedRuntimeMappings } + : {}), }, }; - const resp = await api.esSearch(esSearchRequest); if (!isEsSearchResponse(resp)) { @@ -134,7 +163,17 @@ export const useIndexData = ( fetchDataGridData(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + }, [ + indexPattern.title, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify([ + query, + pagination, + sortingColumns, + indexPatternFields, + combinedRuntimeMappings, + ]), + ]); useEffect(() => { if (chartsVisible) { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index 673d8d38aa8fd1..62b3a077df5e61 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -71,7 +71,8 @@ export const usePivotData = ( indexPatternTitle: SearchItems['indexPattern']['title'], query: PivotQuery, validationStatus: StepDefineExposedState['validationStatus'], - requestPayload: StepDefineExposedState['previewRequest'] + requestPayload: StepDefineExposedState['previewRequest'], + combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'] ): UseIndexDataReturnType => { const [ previewMappingsProperties, @@ -79,7 +80,13 @@ export const usePivotData = ( ] = useState({}); const api = useApi(); const { - ml: { formatHumanReadableDateTimeSeconds, multiColumnSortFactory, useDataGrid, INDEX_STATUS }, + ml: { + getDataGridSchemaFromESFieldType, + formatHumanReadableDateTimeSeconds, + multiColumnSortFactory, + useDataGrid, + INDEX_STATUS, + }, } = useAppDependencies(); // Filters mapping properties of type `object`, which get returned for nested field parents. @@ -97,38 +104,7 @@ export const usePivotData = ( // EuiDataGrid State const columns: EuiDataGridColumn[] = columnKeys.map((id) => { const field = previewMappingsProperties[id]; - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case ES_FIELD_TYPES.GEO_POINT: - case ES_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case ES_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case ES_FIELD_TYPES.DATE: - case ES_FIELD_TYPES.DATE_NANOS: - schema = 'datetime'; - break; - case ES_FIELD_TYPES.BYTE: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.SHORT: - schema = 'numeric'; - break; - // keep schema undefined for text based columns - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.TEXT: - break; - } + const schema = getDataGridSchemaFromESFieldType(field?.type); return { id, schema }; }); @@ -159,7 +135,12 @@ export const usePivotData = ( setNoDataMessage(''); setStatus(INDEX_STATUS.LOADING); - const previewRequest = getPreviewTransformRequestBody(indexPatternTitle, query, requestPayload); + const previewRequest = getPreviewTransformRequestBody( + indexPatternTitle, + query, + requestPayload, + combinedRuntimeMappings + ); const resp = await api.getTransformsPreview(previewRequest); if (!isPostTransformsPreviewResponseSchema(resp)) { @@ -196,11 +177,7 @@ export const usePivotData = ( getPreviewData(); // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ - }, [ - indexPatternTitle, - JSON.stringify([requestPayload, query]), - /* eslint-enable react-hooks/exhaustive-deps */ - ]); + }, [indexPatternTitle, JSON.stringify([requestPayload, query, combinedRuntimeMappings])]); if (sortingColumns.length > 0) { tableItems.sort(multiColumnSortFactory(sortingColumns)); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx new file mode 100644 index 00000000000000..087bae97e287ef --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.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 { isEqual } from 'lodash'; +import React, { memo, FC } from 'react'; + +import { EuiCodeEditor } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { StepDefineFormHook } from '../step_define'; + +export const AdvancedRuntimeMappingsEditor: FC = memo( + ({ + actions: { + convertToJson, + setAdvancedRuntimeMappingsConfig, + setRuntimeMappingsEditorApplyButtonEnabled, + }, + state: { advancedEditorRuntimeMappingsLastApplied, advancedRuntimeMappingsConfig, xJsonMode }, + }) => { + return ( + { + setAdvancedRuntimeMappingsConfig(d); + + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorRuntimeMappingsLastApplied === d) { + setRuntimeMappingsEditorApplyButtonEnabled(false); + return; + } + + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + JSON.parse(convertToJson(d)); + setRuntimeMappingsEditorApplyButtonEnabled(true); + } catch (e) { + setRuntimeMappingsEditorApplyButtonEnabled(false); + } + }} + setOptions={{ + fontSize: '12px', + }} + theme="textmate" + aria-label={i18n.translate('xpack.transform.stepDefineForm.advancedEditorAriaLabel', { + defaultMessage: 'Advanced pivot editor', + })} + /> + ); + }, + (prevProps, nextProps) => isEqual(pickProps(prevProps), pickProps(nextProps)) +); + +function pickProps(props: StepDefineFormHook['runtimeMappingsEditor']) { + return [ + props.state.advancedEditorRuntimeMappingsLastApplied, + props.state.advancedRuntimeMappingsConfig, + ]; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx new file mode 100644 index 00000000000000..be297c10a8f88c --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { EuiSwitch } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { StepDefineFormHook } from '../step_define'; + +export const AdvancedRuntimeMappingsEditorSwitch: FC< + StepDefineFormHook['runtimeMappingsEditor'] +> = (props) => { + const { + actions: { setRuntimeMappingsUpdated, toggleRuntimeMappingsEditor }, + state: { isRuntimeMappingsEditorEnabled }, + } = props; + + // If switching to KQL after updating via editor - reset search + const toggleEditorHandler = (reset = false) => { + if (reset === true) { + setRuntimeMappingsUpdated(false); + } + toggleRuntimeMappingsEditor(reset); + }; + + return ( + toggleEditorHandler()} + data-test-subj="transformAdvancedRuntimeMappingsEditorSwitch" + /> + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/index.ts new file mode 100644 index 00000000000000..89a05690cab52d --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AdvancedRuntimeMappingsEditorSwitch } from './advanced_runtime_mappings_editor_switch'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx new file mode 100644 index 00000000000000..f3c121a86cdc1b --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { StepDefineFormHook } from '../step_define'; +import { AdvancedRuntimeMappingsEditor } from '../advanced_runtime_mappings_editor/advanced_runtime_mappings_editor'; +import { AdvancedRuntimeMappingsEditorSwitch } from '../advanced_runtime_mappings_editor_switch'; +import { + isPivotGroupByConfigWithUiSupport, + PivotAggsConfigWithUiSupport, +} from '../../../../common'; +import { isPivotAggConfigWithUiSupport } from '../../../../common/pivot_group_by'; + +const advancedEditorsSidebarWidth = '220px'; +const COPY_TO_CLIPBOARD_RUNTIME_MAPPINGS = i18n.translate( + 'xpack.transform.indexPreview.copyRuntimeMappingsClipboardTooltip', + { + defaultMessage: 'Copy Dev Console statement of the runtime mappings to the clipboard.', + } +); + +export const AdvancedRuntimeMappingsSettings: FC = (props) => { + const { + actions: { applyRuntimeMappingsEditorChanges }, + state: { + runtimeMappings, + advancedRuntimeMappingsConfig, + isRuntimeMappingsEditorApplyButtonEnabled, + isRuntimeMappingsEditorEnabled, + }, + } = props.runtimeMappingsEditor; + const { + actions: { deleteAggregation, deleteGroupBy }, + state: { groupByList, aggList }, + } = props.pivotConfig; + + const applyChanges = () => { + const nextConfig = JSON.parse(advancedRuntimeMappingsConfig); + const previousConfig = runtimeMappings; + + applyRuntimeMappingsEditorChanges(); + + // If the user updates the name of the runtime mapping fields + // delete any groupBy or aggregation associated with the deleted field + Object.keys(groupByList).forEach((groupByKey) => { + const groupBy = groupByList[groupByKey]; + if ( + isPivotGroupByConfigWithUiSupport(groupBy) && + previousConfig?.hasOwnProperty(groupBy.field) && + !nextConfig.hasOwnProperty(groupBy.field) + ) { + deleteGroupBy(groupByKey); + } + }); + Object.keys(aggList).forEach((aggName) => { + const agg = aggList[aggName] as PivotAggsConfigWithUiSupport; + if ( + isPivotAggConfigWithUiSupport(agg) && + agg.field !== undefined && + previousConfig?.hasOwnProperty(agg.field) && + !nextConfig.hasOwnProperty(agg.field) + ) { + deleteAggregation(aggName); + } + }); + }; + return ( + <> + + + + + + {runtimeMappings !== undefined && Object.keys(runtimeMappings).length > 0 ? ( + + ) : ( + + )} + + {isRuntimeMappingsEditorEnabled && ( + <> + + + + )} + + + + + + + + + + + + {(copy: () => void) => ( + + )} + + + + + + {isRuntimeMappingsEditorEnabled && ( + + + + {i18n.translate( + 'xpack.transform.stepDefineForm.advancedRuntimeMappingsEditorHelpText', + { + defaultMessage: + 'The advanced editor allows you to edit the runtime mappings of the transform configuration.', + } + )} + + + + {i18n.translate( + 'xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText', + { + defaultMessage: 'Apply changes', + } + )} + + + )} + + + + + + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/index.ts new file mode 100644 index 00000000000000..69b3bc36a559e9 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AdvancedRuntimeMappingsSettings } from './advanced_runtime_mappings_settings'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 807830d749892d..34832ec968e296 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -46,6 +46,9 @@ import { PutTransformsLatestRequestSchema, PutTransformsPivotRequestSchema, } from '../../../../../../common/api_schemas/transforms'; +import type { RuntimeField } from '../../../../../../../../../src/plugins/data/common/index_patterns'; +import { isPopulatedObject } from '../../../../common/utils/object_utils'; +import { isLatestTransform } from '../../../../../../common/types/transform'; export interface StepDetailsExposedState { created: boolean; @@ -189,12 +192,19 @@ export const StepCreateForm: FC = React.memo( const createKibanaIndexPattern = async () => { setLoading(true); const indexPatternName = transformConfig.dest.index; + const runtimeMappings = transformConfig.source.runtime_mappings as Record< + string, + RuntimeField + >; try { const newIndexPattern = await indexPatterns.createAndSave( { title: indexPatternName, timeFieldName, + ...(isPopulatedObject(runtimeMappings) && isLatestTransform(transformConfig) + ? { runtimeFieldMap: runtimeMappings } + : {}), }, false, true diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts index 77b60b6f5966af..6298874a203666 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts @@ -30,12 +30,19 @@ import { TRANSFORM_FUNCTION } from '../../../../../../../common/constants'; import { StepDefineFormProps } from '../step_define_form'; import { validateLatestConfig } from '../hooks/use_latest_function_config'; import { validatePivotConfig } from '../hooks/use_pivot_config'; +import { getCombinedRuntimeMappings } from '../../../../../common/request'; export function applyTransformConfigToDefineState( state: StepDefineExposedState, transformConfig?: TransformBaseConfig, indexPattern?: StepDefineFormProps['searchItems']['indexPattern'] ): StepDefineExposedState { + // apply runtime mappings from both the index pattern and inline configurations + state.runtimeMappings = getCombinedRuntimeMappings( + indexPattern, + transformConfig?.source?.runtime_mappings + ); + if (transformConfig === undefined) { return state; } @@ -107,6 +114,5 @@ export function applyTransformConfigToDefineState( // applying a transform config to wizard state will always result in a valid configuration state.valid = true; - return state; } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts index deaaddc44ba7ab..fcdbac8c7ff39c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts @@ -8,6 +8,7 @@ import { getPivotDropdownOptions } from '../common'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { FilterAggForm } from './filter_agg/components'; +import type { RuntimeField } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; describe('Transform: Define Pivot Common', () => { test('getPivotDropdownOptions()', () => { @@ -109,5 +110,169 @@ describe('Transform: Define Pivot Common', () => { }, }, }); + + const runtimeMappings = { + rt_bytes_bigger: { + type: 'double', + script: { + source: "emit(doc['bytes'].value * 2.0)", + }, + } as RuntimeField, + }; + const optionsWithRuntimeFields = getPivotDropdownOptions(indexPattern, runtimeMappings); + expect(optionsWithRuntimeFields).toMatchObject({ + aggOptions: [ + { + label: ' the-f[i]e>ld ', + options: [ + { label: 'avg( the-f[i]e>ld )' }, + { label: 'cardinality( the-f[i]e>ld )' }, + { label: 'max( the-f[i]e>ld )' }, + { label: 'min( the-f[i]e>ld )' }, + { label: 'percentiles( the-f[i]e>ld )' }, + { label: 'sum( the-f[i]e>ld )' }, + { label: 'value_count( the-f[i]e>ld )' }, + { label: 'filter( the-f[i]e>ld )' }, + ], + }, + { + label: 'rt_bytes_bigger', + options: [ + { label: 'avg(rt_bytes_bigger)' }, + { label: 'cardinality(rt_bytes_bigger)' }, + { label: 'max(rt_bytes_bigger)' }, + { label: 'min(rt_bytes_bigger)' }, + { label: 'percentiles(rt_bytes_bigger)' }, + { label: 'sum(rt_bytes_bigger)' }, + { label: 'value_count(rt_bytes_bigger)' }, + { label: 'filter(rt_bytes_bigger)' }, + ], + }, + ], + aggOptionsData: { + 'avg( the-f[i]e>ld )': { + agg: 'avg', + aggName: 'the-field.avg', + dropDownName: 'avg( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + }, + 'cardinality( the-f[i]e>ld )': { + agg: 'cardinality', + aggName: 'the-field.cardinality', + dropDownName: 'cardinality( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + }, + 'max( the-f[i]e>ld )': { + agg: 'max', + aggName: 'the-field.max', + dropDownName: 'max( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + }, + 'min( the-f[i]e>ld )': { + agg: 'min', + aggName: 'the-field.min', + dropDownName: 'min( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + }, + 'percentiles( the-f[i]e>ld )': { + agg: 'percentiles', + aggName: 'the-field.percentiles', + dropDownName: 'percentiles( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + percents: [1, 5, 25, 50, 75, 95, 99], + }, + 'sum( the-f[i]e>ld )': { + agg: 'sum', + aggName: 'the-field.sum', + dropDownName: 'sum( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + }, + 'value_count( the-f[i]e>ld )': { + agg: 'value_count', + aggName: 'the-field.value_count', + dropDownName: 'value_count( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + }, + 'filter( the-f[i]e>ld )': { + agg: 'filter', + aggName: 'the-field.filter', + dropDownName: 'filter( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + isSubAggsSupported: true, + AggFormComponent: FilterAggForm, + }, + 'avg(rt_bytes_bigger)': { + agg: 'avg', + aggName: 'rt_bytes_bigger.avg', + dropDownName: 'avg(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + }, + 'cardinality(rt_bytes_bigger)': { + agg: 'cardinality', + aggName: 'rt_bytes_bigger.cardinality', + dropDownName: 'cardinality(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + }, + 'max(rt_bytes_bigger)': { + agg: 'max', + aggName: 'rt_bytes_bigger.max', + dropDownName: 'max(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + }, + 'min(rt_bytes_bigger)': { + agg: 'min', + aggName: 'rt_bytes_bigger.min', + dropDownName: 'min(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + }, + 'percentiles(rt_bytes_bigger)': { + agg: 'percentiles', + aggName: 'rt_bytes_bigger.percentiles', + dropDownName: 'percentiles(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + percents: [1, 5, 25, 50, 75, 95, 99], + }, + 'sum(rt_bytes_bigger)': { + agg: 'sum', + aggName: 'rt_bytes_bigger.sum', + dropDownName: 'sum(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + }, + 'value_count(rt_bytes_bigger)': { + agg: 'value_count', + aggName: 'rt_bytes_bigger.value_count', + dropDownName: 'value_count(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + }, + 'filter(rt_bytes_bigger)': { + agg: 'filter', + aggName: 'rt_bytes_bigger.filter', + dropDownName: 'filter(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + isSubAggsSupported: true, + AggFormComponent: FilterAggForm, + }, + }, + groupByOptions: [ + { label: 'histogram( the-f[i]e>ld )' }, + { label: 'histogram(rt_bytes_bigger)' }, + ], + groupByOptionsData: { + 'histogram( the-f[i]e>ld )': { + agg: 'histogram', + aggName: 'the-field', + dropDownName: 'histogram( the-f[i]e>ld )', + field: ' the-f[i]e>ld ', + interval: '10', + }, + 'histogram(rt_bytes_bigger)': { + agg: 'histogram', + aggName: 'rt_bytes_bigger', + dropDownName: 'histogram(rt_bytes_bigger)', + field: 'rt_bytes_bigger', + interval: '10', + }, + }, + }); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx index dae8f61aaa4dff..7f9c4256f77557 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx @@ -10,11 +10,23 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { FilterAggForm } from './filter_agg_form'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; -import { KBN_FIELD_TYPES } from '../../../../../../../../../../../../src/plugins/data/common'; +import { + KBN_FIELD_TYPES, + RuntimeField, +} from '../../../../../../../../../../../../src/plugins/data/common'; import { IndexPattern } from '../../../../../../../../../../../../src/plugins/data/public'; import { FilterTermForm } from './filter_term_form'; describe('FilterAggForm', () => { + const runtimeMappings = { + rt_bytes_bigger: { + type: 'double', + script: { + source: "emit(doc['bytes'].value * 2.0)", + }, + } as RuntimeField, + }; + const indexPattern = ({ fields: { getByName: jest.fn((fieldName: string) => { @@ -37,7 +49,7 @@ describe('FilterAggForm', () => { const { getByLabelText, findByTestId, container } = render( - + @@ -62,7 +74,7 @@ describe('FilterAggForm', () => { const { findByTestId } = render( - + @@ -90,7 +102,7 @@ describe('FilterAggForm', () => { const { rerender, findByTestId } = render( - + @@ -99,7 +111,7 @@ describe('FilterAggForm', () => { // re-render the same component with different props rerender( - + @@ -127,7 +139,7 @@ describe('FilterAggForm', () => { const { findByTestId, container } = render( - + { - const { indexPattern } = useContext(CreateTransformWizardContext); + const { indexPattern, runtimeMappings } = useContext(CreateTransformWizardContext); - const filterAggsOptions = useMemo(() => getSupportedFilterAggs(selectedField, indexPattern!), [ - indexPattern, - selectedField, - ]); + const filterAggsOptions = useMemo( + () => getSupportedFilterAggs(selectedField, indexPattern!, runtimeMappings), + [indexPattern, selectedField, runtimeMappings] + ); useUpdateEffect(() => { // reset filter agg on field change diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx index 2e9ad761d3b790..67c904946d302b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx @@ -17,6 +17,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { FilterAggConfigRange } from '../types'; +const BUTTON_SIZE = 40; /** * Form component for the range filter aggregation for number type fields. */ @@ -45,7 +46,7 @@ export const FilterRangeForm: FilterAggConfigRange['aggTypeConfig']['FilterAggFo return ( <> - + { updateConfig({ includeFrom: e.target.checked }); }} @@ -94,13 +96,14 @@ export const FilterRangeForm: FilterAggConfigRange['aggTypeConfig']['FilterAggFo step="any" append={ { updateConfig({ includeTo: !includeTo }); }} fill={includeTo} > - {includeTo ? '≤' : '<'}s + {includeTo ? '≤' : '<'} } /> diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx index ad06cfb31a62f1..f2db6167c163c6 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx @@ -26,7 +26,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm selectedField, }) => { const api = useApi(); - const { indexPattern } = useContext(CreateTransformWizardContext); + const { indexPattern, runtimeMappings } = useContext(CreateTransformWizardContext); const toastNotifications = useToastNotifications(); const [options, setOptions] = useState([]); @@ -38,6 +38,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm const esSearchRequest = { index: indexPattern!.title, body: { + ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), query: { wildcard: { [selectedField!]: { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts index d3b1df41b3cfbd..c75da651f79d0d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts @@ -30,5 +30,8 @@ export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineE isValid: false, }, previewRequest: undefined, + runtimeMappings: undefined, + runtimeMappingsUpdated: false, + isRuntimeMappingsEditorEnabled: false, }; } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts index 6845d096a2e022..c88b604989680a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts @@ -7,6 +7,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { + ES_FIELD_TYPES, IndexPattern, KBN_FIELD_TYPES, } from '../../../../../../../../../../src/plugins/data/public'; @@ -24,11 +25,40 @@ import { import { getDefaultAggregationConfig } from './get_default_aggregation_config'; import { getDefaultGroupByConfig } from './get_default_group_by_config'; -import { Field } from './types'; +import type { Field, StepDefineExposedState } from './types'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; const illegalEsAggNameChars = /[[\]>]/g; -export function getPivotDropdownOptions(indexPattern: IndexPattern) { +export function getKibanaFieldTypeFromEsType(type: string): KBN_FIELD_TYPES { + switch (type) { + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SHORT: + case ES_FIELD_TYPES.UNSIGNED_LONG: + return KBN_FIELD_TYPES.NUMBER; + + case ES_FIELD_TYPES.DATE: + case ES_FIELD_TYPES.DATE_NANOS: + return KBN_FIELD_TYPES.DATE; + + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.STRING: + return KBN_FIELD_TYPES.STRING; + + default: + return type as KBN_FIELD_TYPES; + } +} + +export function getPivotDropdownOptions( + indexPattern: IndexPattern, + runtimeMappings?: StepDefineExposedState['runtimeMappings'] +) { // The available group by options const groupByOptions: EuiComboBoxOptionOption[] = []; const groupByOptionsData: PivotGroupByConfigWithUiSupportDict = {}; @@ -38,11 +68,26 @@ export function getPivotDropdownOptions(indexPattern: IndexPattern) { const aggOptionsData: PivotAggsConfigWithUiSupportDict = {}; const ignoreFieldNames = ['_id', '_index', '_type']; - const fields = indexPattern.fields - .filter((field) => field.aggregatable === true && !ignoreFieldNames.includes(field.name)) + const indexPatternFields = indexPattern.fields + .filter( + (field) => + field.aggregatable === true && !ignoreFieldNames.includes(field.name) && !field.runtimeField + ) .map((field): Field => ({ name: field.name, type: field.type as KBN_FIELD_TYPES })); - fields.forEach((field) => { + // Support for runtime_mappings that are defined by queries + let runtimeFields: Field[] = []; + if (isPopulatedObject(runtimeMappings)) { + runtimeFields = Object.keys(runtimeMappings).map((fieldName) => { + const field = runtimeMappings[fieldName]; + return { name: fieldName, type: getKibanaFieldTypeFromEsType(field.type) }; + }); + } + + const sortByLabel = (a: Field, b: Field) => a.name.localeCompare(b.name); + + const combinedFields = [...indexPatternFields, ...runtimeFields].sort(sortByLabel); + combinedFields.forEach((field) => { // Group by const availableGroupByAggs: [] = getNestedProperty(pivotGroupByFieldSupport, field.type); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index d1325e4af5ce74..cdba7a3f5482c9 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -9,7 +9,11 @@ import { KBN_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/ import { EsFieldName } from '../../../../../../../common/types/fields'; -import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; +import { + PivotAggsConfigDict, + PivotGroupByConfigDict, + PivotGroupByConfigWithUiSupportDict, +} from '../../../../../common'; import { SavedSearchQuery } from '../../../../../hooks/use_search_items'; import { QUERY_LANGUAGE } from './constants'; @@ -30,10 +34,24 @@ export interface Field { type: KBN_FIELD_TYPES; } +// Replace this with import once #88995 is merged +const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export interface RuntimeField { + type: RuntimeType; + script: + | string + | { + source: string; + }; +} + +export type RuntimeMappings = Record; export interface StepDefineExposedState { transformFunction: TransformFunction; aggList: PivotAggsConfigDict; - groupByList: PivotGroupByConfigDict; + groupByList: PivotGroupByConfigDict | PivotGroupByConfigWithUiSupportDict; latestConfig: LatestFunctionConfigUI; isAdvancedPivotEditorEnabled: boolean; isAdvancedSourceEditorEnabled: boolean; @@ -47,6 +65,9 @@ export interface StepDefineExposedState { * Undefined when the form is incomplete or invalid */ previewRequest: { latest: LatestFunctionConfig } | { pivot: PivotConfigDefinition } | undefined; + runtimeMappings?: RuntimeMappings; + runtimeMappingsUpdated: boolean; + isRuntimeMappingsEditorEnabled: boolean; } export function isPivotPartialRequest(arg: any): arg is { pivot: PivotConfigDefinition } { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts new file mode 100644 index 00000000000000..9bb5f91ae03c79 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState } from 'react'; +import { XJsonMode } from '@kbn/ace'; +import { StepDefineExposedState } from '../common'; +import { XJson } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; + +const { useXJsonMode } = XJson; +const xJsonMode = new XJsonMode(); + +export const useAdvancedRuntimeMappingsEditor = (defaults: StepDefineExposedState) => { + const stringifiedRuntimeMappings = JSON.stringify(defaults.runtimeMappings, null, 2); + + // Advanced editor for source config state + const [runtimeMappingsUpdated, setRuntimeMappingsUpdated] = useState( + defaults.runtimeMappingsUpdated + ); + const [runtimeMappings, setRuntimeMappings] = useState(defaults.runtimeMappings); + + const [ + isRuntimeMappingsEditorSwitchModalVisible, + setRuntimeMappingsEditorSwitchModalVisible, + ] = useState(false); + + const [isRuntimeMappingsEditorEnabled, setRuntimeMappingsEditorEnabled] = useState( + defaults.isRuntimeMappingsEditorEnabled + ); + + const [ + isRuntimeMappingsEditorApplyButtonEnabled, + setRuntimeMappingsEditorApplyButtonEnabled, + ] = useState(false); + + const [ + advancedEditorRuntimeMappingsLastApplied, + setAdvancedEditorRuntimeMappingsLastApplied, + ] = useState(stringifiedRuntimeMappings); + + const [advancedEditorRuntimeMappings, setAdvancedEditorRuntimeMappings] = useState( + stringifiedRuntimeMappings + ); + + const { + convertToJson, + setXJson: setAdvancedRuntimeMappingsConfig, + xJson: advancedRuntimeMappingsConfig, + } = useXJsonMode(stringifiedRuntimeMappings ?? ''); + + const applyRuntimeMappingsEditorChanges = () => { + const parsedRuntimeMappings = JSON.parse(advancedRuntimeMappingsConfig); + const prettySourceConfig = JSON.stringify(parsedRuntimeMappings, null, 2); + setRuntimeMappingsUpdated(true); + setRuntimeMappings(parsedRuntimeMappings); + setAdvancedEditorRuntimeMappings(prettySourceConfig); + setAdvancedEditorRuntimeMappingsLastApplied(prettySourceConfig); + setRuntimeMappingsEditorApplyButtonEnabled(false); + }; + + // If switching to KQL after updating via editor - reset search + const toggleRuntimeMappingsEditor = (reset = false) => { + if (reset === true) { + setRuntimeMappingsUpdated(false); + } + if (isRuntimeMappingsEditorEnabled === false) { + setAdvancedEditorRuntimeMappingsLastApplied(advancedEditorRuntimeMappings); + } + + setRuntimeMappingsEditorEnabled(!isRuntimeMappingsEditorEnabled); + setRuntimeMappingsEditorApplyButtonEnabled(false); + }; + + return { + actions: { + applyRuntimeMappingsEditorChanges, + setRuntimeMappingsEditorApplyButtonEnabled, + setRuntimeMappingsEditorEnabled, + setAdvancedEditorRuntimeMappings, + setAdvancedEditorRuntimeMappingsLastApplied, + setRuntimeMappingsEditorSwitchModalVisible, + setRuntimeMappingsUpdated, + toggleRuntimeMappingsEditor, + convertToJson, + setAdvancedRuntimeMappingsConfig, + }, + state: { + advancedEditorRuntimeMappings, + advancedEditorRuntimeMappingsLastApplied, + isRuntimeMappingsEditorApplyButtonEnabled, + isRuntimeMappingsEditorEnabled, + isRuntimeMappingsEditorSwitchModalVisible, + runtimeMappingsUpdated, + advancedRuntimeMappingsConfig, + xJsonMode, + runtimeMappings, + }, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts index ecc8bf673d93a8..d52bd3f5bf7060 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts @@ -32,19 +32,28 @@ export const latestConfigMapper = { * Provides available options for unique_key and sort fields * @param indexPattern * @param aggConfigs + * @param runtimeMappings */ function getOptions( indexPattern: StepDefineFormProps['searchItems']['indexPattern'], - aggConfigs: AggConfigs + aggConfigs: AggConfigs, + runtimeMappings?: StepDefineExposedState['runtimeMappings'] ) { const aggConfig = aggConfigs.aggs[0]; const param = aggConfig.type.params.find((p) => p.type === 'field'); const filteredIndexPatternFields = param - ? ((param as unknown) as FieldParamType).getAvailableFields(aggConfig) + ? ((param as unknown) as FieldParamType) + .getAvailableFields(aggConfig) + // runtimeMappings may already include runtime fields defined by the index pattern + .filter((ip) => ip.runtimeField === undefined) : []; const ignoreFieldNames = new Set(['_source', '_type', '_index', '_id', '_version', '_score']); + const runtimeFieldsOptions = runtimeMappings + ? Object.keys(runtimeMappings).map((k) => ({ label: k, value: k })) + : []; + const uniqueKeyOptions: Array> = filteredIndexPatternFields .filter((v) => !ignoreFieldNames.has(v.name)) .map((v) => ({ @@ -52,7 +61,16 @@ function getOptions( value: v.name, })); - const sortFieldOptions: Array> = indexPattern.fields + const runtimeFieldsSortOptions: Array> = runtimeMappings + ? Object.entries(runtimeMappings) + .filter(([fieldName, fieldMapping]) => fieldMapping.type === 'date') + .map(([fieldName, fieldMapping]) => ({ + label: fieldName, + value: fieldName, + })) + : []; + + const indexPatternFieldsSortOptions: Array> = indexPattern.fields // The backend API for `latest` allows all field types for sort but the UI will be limited to `date`. .filter((v) => !ignoreFieldNames.has(v.name) && v.sortable && v.type === 'date') .map((v) => ({ @@ -60,7 +78,15 @@ function getOptions( value: v.name, })); - return { uniqueKeyOptions, sortFieldOptions }; + const sortByLabel = (a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOption) => + a.label.localeCompare(b.label); + + return { + uniqueKeyOptions: [...uniqueKeyOptions, ...runtimeFieldsOptions].sort(sortByLabel), + sortFieldOptions: [...indexPatternFieldsSortOptions, ...runtimeFieldsSortOptions].sort( + sortByLabel + ), + }; } /** @@ -86,7 +112,8 @@ export function validateLatestConfig(config?: LatestFunctionConfig) { export function useLatestFunctionConfig( defaults: StepDefineExposedState['latestConfig'], - indexPattern: StepDefineFormProps['searchItems']['indexPattern'] + indexPattern: StepDefineFormProps['searchItems']['indexPattern'], + runtimeMappings: StepDefineExposedState['runtimeMappings'] ): { config: LatestFunctionConfigUI; uniqueKeyOptions: Array>; @@ -104,8 +131,8 @@ export function useLatestFunctionConfig( const { uniqueKeyOptions, sortFieldOptions } = useMemo(() => { const aggConfigs = data.search.aggs.createAggConfigs(indexPattern, [{ type: 'terms' }]); - return getOptions(indexPattern, aggConfigs); - }, [indexPattern, data.search.aggs]); + return getOptions(indexPattern, aggConfigs, runtimeMappings); + }, [indexPattern, data.search.aggs, runtimeMappings]); const updateLatestFunctionConfig = useCallback( (update) => diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts index 1748f6f8fd4873..a02d3bafac9848 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts @@ -115,8 +115,8 @@ export const usePivotConfig = ( const toastNotifications = useToastNotifications(); const { aggOptions, aggOptionsData, groupByOptions, groupByOptionsData } = useMemo( - () => getPivotDropdownOptions(indexPattern), - [indexPattern] + () => getPivotDropdownOptions(indexPattern, defaults.runtimeMappings), + [defaults.runtimeMappings, indexPattern] ); // The list of selected aggregations diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts index c2f01db05ff3e7..0ceea070df1b66 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts @@ -19,6 +19,7 @@ import { usePivotConfig } from './use_pivot_config'; import { useSearchBar } from './use_search_bar'; import { useLatestFunctionConfig } from './use_latest_function_config'; import { TRANSFORM_FUNCTION } from '../../../../../../../common/constants'; +import { useAdvancedRuntimeMappingsEditor } from './use_advanced_runtime_mappings_editor'; export type StepDefineFormHook = ReturnType; @@ -30,12 +31,18 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi const searchBar = useSearchBar(defaults, indexPattern); const pivotConfig = usePivotConfig(defaults, indexPattern); - const latestFunctionConfig = useLatestFunctionConfig(defaults.latestConfig, indexPattern); + + const latestFunctionConfig = useLatestFunctionConfig( + defaults.latestConfig, + indexPattern, + defaults?.runtimeMappings + ); const previewRequest = getPreviewTransformRequestBody( indexPattern.title, searchBar.state.pivotQuery, - pivotConfig.state.requestPayload + pivotConfig.state.requestPayload, + defaults?.runtimeMappings ); // pivot config hook @@ -44,12 +51,17 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi // source config hook const advancedSourceEditor = useAdvancedSourceEditor(defaults, previewRequest); + // runtime mappings config hook + const runtimeMappingsEditor = useAdvancedRuntimeMappingsEditor(defaults); + useEffect(() => { + const runtimeMappings = runtimeMappingsEditor.state.runtimeMappings; if (!advancedSourceEditor.state.isAdvancedSourceEditorEnabled) { const previewRequestUpdate = getPreviewTransformRequestBody( indexPattern.title, searchBar.state.pivotQuery, - pivotConfig.state.requestPayload + pivotConfig.state.requestPayload, + runtimeMappings ); const stringifiedSourceConfigUpdate = JSON.stringify( @@ -60,7 +72,6 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi advancedSourceEditor.actions.setAdvancedEditorSourceConfig(stringifiedSourceConfigUpdate); } - onChange({ transformFunction, latestConfig: latestFunctionConfig.config, @@ -84,6 +95,9 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi transformFunction === TRANSFORM_FUNCTION.PIVOT ? pivotConfig.state.requestPayload : latestFunctionConfig.requestPayload, + runtimeMappings, + runtimeMappingsUpdated: runtimeMappingsEditor.state.runtimeMappingsUpdated, + isRuntimeMappingsEditorEnabled: runtimeMappingsEditor.state.isRuntimeMappingsEditorEnabled, }); // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ @@ -92,9 +106,13 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi JSON.stringify(advancedSourceEditor.state), pivotConfig.state, JSON.stringify(searchBar.state), + JSON.stringify([ + runtimeMappingsEditor.state.runtimeMappings, + runtimeMappingsEditor.state.runtimeMappingsUpdated, + runtimeMappingsEditor.state.isRuntimeMappingsEditorEnabled, + ]), latestFunctionConfig.config, transformFunction, - /* eslint-enable react-hooks/exhaustive-deps */ ]); return { @@ -102,6 +120,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi setTransformFunction, advancedPivotEditor, advancedSourceEditor, + runtimeMappingsEditor, pivotConfig, latestFunctionConfig, searchBar, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index a5d9310e586e66..1ddb9aa61045ba 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -57,6 +57,7 @@ import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs'; import { TransformFunctionSelector } from './transform_function_selector'; import { TRANSFORM_FUNCTION } from '../../../../../../common/constants'; import { LatestFunctionForm } from './latest_function_form'; +import { AdvancedRuntimeMappingsSettings } from '../advanced_runtime_mappings_settings'; export interface StepDefineFormProps { overrides?: StepDefineExposedState; @@ -67,7 +68,6 @@ export interface StepDefineFormProps { export const StepDefineForm: FC = React.memo((props) => { const { searchItems } = props; const { indexPattern } = searchItems; - const { ml: { DataGrid }, } = useAppDependencies(); @@ -87,11 +87,14 @@ export const StepDefineForm: FC = React.memo((props) => { const pivotQuery = stepDefineForm.searchBar.state.pivotQuery; const indexPreviewProps = { - ...useIndexData(indexPattern, stepDefineForm.searchBar.state.pivotQuery), + ...useIndexData( + indexPattern, + stepDefineForm.searchBar.state.pivotQuery, + stepDefineForm.runtimeMappingsEditor.state.runtimeMappings + ), dataTestSubj: 'transformIndexPreview', toastNotifications, }; - const { requestPayload, validationStatus } = stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT ? stepDefineForm.pivotConfig.state @@ -102,7 +105,8 @@ export const StepDefineForm: FC = React.memo((props) => { pivotQuery, stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT ? stepDefineForm.pivotConfig.state.requestPayload - : stepDefineForm.latestFunctionConfig.requestPayload + : stepDefineForm.latestFunctionConfig.requestPayload, + stepDefineForm.runtimeMappingsEditor.state.runtimeMappings ); const copyToClipboardSource = getIndexDevConsoleStatement(pivotQuery, indexPattern.title); @@ -122,7 +126,13 @@ export const StepDefineForm: FC = React.memo((props) => { ); const pivotPreviewProps = { - ...usePivotData(indexPattern.title, pivotQuery, validationStatus, requestPayload), + ...usePivotData( + indexPattern.title, + pivotQuery, + validationStatus, + requestPayload, + stepDefineForm.runtimeMappingsEditor.state.runtimeMappings + ), dataTestSubj: 'transformPivotPreview', title: i18n.translate('xpack.transform.pivotPreview.transformPreviewTitle', { defaultMessage: 'Transform preview', @@ -273,7 +283,7 @@ export const StepDefineForm: FC = React.memo((props) => { defaultMessage: 'The advanced editor allows you to edit the source query clause of the transform configuration.', } - )}{' '} + )} {i18n.translate( 'xpack.transform.stepDefineForm.advancedEditorHelpTextLink', @@ -304,6 +314,9 @@ export const StepDefineForm: FC = React.memo((props) => { + + + diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index 614965c8a3efe3..27e25596c980fa 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -37,6 +37,7 @@ interface Props { export const StepDefineSummary: FC = ({ formState: { + runtimeMappings, searchString, searchQuery, groupByList, @@ -57,14 +58,16 @@ export const StepDefineSummary: FC = ({ const previewRequest = getPreviewTransformRequestBody( searchItems.indexPattern.title, pivotQuery, - partialPreviewRequest + partialPreviewRequest, + runtimeMappings ); const pivotPreviewProps = usePivotData( searchItems.indexPattern.title, pivotQuery, validationStatus, - partialPreviewRequest + partialPreviewRequest, + runtimeMappings ); const isModifiedQuery = diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts index 3b8df3b977fff6..fbe32e9bea12ff 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts @@ -5,5 +5,95 @@ * 2.0. */ +import type { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform'; + export type EsIndexName = string; export type IndexPatternTitle = string; + +export interface StepDetailsExposedState { + continuousModeDateField: string; + continuousModeDelay: string; + createIndexPattern: boolean; + destinationIndex: EsIndexName; + isContinuousModeEnabled: boolean; + isRetentionPolicyEnabled: boolean; + retentionPolicyDateField: string; + retentionPolicyMaxAge: string; + touched: boolean; + transformId: TransformId; + transformDescription: string; + transformFrequency: string; + transformSettingsMaxPageSearchSize: number; + transformSettingsDocsPerSecond?: number; + valid: boolean; + indexPatternTimeField?: string | undefined; +} + +const defaultContinuousModeDelay = '60s'; +const defaultTransformFrequency = '1m'; +const defaultTransformSettingsMaxPageSearchSize = 500; + +export function getDefaultStepDetailsState(): StepDetailsExposedState { + return { + continuousModeDateField: '', + continuousModeDelay: defaultContinuousModeDelay, + createIndexPattern: true, + isContinuousModeEnabled: false, + isRetentionPolicyEnabled: false, + retentionPolicyDateField: '', + retentionPolicyMaxAge: '', + transformId: '', + transformDescription: '', + transformFrequency: defaultTransformFrequency, + transformSettingsMaxPageSearchSize: defaultTransformSettingsMaxPageSearchSize, + destinationIndex: '', + touched: false, + valid: false, + indexPatternTimeField: undefined, + }; +} + +export function applyTransformConfigToDetailsState( + state: StepDetailsExposedState, + transformConfig?: TransformPivotConfig +): StepDetailsExposedState { + // apply the transform configuration to wizard DETAILS state + if (transformConfig !== undefined) { + // Continuous mode + const continuousModeTime = transformConfig.sync?.time; + if (continuousModeTime !== undefined) { + state.continuousModeDateField = continuousModeTime.field; + state.continuousModeDelay = continuousModeTime?.delay ?? defaultContinuousModeDelay; + state.isContinuousModeEnabled = true; + } + + // Description + if (transformConfig.description !== undefined) { + state.transformDescription = transformConfig.description; + } + + // Frequency + if (transformConfig.frequency !== undefined) { + state.transformFrequency = transformConfig.frequency; + } + + // Retention policy + const retentionPolicyTime = transformConfig.retention_policy?.time; + if (retentionPolicyTime !== undefined) { + state.retentionPolicyDateField = retentionPolicyTime.field; + state.retentionPolicyMaxAge = retentionPolicyTime.max_age; + state.isRetentionPolicyEnabled = true; + } + + // Settings + if (transformConfig.settings) { + if (typeof transformConfig.settings?.max_page_search_size === 'number') { + state.transformSettingsMaxPageSearchSize = transformConfig.settings.max_page_search_size; + } + if (typeof transformConfig.settings?.docs_per_second === 'number') { + state.transformSettingsDocsPerSecond = transformConfig.settings.docs_per_second; + } + } + } + return state; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts index 4b01e0c3746ec7..bbc4b42e1b236e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts @@ -8,6 +8,7 @@ export { applyTransformConfigToDetailsState, getDefaultStepDetailsState, - StepDetailsForm, -} from './step_details_form'; + StepDetailsExposedState, +} from './common'; +export { StepDetailsForm } from './step_details_form'; export { StepDetailsSummary } from './step_details_summary'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 100c37d911fa01..0d39ec77d059fb 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, FC, useEffect, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -49,87 +49,23 @@ import { import { EsIndexName, IndexPatternTitle } from './common'; import { continuousModeDelayValidator, + retentionPolicyMaxAgeValidator, transformFrequencyValidator, transformSettingsMaxPageSearchSizeValidator, } from '../../../../common/validators'; import { StepDefineExposedState } from '../step_define/common'; import { TRANSFORM_FUNCTION } from '../../../../../../common/constants'; -export interface StepDetailsExposedState { - continuousModeDateField: string; - continuousModeDelay: string; - createIndexPattern: boolean; - destinationIndex: EsIndexName; - isContinuousModeEnabled: boolean; - touched: boolean; - transformId: TransformId; - transformDescription: string; - transformFrequency: string; - transformSettingsMaxPageSearchSize: number; - transformSettingsDocsPerSecond?: number; - valid: boolean; - indexPatternTimeField?: string | undefined; -} - -const defaultContinuousModeDelay = '60s'; -const defaultTransformFrequency = '1m'; -const defaultTransformSettingsMaxPageSearchSize = 500; - -export function getDefaultStepDetailsState(): StepDetailsExposedState { - return { - continuousModeDateField: '', - continuousModeDelay: defaultContinuousModeDelay, - createIndexPattern: true, - isContinuousModeEnabled: false, - transformId: '', - transformDescription: '', - transformFrequency: defaultTransformFrequency, - transformSettingsMaxPageSearchSize: defaultTransformSettingsMaxPageSearchSize, - destinationIndex: '', - touched: false, - valid: false, - indexPatternTimeField: undefined, - }; -} +import { getDefaultStepDetailsState, StepDetailsExposedState } from './common'; -export function applyTransformConfigToDetailsState( - state: StepDetailsExposedState, - transformConfig?: TransformPivotConfig -): StepDetailsExposedState { - // apply the transform configuration to wizard DETAILS state - if (transformConfig !== undefined) { - const time = transformConfig.sync?.time; - if (time !== undefined) { - state.continuousModeDateField = time.field; - state.continuousModeDelay = time?.delay ?? defaultContinuousModeDelay; - state.isContinuousModeEnabled = true; - } - if (transformConfig.description !== undefined) { - state.transformDescription = transformConfig.description; - } - if (transformConfig.frequency !== undefined) { - state.transformFrequency = transformConfig.frequency; - } - if (transformConfig.settings) { - if (typeof transformConfig.settings?.max_page_search_size === 'number') { - state.transformSettingsMaxPageSearchSize = transformConfig.settings.max_page_search_size; - } - if (typeof transformConfig.settings?.docs_per_second === 'number') { - state.transformSettingsDocsPerSecond = transformConfig.settings.docs_per_second; - } - } - } - return state; -} - -interface Props { +interface StepDetailsFormProps { overrides?: StepDetailsExposedState; onChange(s: StepDetailsExposedState): void; searchItems: SearchItems; stepDefineState: StepDefineExposedState; } -export const StepDetailsForm: FC = React.memo( +export const StepDetailsForm: FC = React.memo( ({ overrides = {}, onChange, searchItems, stepDefineState }) => { const deps = useAppDependencies(); const toastNotifications = useToastNotifications(); @@ -171,11 +107,6 @@ export const StepDetailsForm: FC = React.memo( [setIndexPatternTimeField, indexPatternAvailableTimeFields] ); - // Continuous mode state - const [isContinuousModeEnabled, setContinuousModeEnabled] = useState( - defaults.isContinuousModeEnabled - ); - const api = useApi(); // fetch existing transform IDs and indices once for form validation @@ -187,7 +118,8 @@ export const StepDetailsForm: FC = React.memo( const previewRequest = getPreviewTransformRequestBody( searchItems.indexPattern.title, pivotQuery, - partialPreviewRequest + partialPreviewRequest, + stepDefineState.runtimeMappings ); const transformPreview = await api.getTransformsPreview(previewRequest); @@ -268,13 +200,41 @@ export const StepDetailsForm: FC = React.memo( .filter((f) => f.type === KBN_FIELD_TYPES.DATE) .map((f) => f.name) .sort(); + + // Continuous Mode const isContinuousModeAvailable = dateFieldNames.length > 0; + const [isContinuousModeEnabled, setContinuousModeEnabled] = useState( + defaults.isContinuousModeEnabled + ); const [continuousModeDateField, setContinuousModeDateField] = useState( isContinuousModeAvailable ? dateFieldNames[0] : '' ); const [continuousModeDelay, setContinuousModeDelay] = useState(defaults.continuousModeDelay); const isContinuousModeDelayValid = continuousModeDelayValidator(continuousModeDelay); + // Retention Policy + const isRetentionPolicyAvailable = dateFieldNames.length > 0; + const [isRetentionPolicyEnabled, setRetentionPolicyEnabled] = useState( + defaults.isRetentionPolicyEnabled + ); + const [retentionPolicyDateField, setRetentionPolicyDateField] = useState( + isRetentionPolicyAvailable ? dateFieldNames[0] : '' + ); + const [retentionPolicyMaxAge, setRetentionPolicyMaxAge] = useState( + defaults.retentionPolicyMaxAge + ); + const retentionPolicyMaxAgeEmpty = retentionPolicyMaxAge === ''; + const isRetentionPolicyMaxAgeValid = retentionPolicyMaxAgeValidator(retentionPolicyMaxAge); + + // Reset retention policy settings when the user disables the whole option + useEffect(() => { + if (!isRetentionPolicyEnabled) { + setRetentionPolicyDateField(isRetentionPolicyAvailable ? dateFieldNames[0] : ''); + setRetentionPolicyMaxAge(''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isRetentionPolicyEnabled]); + const transformIdExists = transformIds.some((id) => transformId === id); const transformIdEmpty = transformId === ''; const transformIdValid = isTransformIdValid(transformId); @@ -305,7 +265,13 @@ export const StepDetailsForm: FC = React.memo( !indexNameEmpty && indexNameValid && (!indexPatternTitleExists || !createIndexPattern) && - (!isContinuousModeAvailable || (isContinuousModeAvailable && isContinuousModeDelayValid)); + (!isContinuousModeAvailable || (isContinuousModeAvailable && isContinuousModeDelayValid)) && + (!isRetentionPolicyAvailable || + !isRetentionPolicyEnabled || + (isRetentionPolicyAvailable && + isRetentionPolicyEnabled && + !retentionPolicyMaxAgeEmpty && + isRetentionPolicyMaxAgeValid)); // expose state to wizard useEffect(() => { @@ -314,6 +280,9 @@ export const StepDetailsForm: FC = React.memo( continuousModeDelay, createIndexPattern, isContinuousModeEnabled, + isRetentionPolicyEnabled, + retentionPolicyDateField, + retentionPolicyMaxAge, transformId, transformDescription, transformFrequency, @@ -331,6 +300,9 @@ export const StepDetailsForm: FC = React.memo( continuousModeDelay, createIndexPattern, isContinuousModeEnabled, + isRetentionPolicyEnabled, + retentionPolicyDateField, + retentionPolicyMaxAge, transformId, transformDescription, transformFrequency, @@ -417,7 +389,7 @@ export const StepDetailsForm: FC = React.memo( error={ !indexNameEmpty && !indexNameValid && [ - + <> {i18n.translate('xpack.transform.stepDetailsForm.destinationIndexInvalidError', { defaultMessage: 'Invalid destination index name.', })} @@ -430,7 +402,7 @@ export const StepDetailsForm: FC = React.memo( } )} - , + , ] } > @@ -502,6 +474,8 @@ export const StepDetailsForm: FC = React.memo( onTimeFieldChanged={onTimeFieldChanged} /> )} + + {/* Continuous mode */} = React.memo( /> {isContinuousModeEnabled && ( - + <> = React.memo( )} > setContinuousModeDelay(e.target.value)} aria-label={i18n.translate( @@ -580,7 +560,100 @@ export const StepDetailsForm: FC = React.memo( data-test-subj="transformContinuousDelayInput" /> - + + )} + + {/* Retention policy */} + + setRetentionPolicyEnabled(!isRetentionPolicyEnabled)} + disabled={isRetentionPolicyAvailable === false} + data-test-subj="transformRetentionPolicySwitch" + /> + + {isRetentionPolicyEnabled && ( + <> + + ({ text }))} + value={retentionPolicyDateField} + onChange={(e) => setRetentionPolicyDateField(e.target.value)} + data-test-subj="transformRetentionPolicyDateFieldSelect" + /> + + + setRetentionPolicyMaxAge(e.target.value)} + aria-label={i18n.translate( + 'xpack.transform.stepDetailsForm.retentionPolicyMaxAgeAriaLabel', + { + defaultMessage: 'Choose a max age.', + } + )} + isInvalid={!retentionPolicyMaxAgeEmpty && !isRetentionPolicyMaxAgeValid} + data-test-subj="transformRetentionPolicyMaxAgeInput" + /> + + )} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx index 7fb9f8ba06c055..f39132da819859 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx @@ -11,13 +11,16 @@ import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiFormRow, EuiSpacer } from '@elastic/eui'; -import { StepDetailsExposedState } from './step_details_form'; +import { StepDetailsExposedState } from './common'; export const StepDetailsSummary: FC = React.memo((props) => { const { continuousModeDateField, createIndexPattern, isContinuousModeEnabled, + isRetentionPolicyEnabled, + retentionPolicyDateField, + retentionPolicyMaxAge, transformId, transformDescription, transformFrequency, @@ -85,6 +88,28 @@ export const StepDetailsSummary: FC = React.memo((props )} + {isRetentionPolicyEnabled && ( + <> + + {retentionPolicyDateField} + + + {retentionPolicyMaxAge} + + + )} + void; @@ -59,19 +59,17 @@ const cancelButtonText = i18n.translate( ); export const SwitchModal: FC = ({ onCancel, onConfirm, type }) => ( - - -

    {type === 'pivot' ? pivotModalMessage : sourceModalMessage}

    -
    -
    + +

    {type === 'pivot' ? pivotModalMessage : sourceModalMessage}

    +
    ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 9837ace2720725..5ae464affa0164 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -32,6 +32,7 @@ import { } from '../step_details'; import { WizardNav } from '../wizard_nav'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import type { RuntimeMappings } from '../step_define/common/types'; enum KBN_MANAGEMENT_PAGE_CLASSNAME { DEFAULT_BODY = 'mgtPage__body', @@ -89,8 +90,12 @@ interface WizardProps { searchItems: SearchItems; } -export const CreateTransformWizardContext = createContext<{ indexPattern: IndexPattern | null }>({ +export const CreateTransformWizardContext = createContext<{ + indexPattern: IndexPattern | null; + runtimeMappings: RuntimeMappings | undefined; +}>({ indexPattern: null, + runtimeMappings: undefined, }); export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) => { @@ -239,7 +244,9 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) const stepsConfig = [stepDefine, stepDetails, stepCreate]; return ( - + ); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx index 148e6c1a3bac0c..d82f0769c8b74c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx @@ -12,7 +12,6 @@ import { EuiConfirmModal, EuiFlexGroup, EuiFlexItem, - EuiOverlayMask, EuiSpacer, EuiSwitch, } from '@elastic/eui'; @@ -123,22 +122,20 @@ export const DeleteActionModal: FC = ({ ); return ( - - - {isBulkAction ? bulkDeleteModalContent : deleteModalContent} - - + + {isBulkAction ? bulkDeleteModalContent : deleteModalContent} + ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx index c3967dd687a632..bb01fe355a33ec 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { StartAction } from './use_start_action'; @@ -24,27 +24,25 @@ export const StartActionModal: FC = ({ closeModal, items, startAndC }); return ( - - +

    + {i18n.translate('xpack.transform.transformList.startModalBody', { + defaultMessage: + 'A transform increases search and indexing load in your cluster. If excessive load is experienced, stop the transform.', })} - confirmButtonText={i18n.translate('xpack.transform.transformList.startModalStartButton', { - defaultMessage: 'Start', - })} - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - buttonColor="primary" - > -

    - {i18n.translate('xpack.transform.transformList.startModalBody', { - defaultMessage: - 'A transform increases search and indexing load in your cluster. If excessive load is experienced, stop the transform.', - })} -

    -
    -
    +

    + ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx index 250cf6133dbcf0..62c63d90a17b93 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx @@ -98,6 +98,46 @@ export const EditTransformFlyoutForm: FC = ({ + + dispatch({ field: 'retentionPolicyField', value })} + value={formFields.retentionPolicyField.value} + /> + + dispatch({ field: 'retentionPolicyMaxAge', value })} + value={formFields.retentionPolicyMaxAge.value} + /> + + + + { }); }); -describe('Transfom: stringValidator()', () => { +describe('Transform: stringValidator()', () => { it('should allow an empty string for optional fields', () => { expect(stringValidator('')).toHaveLength(0); }); @@ -270,6 +271,43 @@ describe('Transform: frequencyValidator()', () => { }); }); +describe('Transform: retentionPolicyMaxAgeValidator()', () => { + const transformRetentionPolicyMaxAgeValidator = (arg: string) => + retentionPolicyMaxAgeValidator(arg).length === 0; + + it('should only allow values equal or above 60s.', () => { + expect(transformRetentionPolicyMaxAgeValidator('0nanos')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('59999999999nanos')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('60000000000nanos')).toBe(true); + expect(transformRetentionPolicyMaxAgeValidator('60000000001nanos')).toBe(true); + + expect(transformRetentionPolicyMaxAgeValidator('0micros')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('59999999micros')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('60000000micros')).toBe(true); + expect(transformRetentionPolicyMaxAgeValidator('60000001micros')).toBe(true); + + expect(transformRetentionPolicyMaxAgeValidator('0ms')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('59999ms')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('60000ms')).toBe(true); + expect(transformRetentionPolicyMaxAgeValidator('60001ms')).toBe(true); + + expect(transformRetentionPolicyMaxAgeValidator('0s')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('1s')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('59s')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('60s')).toBe(true); + expect(transformRetentionPolicyMaxAgeValidator('61s')).toBe(true); + expect(transformRetentionPolicyMaxAgeValidator('10000s')).toBe(true); + + expect(transformRetentionPolicyMaxAgeValidator('0m')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('1m')).toBe(true); + expect(transformRetentionPolicyMaxAgeValidator('100m')).toBe(true); + + expect(transformRetentionPolicyMaxAgeValidator('0h')).toBe(false); + expect(transformRetentionPolicyMaxAgeValidator('1h')).toBe(true); + expect(transformRetentionPolicyMaxAgeValidator('2h')).toBe(true); + }); +}); + describe('Transform: integerAboveZeroValidator()', () => { it('should only allow integers above zero', () => { // integerAboveZeroValidator() returns an array of error messages so diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts index a86a9cd8012629..6680495bdab912 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts @@ -16,6 +16,12 @@ import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_ import { TransformConfigUnion } from '../../../../../../common/types/transform'; import { getNestedProperty, setNestedProperty } from '../../../../../../common/utils/object_utils'; +import { + isValidFrequency, + isValidRetentionPolicyMaxAge, + ParsedDuration, +} from '../../../../common/validators'; + // This custom hook uses nested reducers to provide a generic framework to manage form state // and apply it to a final possibly nested configuration object suitable for passing on // directly to an API call. For now this is only used for the transform edit form. @@ -25,21 +31,23 @@ import { getNestedProperty, setNestedProperty } from '../../../../../../common/u // The outer most level reducer defines a flat structure of names for form fields. // This is a flat structure regardless of whether the final request object will be nested. // For example, `destinationIndex` and `destinationPipeline` will later be nested under `dest`. -interface EditTransformFlyoutFieldsState { - [key: string]: FormField; - description: FormField; - destinationIndex: FormField; - destinationPipeline: FormField; - frequency: FormField; - docsPerSecond: FormField; -} +type EditTransformFormFields = + | 'description' + | 'destinationIndex' + | 'destinationPipeline' + | 'frequency' + | 'docsPerSecond' + | 'maxPageSearchSize' + | 'retentionPolicyField' + | 'retentionPolicyMaxAge'; +type EditTransformFlyoutFieldsState = Record; // The inner reducers apply validation based on supplied attributes of each field. export interface FormField { formFieldName: string; configFieldName: string; defaultValue: string; - dependsOn: string[]; + dependsOn: EditTransformFormFields[]; errorMessages: string[]; isNullable: boolean; isOptional: boolean; @@ -122,14 +130,7 @@ export const stringValidator: Validator = (value, isOptional = true) => { return []; }; -// Only allow frequencies in the form of 1s/1h etc. -const frequencyNotValidErrorMessage = i18n.translate( - 'xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage', - { - defaultMessage: 'The frequency value is not valid.', - } -); -export const frequencyValidator: Validator = (arg) => { +function parseDurationAboveZero(arg: any, errorMessage: string): ParsedDuration | string[] { if (typeof arg !== 'string' || arg === null) { return [stringNotValidErrorMessage]; } @@ -142,20 +143,49 @@ export const frequencyValidator: Validator = (arg) => { return [frequencyNotValidErrorMessage]; } - const valueNumber = +regexStr[0]; - const valueTimeUnit = regexStr[1]; + const number = +regexStr[0]; + const timeUnit = regexStr[1]; // only valid if number is an integer above 0 - if (isNaN(valueNumber) || !Number.isInteger(valueNumber) || valueNumber === 0) { + if (isNaN(number) || !Number.isInteger(number) || number === 0) { return [frequencyNotValidErrorMessage]; } - // only valid if value is up to 1 hour - return (valueTimeUnit === 's' && valueNumber <= 3600) || - (valueTimeUnit === 'm' && valueNumber <= 60) || - (valueTimeUnit === 'h' && valueNumber === 1) - ? [] - : [frequencyNotValidErrorMessage]; + return { number, timeUnit }; +} + +// Only allow frequencies in the form of 1s/1h etc. +const frequencyNotValidErrorMessage = i18n.translate( + 'xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage', + { + defaultMessage: 'The frequency value is not valid.', + } +); +export const frequencyValidator: Validator = (arg) => { + const parsedArg = parseDurationAboveZero(arg, frequencyNotValidErrorMessage); + + if (Array.isArray(parsedArg)) { + return parsedArg; + } + + return isValidFrequency(parsedArg) ? [] : [frequencyNotValidErrorMessage]; +}; + +// Retention policy max age validator +const retentionPolicyMaxAgeNotValidErrorMessage = i18n.translate( + 'xpack.transform.transformList.editFlyoutFormRetentionPolicyMaxAgeNotValidErrorMessage', + { + defaultMessage: 'Invalid max age format. Minimum of 60s required.', + } +); +export const retentionPolicyMaxAgeValidator: Validator = (arg) => { + const parsedArg = parseDurationAboveZero(arg, retentionPolicyMaxAgeNotValidErrorMessage); + + if (Array.isArray(parsedArg)) { + return parsedArg; + } + + return isValidRetentionPolicyMaxAge(parsedArg) ? [] : [retentionPolicyMaxAgeNotValidErrorMessage]; }; const validate = { @@ -163,10 +193,11 @@ const validate = { frequency: frequencyValidator, integerAboveZero: integerAboveZeroValidator, integerRange10To10000: integerRange10To10000Validator, + retentionPolicyMaxAge: retentionPolicyMaxAgeValidator, } as const; export const initializeField = ( - formFieldName: string, + formFieldName: EditTransformFormFields, configFieldName: string, config: TransformConfigUnion, overloads?: Partial @@ -199,7 +230,7 @@ export interface EditTransformFlyoutState { // This is not a redux type action, // since for now we only have one action type. interface Action { - field: keyof EditTransformFlyoutFieldsState; + field: EditTransformFormFields; value: string; } @@ -207,7 +238,7 @@ interface Action { // of the expected final configuration request object. // Considers options like if a value is nullable or optional. const getUpdateValue = ( - attribute: keyof EditTransformFlyoutFieldsState, + attribute: EditTransformFormFields, config: TransformConfigUnion, formState: EditTransformFlyoutFieldsState, enforceFormValue = false @@ -251,7 +282,7 @@ export const applyFormFieldsToTransformConfig = ( ): PostTransformsUpdateRequestSchema => // Iterates over all form fields and only if necessary applies them to // the request object used for updating the transform. - Object.keys(formState).reduce( + (Object.keys(formState) as EditTransformFormFields[]).reduce( (updateConfig, field) => merge({ ...updateConfig }, getUpdateValue(field, config, formState)), {} ); @@ -292,6 +323,25 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo valueParser: (v) => +v, } ), + + // retention_policy.* + retentionPolicyField: initializeField( + 'retentionPolicyField', + 'retention_policy.time.field', + config, + { dependsOn: ['retentionPolicyMaxAge'], isNullable: false, isOptional: true } + ), + retentionPolicyMaxAge: initializeField( + 'retentionPolicyMaxAge', + 'retention_policy.time.max_age', + config, + { + dependsOn: ['retentionPolicyField'], + isNullable: false, + isOptional: true, + validator: 'retentionPolicyMaxAge', + } + ), }, isFormTouched: false, isFormValid: true, @@ -300,7 +350,10 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo // Checks each form field for error messages to return // if the overall form is valid or not. const isFormValid = (fieldsState: EditTransformFlyoutFieldsState) => - Object.keys(fieldsState).reduce((p, c) => p && fieldsState[c].errorMessages.length === 0, true); + (Object.keys(fieldsState) as EditTransformFormFields[]).reduce( + (p, c) => p && fieldsState[c].errorMessages.length === 0, + true + ); // Updates a form field with its new value, // runs validation and populates diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 2ee558d449c9ac..87ae90afdf9c97 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -29,7 +29,7 @@ export const ExpandedRowPreviewPane: FC = ({ transf } = useAppDependencies(); const toastNotifications = useToastNotifications(); - const { searchQuery, validationStatus, previewRequest } = useMemo( + const { searchQuery, validationStatus, previewRequest, runtimeMappings } = useMemo( () => applyTransformConfigToDefineState( getDefaultStepDefineState({} as SearchItems), @@ -48,7 +48,8 @@ export const ExpandedRowPreviewPane: FC = ({ transf indexPatternTitle, pivotQuery, validationStatus, - previewRequest + previewRequest, + runtimeMappings ); return ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index b84d7fc433dfc1..bcb07c8069ab2f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -14,7 +14,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiModal, - EuiOverlayMask, EuiPageContent, EuiPageContentBody, EuiSpacer, @@ -124,15 +123,13 @@ export const TransformManagement: FC = () => { {isSearchSelectionVisible && ( - - - - - + + + )} ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a408d2d8d76d8e..d60729aeb055f8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -646,7 +646,6 @@ "dashboard.savedDashboard.newDashboardTitle": "新規ダッシュボード", "dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "このダッシュボードに時刻が保存されていないため、同期できません。", "dashboard.strings.dashboardEditTitle": "{title}を編集中", - "dashboard.strings.dashboardUnsavedEditTitle": "{title}を編集中(未保存)", "dashboard.topNav.cloneModal.cancelButtonLabel": "キャンセル", "dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "ダッシュボードのクローンを作成", "dashboard.topNav.cloneModal.confirmButtonLabel": "クローンの確認", @@ -892,7 +891,6 @@ "data.noDataPopover.subtitle": "ヒント", "data.noDataPopover.title": "空のデータセット", "data.painlessError.buttonTxt": "スクリプトを編集", - "data.painlessError.painlessScriptedFieldErrorMessage": "Painlessスクリプトの実行エラー:「{script}」。", "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です", "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔形式:{interval}", "data.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2ce673ef9c85b8..4643e64eb6b10b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -646,7 +646,6 @@ "dashboard.savedDashboard.newDashboardTitle": "新建仪表板", "dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "时间未随此仪表板保存,因此无法同步。", "dashboard.strings.dashboardEditTitle": "正在编辑 {title}", - "dashboard.strings.dashboardUnsavedEditTitle": "正在编辑 {title}(未保存)", "dashboard.topNav.cloneModal.cancelButtonLabel": "取消", "dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "克隆仪表板", "dashboard.topNav.cloneModal.confirmButtonLabel": "确认克隆", @@ -892,7 +891,6 @@ "data.noDataPopover.subtitle": "提示", "data.noDataPopover.title": "空数据集", "data.painlessError.buttonTxt": "编辑脚本", - "data.painlessError.painlessScriptedFieldErrorMessage": "执行 Painless 脚本时出错:“{script}”。", "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1", "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}", "data.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx index 952ea07ba05c33..b98db1178f4623 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { HttpSetup } from 'kibana/public'; @@ -73,56 +73,54 @@ export const DeleteModalConfirmation = ({ } ); return ( - - { - setDeleteModalVisibility(false); - onCancel(); - }} - onConfirm={async () => { - setDeleteModalVisibility(false); - setIsLoadingState(true); - const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http }); - setIsLoadingState(false); + { + setDeleteModalVisibility(false); + onCancel(); + }} + onConfirm={async () => { + setDeleteModalVisibility(false); + setIsLoadingState(true); + const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http }); + setIsLoadingState(false); - const numSuccesses = successes.length; - const numErrors = errors.length; - if (numSuccesses > 0) { - toasts.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText', - { - defaultMessage: - 'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}', - values: { numSuccesses, singleTitle, multipleTitle }, - } - ) - ); - } + const numSuccesses = successes.length; + const numErrors = errors.length; + if (numSuccesses > 0) { + toasts.addSuccess( + i18n.translate( + 'xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText', + { + defaultMessage: + 'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { numSuccesses, singleTitle, multipleTitle }, + } + ) + ); + } - if (numErrors > 0) { - toasts.addDanger( - i18n.translate( - 'xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText', - { - defaultMessage: - 'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}', - values: { numErrors, singleTitle, multipleTitle }, - } - ) - ); - await onErrors(); - } - await onDeleted(successes); - }} - cancelButtonText={cancelButtonText} - confirmButtonText={confirmButtonText} - > - {confirmModalText} - - + if (numErrors > 0) { + toasts.addDanger( + i18n.translate( + 'xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText', + { + defaultMessage: + 'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { numErrors, singleTitle, multipleTitle }, + } + ) + ); + await onErrors(); + } + await onDeleted(successes); + }} + cancelButtonText={cancelButtonText} + confirmButtonText={confirmButtonText} + > + {confirmModalText} + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index 0e848d8cc07882..0dca49ea5353d4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -44,10 +44,18 @@ describe('transformActionVariables', () => { "description": "The alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroup", }, + Object { + "description": "The alert action subgroup that was used to scheduled actions for the alert.", + "name": "alertActionSubgroup", + }, Object { "description": "The human readable name of the alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroupName", }, + Object { + "description": "The configured server.publicBaseUrl value or empty string if not configured.", + "name": "kibanaBaseUrl", + }, ] `); }); @@ -91,10 +99,18 @@ describe('transformActionVariables', () => { "description": "The alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroup", }, + Object { + "description": "The alert action subgroup that was used to scheduled actions for the alert.", + "name": "alertActionSubgroup", + }, Object { "description": "The human readable name of the alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroupName", }, + Object { + "description": "The configured server.publicBaseUrl value or empty string if not configured.", + "name": "kibanaBaseUrl", + }, Object { "description": "foo-description", "name": "context.foo", @@ -146,10 +162,18 @@ describe('transformActionVariables', () => { "description": "The alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroup", }, + Object { + "description": "The alert action subgroup that was used to scheduled actions for the alert.", + "name": "alertActionSubgroup", + }, Object { "description": "The human readable name of the alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroupName", }, + Object { + "description": "The configured server.publicBaseUrl value or empty string if not configured.", + "name": "kibanaBaseUrl", + }, Object { "description": "foo-description", "name": "state.foo", @@ -204,10 +228,18 @@ describe('transformActionVariables', () => { "description": "The alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroup", }, + Object { + "description": "The alert action subgroup that was used to scheduled actions for the alert.", + "name": "alertActionSubgroup", + }, Object { "description": "The human readable name of the alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroupName", }, + Object { + "description": "The configured server.publicBaseUrl value or empty string if not configured.", + "name": "kibanaBaseUrl", + }, Object { "description": "fooC-description", "name": "context.fooC", @@ -280,10 +312,18 @@ describe('transformActionVariables', () => { "description": "The alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroup", }, + Object { + "description": "The alert action subgroup that was used to scheduled actions for the alert.", + "name": "alertActionSubgroup", + }, Object { "description": "The human readable name of the alert action group that was used to scheduled actions for the alert.", "name": "alertActionGroupName", }, + Object { + "description": "The configured server.publicBaseUrl value or empty string if not configured.", + "name": "kibanaBaseUrl", + }, Object { "description": "fooC-description", "name": "context.fooC", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index 65c1145b6d1d97..92be6a8685c7c8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -88,6 +88,17 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] { }), }); + result.push({ + name: 'alertActionSubgroup', + description: i18n.translate( + 'xpack.triggersActionsUI.actionVariables.alertActionSubgroupLabel', + { + defaultMessage: + 'The alert action subgroup that was used to scheduled actions for the alert.', + } + ), + }); + result.push({ name: 'alertActionGroupName', description: i18n.translate( @@ -99,5 +110,13 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] { ), }); + result.push({ + name: 'kibanaBaseUrl', + description: i18n.translate('xpack.triggersActionsUI.actionVariables.kibanaBaseUrlLabel', { + defaultMessage: + 'The configured server.publicBaseUrl value or empty string if not configured.', + }), + }); + return result; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index b7450d742bc45b..8732727b9a77a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -7,17 +7,19 @@ import React, { useCallback, useMemo, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup } from '@elastic/eui'; import { EuiModal, EuiButton, + EuiButtonEmpty, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody, EuiModalFooter, + EuiTitle, + EuiFlexItem, + EuiIcon, + EuiFlexGroup, } from '@elastic/eui'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionConnectorForm, getConnectorErrors } from './action_connector_form'; import { createConnectorReducer, InitialConnector, ConnectorReducer } from './connector_reducer'; @@ -127,92 +129,90 @@ export const ConnectorAddModal = ({ }); return ( - - - - - - {actionTypeModel && actionTypeModel.iconClass ? ( - - - - ) : null} - - -

    - -

    -
    + + + + + {actionTypeModel && actionTypeModel.iconClass ? ( + + - - - + ) : null} + + +

    + +

    +
    +
    +
    +
    +
    - - - - - - {i18n.translate( - 'xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel', - { - defaultMessage: 'Cancel', + + + + + + {i18n.translate( + 'xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + + {canSave ? ( + { + if (hasErrors) { + setConnector( + getConnectorWithInvalidatedFields( + connector, + configErrors, + secretsErrors, + connectorBaseErrors + ) + ); + return; } - )} - - {canSave ? ( - { - if (hasErrors) { - setConnector( - getConnectorWithInvalidatedFields( - connector, - configErrors, - secretsErrors, - connectorBaseErrors - ) - ); - return; - } - setIsSaving(true); - const savedAction = await onActionConnectorSave(); - setIsSaving(false); - if (savedAction) { - if (postSaveEventHandler) { - postSaveEventHandler(savedAction); - } - closeModal(); + setIsSaving(true); + const savedAction = await onActionConnectorSave(); + setIsSaving(false); + if (savedAction) { + if (postSaveEventHandler) { + postSaveEventHandler(savedAction); } - }} - > - - - ) : null} - -
    -
    + closeModal(); + } + }} + > + + + ) : null} + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx index 9ef7e414d505e9..6d71fe858f1c12 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -17,38 +17,36 @@ interface Props { export const ConfirmAlertClose: React.FC = ({ onConfirm, onCancel }) => { return ( - - -

    - -

    -
    -
    + +

    + +

    +
    ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx index 48d4229bb9b303..c406ec7c802837 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -17,38 +17,36 @@ interface Props { export const ConfirmAlertSave: React.FC = ({ onConfirm, onCancel }) => { return ( - - -

    - -

    -
    -
    + +

    + +

    +
    ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx index f13e5fd96d2ad8..4a5739c8b44309 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { capitalize } from 'lodash'; interface Props { @@ -26,37 +26,35 @@ export const ManageLicenseModal: React.FC = ({ }) => { const licenseRequired = capitalize(licenseType); return ( - - -

    - -

    -
    -
    + +

    + +

    +
    ); }; diff --git a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap index 238ce6c3f9ceec..b689ca7ff56f0e 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap @@ -50,6 +50,17 @@ exports[`DonutChart component passes correct props without errors for valid prop "strokeWidth": 1, "visible": true, }, + "axisPanelTitle": Object { + "fill": "#333", + "fontFamily": "sans-serif", + "fontSize": 10, + "fontStyle": "bold", + "padding": Object { + "inner": 8, + "outer": 0, + }, + "visible": true, + }, "axisTitle": Object { "fill": "#333", "fontFamily": "sans-serif", @@ -331,6 +342,14 @@ exports[`DonutChart component passes correct props without errors for valid prop "band": Object { "fill": "rgba(245, 247, 250, 1)", }, + "crossLine": Object { + "dash": Array [ + 4, + 4, + ], + "stroke": "rgba(105, 112, 125, 1)", + "strokeWidth": 1, + }, "line": Object { "dash": Array [ 4, diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/confirm_delete.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/confirm_delete.test.tsx.snap index d83e45fea1aece..9d670158bc53a7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/confirm_delete.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/confirm_delete.test.tsx.snap @@ -1,58 +1,54 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ML Confirm Job Delete shallow renders without errors 1`] = ` - - -

    - -

    -

    - -

    -
    -
    + +

    + +

    +

    + +

    +
    `; exports[`ML Confirm Job Delete shallow renders without errors while loading 1`] = ` - - -

    - - ) -

    - +

    + - - + ) +

    + +
    `; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap index fd59b14520ce17..23feec1e5181c7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap @@ -84,7 +84,7 @@ exports[`ML Flyout component shows license info if no ml available 1`] = ` data-eui="EuiFocusTrap" >