diff --git a/.ci/pipeline-library/src/test/slackNotifications.groovy b/.ci/pipeline-library/src/test/slackNotifications.groovy index f7e39f5fad903..33b3afed80bde 100644 --- a/.ci/pipeline-library/src/test/slackNotifications.groovy +++ b/.ci/pipeline-library/src/test/slackNotifications.groovy @@ -9,6 +9,7 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { super.setUp() helper.registerAllowedMethod('slackSend', [Map.class], null) + prop('buildState', loadScript("vars/buildState.groovy")) slackNotifications = loadScript('vars/slackNotifications.groovy') } @@ -25,13 +26,49 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { } @Test - void 'sendFailedBuild() should call slackSend() with message'() { + void 'sendFailedBuild() should call slackSend() with an in-progress message'() { mockFailureBuild() slackNotifications.sendFailedBuild() def args = fnMock('slackSend').args[0] + def expected = [ + channel: '#kibana-operations-alerts', + username: 'Kibana Operations', + iconEmoji: ':jenkins:', + color: 'danger', + message: ':hourglass_flowing_sand: elastic / kibana # master #1', + ] + + expected.each { + assertEquals(it.value.toString(), args[it.key].toString()) + } + + assertEquals( + ":hourglass_flowing_sand: **", + args.blocks[0].text.text.toString() + ) + + assertEquals( + "*Failed Steps*\n• ", + args.blocks[1].text.text.toString() + ) + + assertEquals( + "*Test Failures*\n• ", + args.blocks[2].text.text.toString() + ) + } + + @Test + void 'sendFailedBuild() should call slackSend() with message'() { + mockFailureBuild() + + slackNotifications.sendFailedBuild(isFinal: true) + + def args = fnMock('slackSend').args[0] + def expected = [ channel: '#kibana-operations-alerts', username: 'Kibana Operations', @@ -65,7 +102,7 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { mockFailureBuild() def counter = 0 helper.registerAllowedMethod('slackSend', [Map.class], { ++counter > 1 }) - slackNotifications.sendFailedBuild() + slackNotifications.sendFailedBuild(isFinal: true) def args = fnMocks('slackSend')[1].args[0] @@ -88,6 +125,29 @@ class SlackNotificationsTest extends KibanaBasePipelineTest { ) } + @Test + void 'sendFailedBuild() should call slackSend() with a channel id and timestamp on second call'() { + mockFailureBuild() + helper.registerAllowedMethod('slackSend', [Map.class], { [ channelId: 'CHANNEL_ID', ts: 'TIMESTAMP' ] }) + slackNotifications.sendFailedBuild(isFinal: false) + slackNotifications.sendFailedBuild(isFinal: true) + + def args = fnMocks('slackSend')[1].args[0] + + def expected = [ + channel: 'CHANNEL_ID', + timestamp: 'TIMESTAMP', + username: 'Kibana Operations', + iconEmoji: ':jenkins:', + color: 'danger', + message: ':broken_heart: elastic / kibana # master #1', + ] + + expected.each { + assertEquals(it.value.toString(), args[it.key].toString()) + } + } + @Test void 'getTestFailures() should truncate list of failures to 10'() { prop('testUtils', [ diff --git a/.eslintrc.js b/.eslintrc.js index c9f9d96f9ddae..b3d29c9866411 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -529,6 +529,7 @@ module.exports = { 'x-pack/test_utils/**/*', 'x-pack/gulpfile.js', 'x-pack/plugins/apm/public/utils/testHelpers.js', + 'x-pack/plugins/canvas/shareable_runtime/postcss.config.js', ], rules: { 'import/no-extraneous-dependencies': [ diff --git a/Jenkinsfile b/Jenkinsfile index 818ba748ee165..ad1d244c78874 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,59 +4,60 @@ library 'kibana-pipeline-library' kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) { - githubPr.withDefaultPrComments { - ciStats.trackBuild { - catchError { - retryable.enable() - parallel([ - 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), - 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), - 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), - 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), - 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), - 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), - 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), - 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), - 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), - 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), - 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), - 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), - 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), - 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), - // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), - ]), - 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), - 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), - 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), - 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), - 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), - 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), - 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), - 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), - 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), - 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), - 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), - 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), - 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), - 'xpack-securitySolutionCypress': { processNumber -> - whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/', 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx']) { - kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) - } - }, + slackNotifications.onFailure(disabled: !params.NOTIFY_ON_FAILURE) { + githubPr.withDefaultPrComments { + ciStats.trackBuild { + catchError { + retryable.enable() + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), + // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), + 'xpack-securitySolutionCypress': { processNumber -> + whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/', 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx']) { + kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) + } + }, - // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), - ]), - ]) + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) + } } } } if (params.NOTIFY_ON_FAILURE) { - slackNotifications.onFailure() kibanaPipeline.sendMail() } } diff --git a/docs/apm/images/apm-anomaly-alert.png b/docs/apm/images/apm-anomaly-alert.png new file mode 100644 index 0000000000000..35ce9a2296c9c Binary files /dev/null and b/docs/apm/images/apm-anomaly-alert.png differ diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc index b203b8668072f..db2a1ef6e2da0 100644 --- a/docs/apm/machine-learning.asciidoc +++ b/docs/apm/machine-learning.asciidoc @@ -1,36 +1,61 @@ [role="xpack"] [[machine-learning-integration]] -=== integration +=== Machine learning integration ++++ Integrate with machine learning ++++ -The Machine Learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. -Jobs can be created per transaction type, and are based on the service's average response time. +The Machine learning integration initiates a new job predefined to calculate anomaly scores on APM transaction durations. +With this integration, you can quickly pinpoint anomalous transactions and see the health of +any upstream and downstream services. -After a machine learning job is created, results are shown in two places: +Machine learning jobs are created per environment, and are based on a service's average response time. +Because jobs are created at the environment level, +you can add new services to your existing environments without the need for additional machine learning jobs. -The transaction duration graph will show the expected bounds and add an annotation when the anomaly score is 75 or above. +After a machine learning job is created, results are shown in two places: +* The transaction duration chart will show the expected bounds and add an annotation when the anomaly score is 75 or above. ++ [role="screenshot"] image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in the APM app] -Service maps will display a color-coded anomaly indicator based on the detected anomaly score. - +* Service maps will display a color-coded anomaly indicator based on the detected anomaly score. ++ [role="screenshot"] image::apm/images/apm-service-map-anomaly.png[Example view of anomaly scores on service maps in the APM app] [float] [[create-ml-integration]] -=== Create a new machine learning job +=== Enable anomaly detection + +To enable machine learning anomaly detection: + +. From the Services overview, Traces overview, or Service Map tab, +select **Anomaly detection**. + +. Click **Create ML Job**. -To enable machine learning anomaly detection, first choose a service to monitor. -Then, select **Integrations** > **Enable ML anomaly detection** and click **Create job**. +. Machine learning jobs are created at the environment level. +Select all of the service environments that you want to enable anomaly detection in. +Anomalies will surface for all services and transaction types within the selected environments. + +. Click **Create Jobs**. That's it! After a few minutes, the job will begin calculating results; -it might take additional time for results to appear on your graph. -Jobs can be managed in *Machine Learning jobs management*. +it might take additional time for results to appear on your service maps. +Existing jobs can be managed in *Machine Learning jobs management*. APM specific anomaly detection wizards are also available for certain Agents. See the machine learning {ml-docs}/ootb-ml-jobs-apm.html[APM anomaly detection configurations] for more information. + +[float] +[[warning-ml-integration]] +=== Anomaly detection warning + +To make machine learning as easy as possible to set up, +the APM app will warn you when filtered to an environment without a machine learning job. + +[role="screenshot"] +image::apm/images/apm-anomaly-alert.png[Example view of anomaly alert in the APM app] \ No newline at end of file diff --git a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md index 9bc24f4d1d366..4778c98493b5b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md @@ -4,6 +4,9 @@ ## AssistanceAPIResponse interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md index 8be7a9edde363..6d3f8df2fa518 100644 --- a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md @@ -4,6 +4,9 @@ ## AssistantAPIClientParams interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md index e545cf42d3c26..ed64d61e75fab 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md @@ -4,6 +4,9 @@ ## DeprecationAPIClientParams interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md index 1f6e1f9988fc2..1d837d9b4705d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md @@ -4,6 +4,9 @@ ## DeprecationAPIResponse interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md index bd343f5bc7476..8eeb5ef638a82 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md @@ -4,6 +4,9 @@ ## DeprecationInfo interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md new file mode 100644 index 0000000000000..1ba359e81b9c6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) + +## ElasticsearchClientConfig type + +Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) + +Signature: + +```typescript +export declare type ElasticsearchClientConfig = Pick & { + pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; + requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; + ssl?: Partial; + keepAlive?: boolean; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md new file mode 100644 index 0000000000000..591f126c423e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) > [client](./kibana-plugin-core-server.elasticsearchservicestart.client.md) + +## ElasticsearchServiceStart.client property + +A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) + +Signature: + +```typescript +readonly client: IClusterClient; +``` + +## Example + + +```js +const client = core.elasticsearch.client; + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md new file mode 100644 index 0000000000000..d4a13812ab533 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) > [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) + +## ElasticsearchServiceStart.createClient property + +Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). + +Signature: + +```typescript +readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; +``` + +## Example + + +```js +const client = elasticsearch.createClient('my-app-name', config); +const data = await client.asInternalUser.search(); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md index e059acdbd52fa..860867d654435 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md @@ -15,5 +15,7 @@ export interface ElasticsearchServiceStart | Property | Type | Description | | --- | --- | --- | +| [client](./kibana-plugin-core-server.elasticsearchservicestart.client.md) | IClusterClient | A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) | +| [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | | [legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | {
readonly createClient: (type: string, clientConfig?: Partial<LegacyElasticsearchClientConfig>) => ILegacyCustomClusterClient;
readonly client: ILegacyClusterClient;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md index 8958b49d98b0c..b0dc4d44f7559 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md @@ -21,7 +21,7 @@ registerRouteHandlerContext: (contextName 'myApp', (context, req) => { async function search (id: string) { - return await context.elasticsearch.legacy.client.callAsInternalUser('endpoint', id); + return await context.elasticsearch.client.asCurrentUser.find(id); } return { search }; } diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md new file mode 100644 index 0000000000000..c7adc345af5a3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) > [asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md) + +## IClusterClient.asInternalUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the ES cluster on behalf of the Kibana internal user + +Signature: + +```typescript +readonly asInternalUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md new file mode 100644 index 0000000000000..301fcbfee5858 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) > [asScoped](./kibana-plugin-core-server.iclusterclient.asscoped.md) + +## IClusterClient.asScoped property + +Creates a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md) bound to given [request](./kibana-plugin-core-server.scopeablerequest.md) + +Signature: + +```typescript +asScoped: (request: ScopeableRequest) => IScopedClusterClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md new file mode 100644 index 0000000000000..f6bacee322538 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) + +## IClusterClient interface + +Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). + +Signature: + +```typescript +export interface IClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the ES cluster on behalf of the Kibana internal user | +| [asScoped](./kibana-plugin-core-server.iclusterclient.asscoped.md) | (request: ScopeableRequest) => IScopedClusterClient | Creates a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md) bound to given [request](./kibana-plugin-core-server.scopeablerequest.md) | + diff --git a/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md new file mode 100644 index 0000000000000..5fa2e93cca75b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) > [close](./kibana-plugin-core-server.icustomclusterclient.close.md) + +## ICustomClusterClient.close property + +Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. + +Signature: + +```typescript +close: () => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md new file mode 100644 index 0000000000000..189a50b5d6c20 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) + +## ICustomClusterClient interface + +See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) + +Signature: + +```typescript +export interface ICustomClusterClient extends IClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [close](./kibana-plugin-core-server.icustomclusterclient.close.md) | () => Promise<void> | Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md index c70a5ac07c6ad..b5fbb3d54b972 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyClusterClient type +> Warning: This API is now obsolete. +> +> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). +> + Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md index a3cb8f1315021..4da121984d084 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyCustomClusterClient type +> Warning: This API is now obsolete. +> +> Use [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md). +> + Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md index 1263b85acb498..51d0b2e4882cb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyScopedClusterClient type +> Warning: This API is now obsolete. +> +> Use [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md). +> + Serves the same purpose as "normal" `ClusterClient` but exposes additional `callAsCurrentUser` method that doesn't use credentials of the Kibana internal user (as `callAsInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API. See [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md index 00f1659549078..706898c4ad9aa 100644 --- a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md @@ -4,6 +4,9 @@ ## IndexSettingsDeprecationInfo interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md new file mode 100644 index 0000000000000..ddc6357bb8835 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) > [asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md) + +## IScopedClusterClient.asCurrentUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the user that initiated the request to the Kibana server. + +Signature: + +```typescript +readonly asCurrentUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md new file mode 100644 index 0000000000000..f7f308aa13161 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) > [asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md) + +## IScopedClusterClient.asInternalUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the internal Kibana user. + +Signature: + +```typescript +readonly asInternalUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md new file mode 100644 index 0000000000000..f39db268288a6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) + +## IScopedClusterClient interface + +Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. + +Signature: + +```typescript +export interface IScopedClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the user that initiated the request to the Kibana server. | +| [asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the internal Kibana user. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md new file mode 100644 index 0000000000000..c9f8ab11f6b12 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.completed_.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) > [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) + +## KibanaRequestEvents.completed$ property + +Observable that emits once if and when the request has been completely handled. + +Signature: + +```typescript +completed$: Observable; +``` + +## Remarks + +The request may be considered completed if: - A response has been sent to the client; or - The request was aborted. + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md index 21826c8b29383..dfd7efd27cb5a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md @@ -17,4 +17,5 @@ export interface KibanaRequestEvents | Property | Type | Description | | --- | --- | --- | | [aborted$](./kibana-plugin-core-server.kibanarequestevents.aborted_.md) | Observable<void> | Observable that emits once if and when the request has been aborted. | +| [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) | Observable<void> | Observable that emits once if and when the request has been completely handled. | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md index e6c2878d2b355..168209659046e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md @@ -4,6 +4,9 @@ ## LegacyAPICaller interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md index 9ebe2fc57a54b..40def157114ef 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md @@ -4,6 +4,10 @@ ## LegacyCallAPIOptions interface +> Warning: This API is now obsolete. +> +> + The set of options that defines how API call should be made and result be processed. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md index c51f1858c97a5..668d0b2866a26 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md @@ -4,6 +4,12 @@ ## LegacyClusterClient class +> Warning: This API is now obsolete. +> +> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). +> + +Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md index 62b0f216c863c..78f7bf582d355 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md @@ -4,6 +4,9 @@ ## LegacyElasticsearchClientConfig type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md index f760780504e55..40fc1a8e05a68 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md @@ -4,6 +4,7 @@ ## LegacyElasticsearchError interface +@deprecated. The new elasticsearch client doesn't wrap errors anymore. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md index c4a94d8661c47..7f752d70921ba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md @@ -4,6 +4,12 @@ ## LegacyScopedClusterClient class +> Warning: This API is now obsolete. +> +> Use [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md). +> + +Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 95b7627398b45..5347f0b55e19b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -20,9 +20,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CspConfig](./kibana-plugin-core-server.cspconfig.md) | CSP configuration for use in Kibana. | | [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) | Wrapper of config schema. | | [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | -| [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) | | +| [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [LegacyElasticsearchErrorHelpers](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | -| [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) | | +| [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [RouteValidationError](./kibana-plugin-core-server.routevalidationerror.md) | Error to return when the validation is not successful. | | [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | @@ -98,20 +98,23 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | +| [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. | +| [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) | See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | | [IKibanaResponse](./kibana-plugin-core-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [ImageValidation](./kibana-plugin-core-server.imagevalidation.md) | | | [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) | | | [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. | +| [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) | | | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | -| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | | +| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. | | [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | | | [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) | | | [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) | | @@ -137,7 +140,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | @@ -236,6 +239,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ConfigPath](./kibana-plugin-core-server.configpath.md) | | | [DestructiveRouteMethod](./kibana-plugin-core-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | | [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. | +| [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | | [Freezable](./kibana-plugin-core-server.freezable.md) | | | [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md index 69fb573d36530..a924f0cea6b6b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md +++ b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md @@ -4,6 +4,9 @@ ## MIGRATION\_ASSISTANCE\_INDEX\_ACTION type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md index c3256eaa78331..0fcae8c847cb4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md +++ b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md @@ -4,6 +4,9 @@ ## MIGRATION\_DEPRECATION\_LEVEL type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index 2d31c24a077cb..5b8492ec5ece1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -13,6 +13,7 @@ core: { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 07e6dcbdae125..4e530973f9d50 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc new file mode 100644 index 0000000000000..6bc085b0f78b9 --- /dev/null +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -0,0 +1,202 @@ +[role="xpack"] +[[alerting-getting-started]] += Alerting and Actions + +beta[] + +-- + +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. + +image::images/alerting-overview.png[Alerts and actions UI] + +[IMPORTANT] +============================================== +To make sure you can access alerting and actions, see the <> section. +============================================== + +[float] +== Concepts and terminology + +*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. +Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. +This section describes all of these elements and how they operate together. + +[float] +=== What is an alert? + +An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: + +* *Conditions*: what needs to be detected? +* *Schedule*: when/how often should detection checks run? +* *Actions*: what happens when a condition is detected? + +For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). + +image::images/what-is-an-alert.svg[Three components of an alert] + +The following sections each part of the alert is described in more detail. + +[float] +[[alerting-concepts-conditions]] +==== Conditions + +Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. + +These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters +to control the details of the conditions to detect. + +For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. + +See <> for the types of alerts provided by {kib} and how they express their conditions. + +[float] +[[alerting-concepts-scheduling]] +==== Schedule + +Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. + +[IMPORTANT] +============================================== +The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +============================================== + +[float] +[[alerting-concepts-actions]] +==== Actions + +Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. + +When defining actions in an alert, you specify: + +* the *action type*: the type of service or integration to use +* the connection for that type by referencing a <> +* a mapping of alert values to properties exposed for that type of action + +The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. + +In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. + +When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. + +image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] + +See <> for details on the types of actions provided by {kib}. + +[float] +[[alerting-concepts-alert-instances]] +=== Alert instances + +When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. + +Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. + +image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] + +[float] +[[alerting-concepts-suppressing-duplicate-notifications]] +=== Suppressing duplicate notifications + +Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. + +In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 +* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. + +[float] +[[alerting-concepts-connectors]] +=== Connectors + +Actions often involve connecting with services inside {kib} or integrations with third-party systems. +Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. + +*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, +they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. + +image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] + +[float] +=== Summary + +An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. + +image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] + +* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. +* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. +* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. +* *Connector*: centralized configurations for services and third party integration that are referenced by actions. + +[float] +[[alerting-concepts-differences]] +== Differences from Watcher + +{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. + +This section will clarify some of the important differences in the function and intent of the two systems. + +Functionally, {kib} alerting differs in that: + +* Scheduled checks are run on {kib} instead of {es} +* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. +* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. +* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. + +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. + +[float] +[[alerting-setup-prerequisites]] +== Setup and prerequisites + +If you are using an *on-premises* Elastic Stack deployment: + +* In the kibana.yml configuration file, add the <> setting. + +If you are using an *on-premises* Elastic Stack deployment with <>: + +* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. + +[float] +[[alerting-security]] +== Security + +To access alerting in a space, a user must have access to one of the following features: + +* <> +* <> +* <> +* <> + +See <> for more information on configuring roles that provide access to these features. + +[float] +[[alerting-spaces]] +=== Space isolation + +Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. + +[float] +[[alerting-authorization]] +=== Authorization + +Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. + +[IMPORTANT] +============================================== +If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. +============================================== + +[float] +[[alerting-restricting-actions]] +=== Restricting actions + +For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. + +-- \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 6f691f2715bc8..56404d9a33b80 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,205 +1,4 @@ -[role="xpack"] -[[alerting-getting-started]] -= Alerting and Actions - -beta[] - --- - -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. - -image::images/alerting-overview.png[Alerts and actions UI] - -[IMPORTANT] -============================================== -To make sure you can access alerting and actions, see the <> section. -============================================== - -[float] -== Concepts and terminology - -*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. -Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. -This section describes all of these elements and how they operate together. - -[float] -=== What is an alert? - -An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: - -* *Conditions*: what needs to be detected? -* *Schedule*: when/how often should detection checks run? -* *Actions*: what happens when a condition is detected? - -For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). - -image::images/what-is-an-alert.svg[Three components of an alert] - -The following sections each part of the alert is described in more detail. - -[float] -[[alerting-concepts-conditions]] -==== Conditions - -Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. - -These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters -to control the details of the conditions to detect. - -For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. - -See <> for the types of alerts provided by {kib} and how they express their conditions. - -[float] -[[alerting-concepts-scheduling]] -==== Schedule - -Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. - -[IMPORTANT] -============================================== -The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. -============================================== - -[float] -[[alerting-concepts-actions]] -==== Actions - -Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. - -When defining actions in an alert, you specify -* the *action type*: the type of service or integration to use> -* the connection for that type by referencing a <>. -* a mapping of alert values to properties exposed for that type of action. - -The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. - -In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. - -When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. - -image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] - -See <> for details on the types of actions provided by {kib}. - -[float] -[[alerting-concepts-alert-instances]] -=== Alert instances - -When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. - -Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. - -image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] - -[float] -[[alerting-concepts-suppressing-duplicate-notifications]] -=== Suppressing duplicate notifications - -Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. - -In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 -* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. - -[float] -[[alerting-concepts-connectors]] -=== Connectors - -Actions often involve connecting with services inside {kib} or integrations with third-party systems. -Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. - -*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, -they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. - -image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] - -[float] -=== Summary - -An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. - -image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] - -* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. -* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. -* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. -* *Connector*: centralized configurations for services and third party integration that are referenced by actions. - -[float] -[[alerting-concepts-differences]] -== Differences from Watcher - -{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. - -This section will clarify some of the important differences in the function and intent of the two systems. - -Functionally, {kib} alerting differs in that: - -* Scheduled checks are run on {kib} instead of {es} -* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. -* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. -* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. - -At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. -Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. - -[float] -[[alerting-setup-prerequisites]] -== Setup and prerequisites - -If you are using an *on-premises* Elastic Stack deployment: - -* In the kibana.yml configuration file, add the <> setting. - -If you are using an *on-premises* Elastic Stack deployment with <>: - -* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. - -[float] -[[alerting-security]] -== Security - -To access alerting in a space, a user must have access to one of the following features: - -* <> -* <> -* <> -* <> - -See <> for more information on configuring roles that provide access to these features. - -[float] -[[alerting-spaces]] -=== Space isolation - -Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. - -[float] -[[alerting-authorization]] -=== Authorization - -Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. - -[IMPORTANT] -============================================== -If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. -============================================== - -[float] -[[alerting-restricting-actions]] -=== Restricting actions - -For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. - --- - +include::alerting-getting-started.asciidoc[] include::defining-alerts.asciidoc[] include::action-types.asciidoc[] include::alert-types.asciidoc[] diff --git a/package.json b/package.json index 51a41cbbab9ff..880534997cff0 100644 --- a/package.json +++ b/package.json @@ -480,7 +480,7 @@ "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-url": "^8.0.0", "prettier": "^2.0.5", "proxyquire": "1.8.0", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index c11bd1b646933..4fbbc920c4447 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -36,6 +36,7 @@ "loader-utils": "^1.2.3", "node-sass": "^4.13.0", "normalize-path": "^3.0.0", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "resolve-url-loader": "^3.1.1", diff --git a/packages/kbn-optimizer/src/worker/postcss.config.js b/packages/kbn-optimizer/postcss.config.js similarity index 100% rename from packages/kbn-optimizer/src/worker/postcss.config.js rename to packages/kbn-optimizer/postcss.config.js diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json new file mode 100644 index 0000000000000..10602d2e7981a --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json @@ -0,0 +1,4 @@ +{ + "id": "baz", + "ui": true +} diff --git a/packages/kbn-ui-framework/doc_site/postcss.config.js b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts similarity index 91% rename from packages/kbn-ui-framework/doc_site/postcss.config.js rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts index 571bae86dee37..7313de07be04c 100644 --- a/packages/kbn-ui-framework/doc_site/postcss.config.js +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts @@ -17,6 +17,5 @@ * under the License. */ -module.exports = { - plugins: [require('autoprefixer')()], -}; +// eslint-disable-next-line no-console +console.log('plugin in an x-pack dir'); diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index 6197a08485854..b8f9b94379f20 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -48,6 +48,7 @@ it('creates cache keys', () => { "/foo/bar/c": 789, }, "spec": Object { + "banner": undefined, "contextDir": "/foo/bar", "id": "bar", "manifestPath": undefined, @@ -80,6 +81,7 @@ it('parses bundles from JSON specs', () => { expect(bundles).toMatchInlineSnapshot(` Array [ Bundle { + "banner": undefined, "cache": BundleCache { "path": "/foo/bar/target/.kbn-optimizer-cache", "state": undefined, diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index a354da7a21521..25b37ace09a8f 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -43,6 +43,8 @@ export interface BundleSpec { readonly sourceRoot: string; /** Absolute path to the directory where output should be written */ readonly outputDir: string; + /** Banner that should be written to all bundle JS files */ + readonly banner?: string; /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */ readonly manifestPath?: string; } @@ -64,6 +66,8 @@ export class Bundle { public readonly sourceRoot: BundleSpec['sourceRoot']; /** Absolute path to the output directory for this bundle */ public readonly outputDir: BundleSpec['outputDir']; + /** Banner that should be written to all bundle JS files */ + public readonly banner: BundleSpec['banner']; /** * Absolute path to a manifest file with "requiredBundles" which will be * used to allow bundleRefs from this bundle to the exports of another bundle. @@ -81,6 +85,7 @@ export class Bundle { this.sourceRoot = spec.sourceRoot; this.outputDir = spec.outputDir; this.manifestPath = spec.manifestPath; + this.banner = spec.banner; this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); } @@ -112,6 +117,7 @@ export class Bundle { sourceRoot: this.sourceRoot, outputDir: this.outputDir, manifestPath: this.manifestPath, + banner: this.banner, }; } @@ -220,6 +226,13 @@ export function parseBundles(json: string) { } } + const { banner } = spec; + if (banner !== undefined) { + if (!(typeof banner === 'string')) { + throw new Error('`bundles[]` must have a string `banner` property'); + } + } + return new Bundle({ type, id, @@ -227,6 +240,7 @@ export function parseBundles(json: string) { contextDir, sourceRoot, outputDir, + banner, manifestPath, }); } diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 109188e163d06..5f44d8068e694 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -4,6 +4,7 @@ exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConf OptimizerConfig { "bundles": Array [ Bundle { + "banner": undefined, "cache": BundleCache { "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, "state": undefined, @@ -19,6 +20,7 @@ OptimizerConfig { "type": "plugin", }, Bundle { + "banner": undefined, "cache": BundleCache { "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, "state": undefined, @@ -33,6 +35,24 @@ OptimizerConfig { "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, + Bundle { + "banner": "/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. + * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ +", + "cache": BundleCache { + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz, + "id": "baz", + "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "type": "plugin", + }, ], "cache": true, "dist": false, @@ -60,6 +80,13 @@ OptimizerConfig { "isUiPlugin": false, "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz/kibana.json, }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz, + "extraPublicDirs": Array [], + "id": "baz", + "isUiPlugin": true, + "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + }, ], "profileWebpack": false, "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, @@ -73,6 +100,11 @@ OptimizerConfig { exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { it('builds expected bundles, saves bundle counts to metadata', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: false, }); @@ -100,7 +100,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { (msg.event?.type === 'bundle cached' || msg.event?.type === 'bundle not cached') && msg.state.phase === 'initializing' ); - assert('produce two bundle cache events while initializing', bundleCacheStates.length === 2); + assert('produce three bundle cache events while initializing', bundleCacheStates.length === 3); const initializedStates = msgs.filter((msg) => msg.state.phase === 'initialized'); assert('produce at least one initialized event', initializedStates.length >= 1); @@ -110,17 +110,17 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const runningStates = msgs.filter((msg) => msg.state.phase === 'running'); assert( - 'produce two or three "running" states', - runningStates.length === 2 || runningStates.length === 3 + 'produce three to five "running" states', + runningStates.length >= 3 && runningStates.length <= 5 ); const bundleNotCachedEvents = msgs.filter((msg) => msg.event?.type === 'bundle not cached'); - assert('produce two "bundle not cached" events', bundleNotCachedEvents.length === 2); + assert('produce three "bundle not cached" events', bundleNotCachedEvents.length === 3); const successStates = msgs.filter((msg) => msg.state.phase === 'success'); assert( - 'produce one or two "compiler success" states', - successStates.length === 1 || successStates.length === 2 + 'produce one to three "compiler success" states', + successStates.length >= 1 && successStates.length <= 3 ); const otherStates = msgs.filter( @@ -161,6 +161,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { Array [ /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, + /packages/kbn-optimizer/postcss.config.js, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, @@ -171,7 +172,20 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss, /packages/kbn-optimizer/target/worker/entry_point_creator.js, - /packages/kbn-optimizer/target/worker/postcss.config.js, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, + ] + `); + + const baz = config.bundles.find((b) => b.id === 'baz')!; + expect(baz).toBeTruthy(); + baz.cache.refresh(); + expect(baz.cache.getModuleCount()).toBe(3); + + expect(baz.cache.getReferencedFiles()).toMatchInlineSnapshot(` + Array [ + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts, + /packages/kbn-optimizer/target/worker/entry_point_creator.js, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); @@ -180,7 +194,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { it('uses cache on second run and exist cleanly', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: false, }); @@ -202,6 +216,7 @@ it('uses cache on second run and exist cleanly', async () => { "initializing", "initializing", "initializing", + "initializing", "initialized", "success", ] @@ -211,7 +226,7 @@ it('uses cache on second run and exist cleanly', async () => { it('prepares assets for distribution', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: true, }); @@ -224,6 +239,7 @@ it('prepares assets for distribution', async () => { 'foo async bundle' ); expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); + expectFileMatchesSnapshotWithCompression('x-pack/baz/target/public/baz.plugin.js', 'baz bundle'); }); /** diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index a70cfc759dd55..a823f66cf767b 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -48,12 +48,20 @@ it('returns a bundle for core and each plugin', () => { extraPublicDirs: [], manifestPath: '/outside/of/repo/plugins/baz/kibana.json', }, + { + directory: '/repo/x-pack/plugins/box', + id: 'box', + isUiPlugin: true, + extraPublicDirs: [], + manifestPath: '/repo/x-pack/plugins/box/kibana.json', + }, ], '/repo' ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ Object { + "banner": undefined, "contextDir": /plugins/foo, "id": "foo", "manifestPath": /plugins/foo/kibana.json, @@ -65,6 +73,7 @@ it('returns a bundle for core and each plugin', () => { "type": "plugin", }, Object { + "banner": undefined, "contextDir": "/outside/of/repo/plugins/baz", "id": "baz", "manifestPath": "/outside/of/repo/plugins/baz/kibana.json", @@ -75,6 +84,20 @@ it('returns a bundle for core and each plugin', () => { "sourceRoot": , "type": "plugin", }, + Object { + "banner": "/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. + * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ + ", + "contextDir": /x-pack/plugins/box, + "id": "box", + "manifestPath": /x-pack/plugins/box/kibana.json, + "outputDir": /x-pack/plugins/box/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": , + "type": "plugin", + }, ] `); }); diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 04ab992addeec..9350b9464242a 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -24,6 +24,8 @@ import { Bundle } from '../common'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { + const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; + return plugins .filter((p) => p.isUiPlugin) .map( @@ -36,6 +38,10 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri contextDir: p.directory, outputDir: Path.resolve(p.directory, 'target/public'), manifestPath: p.manifestPath, + banner: p.directory.startsWith(xpackDirSlash) + ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements.\n` + + ` * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */\n` + : undefined, }) ); } diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 271ad49aee351..ae5d2b5fb3292 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -72,6 +72,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: new CleanWebpackPlugin(), new DisallowedSyntaxPlugin(), new BundleRefsPlugin(bundle, bundleRefs), + ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], module: { @@ -151,7 +152,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: options: { sourceMap: !worker.dist, config: { - path: require.resolve('./postcss.config'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/lib/webpack.dll.config.js b/packages/kbn-storybook/lib/webpack.dll.config.js index 740ee3819c36f..661312b9a0581 100644 --- a/packages/kbn-storybook/lib/webpack.dll.config.js +++ b/packages/kbn-storybook/lib/webpack.dll.config.js @@ -127,7 +127,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(REPO_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index b2df4f40d4fbe..0a9977463aee8 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -91,7 +91,7 @@ module.exports = async ({ config }) => { loader: 'postcss-loader', options: { config: { - path: resolve(REPO_ROOT, 'src/optimize/'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-ui-framework/Gruntfile.js b/packages/kbn-ui-framework/Gruntfile.js index b7ba1e87b2f00..bb8e7b72cb7bd 100644 --- a/packages/kbn-ui-framework/Gruntfile.js +++ b/packages/kbn-ui-framework/Gruntfile.js @@ -19,7 +19,7 @@ const sass = require('node-sass'); const postcss = require('postcss'); -const postcssConfig = require('../../src/optimize/postcss.config'); +const postcssConfig = require('@kbn/optimizer/postcss.config.js'); const chokidar = require('chokidar'); const { debounce } = require('lodash'); diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index abf64906e0253..7933ce06d6847 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -33,7 +33,7 @@ "@babel/core": "^7.10.2", "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", - "autoprefixer": "^9.7.4", + "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", "brace": "0.11.1", "chalk": "^2.4.2", @@ -54,7 +54,7 @@ "keymirror": "0.1.1", "moment": "^2.24.0", "node-sass": "^4.13.1", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "react-dom": "^16.12.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 8398d1c081da6..3c03a52383f77 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -21,6 +21,7 @@ "custom-event-polyfill": "^0.3.0", "elasticsearch-browser": "^16.7.0", "jquery": "^3.5.0", + "mini-css-extract-plugin": "0.8.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", "react": "^16.12.0", diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 3fdab481dc750..4facbe1ffbb07 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -20,7 +20,7 @@ // eslint-disable-next-line no-restricted-syntax const alwaysImportedTests = [ require.resolve('../test/functional/config.js'), - require.resolve('../test/plugin_functional/config.js'), + require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), require.resolve('../test/new_visualize_flow/config.js'), ]; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index c93294404b52f..2f2ca08fee6f2 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -70,15 +70,14 @@ const createInternalClientMock = (): DeeplyMockedKeys => { return (mock as unknown) as DeeplyMockedKeys; }; -// TODO fix naming ElasticsearchClientMock -export type ElasticSearchClientMock = DeeplyMockedKeys; +export type ElasticsearchClientMock = DeeplyMockedKeys; -const createClientMock = (): ElasticSearchClientMock => - (createInternalClientMock() as unknown) as ElasticSearchClientMock; +const createClientMock = (): ElasticsearchClientMock => + (createInternalClientMock() as unknown) as ElasticsearchClientMock; interface ScopedClusterClientMock { - asInternalUser: ElasticSearchClientMock; - asCurrentUser: ElasticSearchClientMock; + asInternalUser: ElasticsearchClientMock; + asCurrentUser: ElasticsearchClientMock; } const createScopedClusterClientMock = () => { @@ -91,7 +90,7 @@ const createScopedClusterClientMock = () => { }; export interface ClusterClientMock { - asInternalUser: ElasticSearchClientMock; + asInternalUser: ElasticsearchClientMock; asScoped: jest.MockedFunction<() => ScopedClusterClientMock>; } @@ -157,7 +156,7 @@ export const elasticsearchClientMock = { createClusterClient: createClusterClientMock, createCustomClusterClient: createCustomClusterClientMock, createScopedClusterClient: createScopedClusterClientMock, - createElasticSearchClient: createClientMock, + createElasticsearchClient: createClientMock, createInternalClient: createInternalClientMock, createSuccessTransportRequestPromise, createErrorTransportRequestPromise, diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts index 3aa47e8b40e24..c9366c575ba74 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -27,10 +27,10 @@ const createErrorReturn = (err: any) => elasticsearchClientMock.createErrorTransportRequestPromise(err); describe('retryCallCluster', () => { - let client: ReturnType; + let client: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); }); it('returns response from ES API call in case of success', async () => { @@ -91,11 +91,11 @@ describe('retryCallCluster', () => { }); describe('migrationRetryCallCluster', () => { - let client: ReturnType; + let client: ReturnType; let logger: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); logger = loggingSystemMock.createLogger(); }); diff --git a/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts index 78ca8fcbd3c07..4288c6bf6421d 100644 --- a/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts @@ -22,8 +22,8 @@ import { ScopedClusterClient } from './scoped_cluster_client'; describe('ScopedClusterClient', () => { it('uses the internal client passed in the constructor', () => { - const internalClient = elasticsearchClientMock.createElasticSearchClient(); - const scopedClient = elasticsearchClientMock.createElasticSearchClient(); + const internalClient = elasticsearchClientMock.createElasticsearchClient(); + const scopedClient = elasticsearchClientMock.createElasticsearchClient(); const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); @@ -31,8 +31,8 @@ describe('ScopedClusterClient', () => { }); it('uses the scoped client passed in the constructor', () => { - const internalClient = elasticsearchClientMock.createElasticSearchClient(); - const scopedClient = elasticsearchClientMock.createElasticSearchClient(); + const internalClient = elasticsearchClientMock.createElasticsearchClient(); + const scopedClient = elasticsearchClientMock.createElasticsearchClient(); const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); diff --git a/src/core/server/elasticsearch/client/scoped_cluster_client.ts b/src/core/server/elasticsearch/client/scoped_cluster_client.ts index 1af7948a65e16..05ab67073f9e1 100644 --- a/src/core/server/elasticsearch/client/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/client/scoped_cluster_client.ts @@ -20,7 +20,7 @@ import { ElasticsearchClient } from './types'; /** - * Serves the same purpose as the normal {@link ClusterClient | cluster client} but exposes + * Serves the same purpose as the normal {@link IClusterClient | cluster client} but exposes * an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal * user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers * extracted from the current user request to the API instead. diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index b97f6df6b0afc..501ab619316c2 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -24,6 +24,7 @@ import { ClusterClientMock, CustomClusterClientMock, } from './client/mocks'; +import { ElasticsearchClientConfig } from './client'; import { legacyClientMock } from './legacy/mocks'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; @@ -38,12 +39,12 @@ interface MockedElasticSearchServiceSetup { }; } -type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup; - -interface MockedInternalElasticSearchServiceStart extends MockedElasticSearchServiceStart { +type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup & { client: ClusterClientMock; - createClient: jest.MockedFunction<() => CustomClusterClientMock>; -} + createClient: jest.MockedFunction< + (name: string, config?: Partial) => CustomClusterClientMock + >; +}; const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { @@ -61,6 +62,8 @@ const createSetupContractMock = () => { const createStartContractMock = () => { const startContract: MockedElasticSearchServiceStart = { + client: elasticsearchClientMock.createClusterClient(), + createClient: jest.fn(), legacy: { createClient: jest.fn(), client: legacyClientMock.createClusterClient(), @@ -70,20 +73,13 @@ const createStartContractMock = () => { startContract.legacy.client.asScoped.mockReturnValue( legacyClientMock.createScopedClusterClient() ); + startContract.createClient.mockImplementation(() => + elasticsearchClientMock.createCustomClusterClient() + ); return startContract; }; -const createInternalStartContractMock = () => { - const startContract: MockedInternalElasticSearchServiceStart = { - ...createStartContractMock(), - client: elasticsearchClientMock.createClusterClient(), - createClient: jest.fn(), - }; - - startContract.createClient.mockReturnValue(elasticsearchClientMock.createCustomClusterClient()); - - return startContract; -}; +const createInternalStartContractMock = createStartContractMock; type MockedInternalElasticSearchServiceSetup = jest.Mocked< InternalElasticsearchServiceSetup & { @@ -136,4 +132,6 @@ export const elasticsearchServiceMock = { createLegacyCustomClusterClient: legacyClientMock.createCustomClusterClient, createLegacyScopedClusterClient: legacyClientMock.createScopedClusterClient, createLegacyElasticsearchClient: legacyClientMock.createElasticsearchClient, + + ...elasticsearchClientMock, }; diff --git a/src/core/server/elasticsearch/legacy/api_types.ts b/src/core/server/elasticsearch/legacy/api_types.ts index b9699ab290e3f..896a58e085d49 100644 --- a/src/core/server/elasticsearch/legacy/api_types.ts +++ b/src/core/server/elasticsearch/legacy/api_types.ts @@ -150,6 +150,7 @@ import { * processed. * * @public + * @deprecated */ export interface LegacyCallAPIOptions { /** @@ -165,7 +166,10 @@ export interface LegacyCallAPIOptions { signal?: AbortSignal; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface LegacyAPICaller { /* eslint-disable */ (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: LegacyCallAPIOptions): ReturnType; @@ -317,18 +321,30 @@ export interface LegacyAPICaller { /* eslint-enable */ } -/** @public */ +/** + * @deprecated + * @public + * */ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; method: 'GET'; } -/** @public */ +/** + * @deprecated + * @public + * */ export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -/** @public */ +/** + * @deprecated + * @public + * */ export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; -/** @public */ +/** + * @deprecated + * @public + * */ export interface AssistanceAPIResponse { indices: { [indexName: string]: { @@ -337,13 +353,19 @@ export interface AssistanceAPIResponse { }; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationAPIClientParams extends GenericParams { path: '/_migration/deprecations'; method: 'GET'; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationInfo { level: MIGRATION_DEPRECATION_LEVEL; message: string; @@ -351,12 +373,18 @@ export interface DeprecationInfo { details?: string; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface IndexSettingsDeprecationInfo { [indexName: string]: DeprecationInfo[]; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationAPIResponse { cluster_settings: DeprecationInfo[]; ml_settings: DeprecationInfo[]; diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 7a39113d25a14..f8b2d39a4251c 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -88,6 +88,7 @@ const callAPI = async ( * * See {@link LegacyClusterClient}. * + * @deprecated Use {@link IClusterClient}. * @public */ export type ILegacyClusterClient = Pick; @@ -98,7 +99,7 @@ export type ILegacyClusterClient = Pick & diff --git a/src/core/server/elasticsearch/legacy/errors.ts b/src/core/server/elasticsearch/legacy/errors.ts index 3b3b8da51a907..de4d2739977bb 100644 --- a/src/core/server/elasticsearch/legacy/errors.ts +++ b/src/core/server/elasticsearch/legacy/errors.ts @@ -26,7 +26,10 @@ enum ErrorCode { NOT_AUTHORIZED = 'Elasticsearch/notAuthorized', } -/** @public */ +/** + * @deprecated. The new elasticsearch client doesn't wrap errors anymore. + * @public + * */ export interface LegacyElasticsearchError extends Boom { [code]?: string; } diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts index 9edb73645f0e2..aee7a1daa8166 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts @@ -30,6 +30,7 @@ import { LegacyAPICaller, LegacyCallAPIOptions } from './api_types'; * * See {@link LegacyScopedClusterClient}. * + * @deprecated Use {@link IScopedClusterClient}. * @public */ export type ILegacyScopedClusterClient = Pick< @@ -39,6 +40,7 @@ export type ILegacyScopedClusterClient = Pick< /** * {@inheritDoc IScopedClusterClient} + * @deprecated Use {@link IScopedClusterClient | scoped cluster client}. * @public */ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 40399aecbc446..88094af8047e7 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -95,6 +95,37 @@ export interface InternalElasticsearchServiceSetup { * @public */ export interface ElasticsearchServiceStart { + /** + * A pre-configured {@link IClusterClient | Elasticsearch client} + * + * @example + * ```js + * const client = core.elasticsearch.client; + * ``` + */ + readonly client: IClusterClient; + /** + * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. + * + * @param type Unique identifier of the client + * @param clientConfig A config consists of Elasticsearch JS client options and + * valid sub-set of Elasticsearch service config. + * We fill all the missing properties in the `clientConfig` using the default + * Elasticsearch config so that we don't depend on default values set and + * controlled by underlying Elasticsearch JS client. + * We don't run validation against the passed config and expect it to be valid. + * + * @example + * ```js + * const client = elasticsearch.createClient('my-app-name', config); + * const data = await client.asInternalUser.search(); + * ``` + */ + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; + /** * @deprecated * Provided for the backward compatibility. @@ -138,38 +169,7 @@ export interface ElasticsearchServiceStart { /** * @internal */ -export interface InternalElasticsearchServiceStart extends ElasticsearchServiceStart { - /** - * A pre-configured {@link IClusterClient | Elasticsearch client} - * - * @example - * ```js - * const client = core.elasticsearch.client; - * ``` - */ - readonly client: IClusterClient; - /** - * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. - * - * @param type Unique identifier of the client - * @param clientConfig A config consists of Elasticsearch JS client options and - * valid sub-set of Elasticsearch service config. - * We fill all the missing properties in the `clientConfig` using the default - * Elasticsearch config so that we don't depend on default values set and - * controlled by underlying Elasticsearch JS client. - * We don't run validation against the passed config and expect it to be valid. - * - * @example - * ```js - * const client = elasticsearch.createClient('my-app-name', config); - * const data = await client.asInternalUser().search(); - * ``` - */ - readonly createClient: ( - type: string, - clientConfig?: Partial - ) => ICustomClusterClient; -} +export type InternalElasticsearchServiceStart = ElasticsearchServiceStart; /** @public */ export interface ElasticsearchStatusMeta { diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 2d018f7f464b5..3a7335583296e 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -23,6 +23,7 @@ import { HttpService } from '../http_service'; import { contextServiceMock } from '../../context/context_service.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createHttpServer } from '../test_utils'; +import { schema } from '@kbn/config-schema'; let server: HttpService; @@ -195,6 +196,96 @@ describe('KibanaRequest', () => { expect(nextSpy).toHaveBeenCalledTimes(0); expect(completeSpy).toHaveBeenCalledTimes(1); }); + + it('does not complete before response has been sent', async () => { + const { server: innerServer, createRouter, registerOnPreAuth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + const completeSpy = jest.fn(); + + registerOnPreAuth((req, res, toolkit) => { + req.events.aborted$.subscribe({ + next: nextSpy, + complete: completeSpy, + }); + return toolkit.next(); + }); + + router.post( + { path: '/', validate: { body: schema.any() } }, + async (context, request, res) => { + expect(completeSpy).not.toHaveBeenCalled(); + return res.ok({ body: 'ok' }); + } + ); + + await server.start(); + + await supertest(innerServer.listener).post('/').send({ data: 'test' }).expect(200); + + expect(nextSpy).toHaveBeenCalledTimes(0); + expect(completeSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('completed$', () => { + it('emits once and completes when response is sent', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + const completeSpy = jest.fn(); + + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: completeSpy, + }); + + expect(nextSpy).not.toHaveBeenCalled(); + expect(completeSpy).not.toHaveBeenCalled(); + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + await supertest(innerServer.listener).get('/').expect(200); + expect(nextSpy).toHaveBeenCalledTimes(1); + expect(completeSpy).toHaveBeenCalledTimes(1); + }); + + it('emits once and completes when response is aborted', async (done) => { + expect.assertions(2); + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + const nextSpy = jest.fn(); + + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: () => { + expect(nextSpy).toHaveBeenCalledTimes(1); + done(); + }, + }); + + expect(nextSpy).not.toHaveBeenCalled(); + await delay(30000); + return res.ok({ body: 'ok' }); + }); + + await server.start(); + + const incomingRequest = supertest(innerServer.listener) + .get('/') + // end required to send request + .end(); + setTimeout(() => incomingRequest.abort(), 50); + }); }); }); }); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 0e73431fe7c6d..93ffb5aa48259 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -64,6 +64,16 @@ export interface KibanaRequestEvents { * Observable that emits once if and when the request has been aborted. */ aborted$: Observable; + + /** + * Observable that emits once if and when the request has been completely handled. + * + * @remarks + * The request may be considered completed if: + * - A response has been sent to the client; or + * - The request was aborted. + */ + completed$: Observable; } /** @@ -186,11 +196,16 @@ export class KibanaRequest< private getEvents(request: Request): KibanaRequestEvents { const finish$ = merge( - fromEvent(request.raw.req, 'end'), // all data consumed + fromEvent(request.raw.res, 'finish'), // Response has been sent fromEvent(request.raw.req, 'close') // connection was closed ).pipe(shareReplay(1), first()); + + const aborted$ = fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)); + const completed$ = merge(finish$, aborted$).pipe(shareReplay(1), first()); + return { - aborted$: fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)), + aborted$, + completed$, } as const; } diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 3df098a1df00d..4345783e46e11 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -250,7 +250,7 @@ export interface HttpServiceSetup { * 'myApp', * (context, req) => { * async function search (id: string) { - * return await context.elasticsearch.legacy.client.callAsInternalUser('endpoint', id); + * return await context.elasticsearch.client.asCurrentUser.find(id); * } * return { search }; * } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index f46b41d6b8793..382318ea86a34 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -44,6 +44,7 @@ import { ILegacyScopedClusterClient, configSchema as elasticsearchConfigSchema, ElasticsearchServiceStart, + IScopedClusterClient, } from './elasticsearch'; import { HttpServiceSetup, HttpServiceStart } from './http'; @@ -110,6 +111,10 @@ export { FakeRequest, ScopeableRequest, ElasticsearchClient, + IClusterClient, + ICustomClusterClient, + ElasticsearchClientConfig, + IScopedClusterClient, SearchResponse, CountResponse, ShardsInfo, @@ -367,10 +372,13 @@ export { * which uses the credentials of the incoming request * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing * all the registered types. - * - {@link LegacyScopedClusterClient | elasticsearch.legacy.client} - Elasticsearch + * - {@link IScopedClusterClient | elasticsearch.client} - Elasticsearch + * data client which uses the credentials of the incoming request + * - {@link LegacyScopedClusterClient | elasticsearch.legacy.client} - The legacy Elasticsearch * data client which uses the credentials of the incoming request * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client * which uses the credentials of the incoming request + * - {@link Auditor | uiSettings.auditor} - AuditTrail client scoped to the incoming request * * @public */ @@ -381,6 +389,7 @@ export interface RequestHandlerContext { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 84e4b4741b717..bf9dcc4abe01c 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -193,6 +193,7 @@ function createCoreRequestHandlerContextMock() { typeRegistry: savedObjectsTypeRegistryMock.create(), }, elasticsearch: { + client: elasticsearchServiceMock.createScopedClusterClient(), legacy: { client: elasticsearchServiceMock.createLegacyScopedClusterClient(), }, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index c17b8df8bb52c..5235f3ee6d580 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -212,6 +212,8 @@ export function createPluginStartContext( resolveCapabilities: deps.capabilities.resolveCapabilities, }, elasticsearch: { + client: deps.elasticsearch.client, + createClient: deps.elasticsearch.createClient, legacy: deps.elasticsearch.legacy, }, http: { diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 95230b52c5c03..eab29731ea524 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -10,37 +10,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -83,37 +64,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -156,37 +118,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -233,37 +176,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/translations/en.json", @@ -306,37 +230,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -379,37 +284,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -452,37 +338,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/translations/en.json", @@ -525,37 +392,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -600,37 +448,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", @@ -673,37 +502,18 @@ Object { "warnLegacyBrowsers": true, }, "env": Object { - "binDir": Any, - "cliArgs": Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "runExamples": false, - "silent": false, - "watch": false, - }, - "configDir": Any, - "configs": Array [], - "homeDir": Any, - "isDevClusterMaster": false, - "logDir": Any, "mode": Object { - "dev": true, - "name": "development", - "prod": false, + "dev": Any, + "name": Any, + "prod": Any, }, "packageInfo": Object { "branch": Any, "buildNum": Any, "buildSha": Any, - "dist": false, + "dist": Any, "version": Any, }, - "pluginSearchPaths": Any, }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index d1c527aca4dba..7caf4af850c10 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -30,17 +30,18 @@ const INJECTED_METADATA = { branch: expect.any(String), buildNumber: expect.any(Number), env: { - binDir: expect.any(String), - configDir: expect.any(String), - homeDir: expect.any(String), - logDir: expect.any(String), + mode: { + name: expect.any(String), + dev: expect.any(Boolean), + prod: expect.any(Boolean), + }, packageInfo: { branch: expect.any(String), buildNum: expect.any(Number), buildSha: expect.any(String), + dist: expect.any(Boolean), version: expect.any(String), }, - pluginSearchPaths: expect.any(Array), }, legacyMetadata: { branch: expect.any(String), diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 8f87d62496891..f49952ec713fb 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -55,7 +55,10 @@ export class RenderingService implements CoreService { - let client: ReturnType; + let client: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); }); describe('fetchInfo', () => { test('it handles 404', async () => { diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 78601d033f8d8..b0669774207dd 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -27,13 +27,13 @@ import { loggingSystemMock } from '../../../logging/logging_system.mock'; describe('IndexMigrator', () => { let testOpts: jest.Mocked & { - client: ReturnType; + client: ReturnType; }; beforeEach(() => { testOpts = { batchSize: 10, - client: elasticsearchClientMock.createElasticSearchClient(), + client: elasticsearchClientMock.createElasticsearchClient(), index: '.kibana', log: loggingSystemMock.create().get(), mappingProperties: {}, @@ -366,7 +366,7 @@ describe('IndexMigrator', () => { }); function withIndex( - client: ReturnType, + client: ReturnType, opts: any = {} ) { const defaultIndex = { diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts index 40c06677c4a5a..a6da62095060c 100644 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts @@ -24,11 +24,11 @@ import { loggerMock } from '../../../logging/logger.mock'; import { SavedObjectsErrorHelpers } from '../../service/lib/errors'; describe('MigrationEsClient', () => { - let client: ReturnType; + let client: ReturnType; let migrationEsClient: MigrationEsClient; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); migrationEsClient = createMigrationEsClient(client, loggerMock.create()); migrationRetryCallClusterMock.mockClear(); }); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index c3ed97a89af80..cc443093e30a3 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -127,7 +127,7 @@ describe('KibanaMigrator', () => { }); type MockedOptions = KibanaMigratorOptions & { - client: ReturnType; + client: ReturnType; }; const mockOptions = () => { @@ -170,7 +170,7 @@ const mockOptions = () => { scrollDuration: '10m', skip: false, }, - client: elasticsearchClientMock.createElasticSearchClient(), + client: elasticsearchClientMock.createElasticsearchClient(), }; return options; }; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index b902179b012ff..4a9fceb9bf357 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -201,7 +201,7 @@ describe('SavedObjectsRepository', () => { }; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); migrator = { migrateDocument: jest.fn().mockImplementation(documentMigrator.migrate), runMigrations: async () => ({ status: 'skipped' }), diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.test.ts b/src/core/server/saved_objects/service/lib/repository_es_client.test.ts index 86a984fb67124..61df94fb6bfe2 100644 --- a/src/core/server/saved_objects/service/lib/repository_es_client.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_es_client.test.ts @@ -23,11 +23,11 @@ import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { SavedObjectsErrorHelpers } from './errors'; describe('RepositoryEsClient', () => { - let client: ReturnType; + let client: ReturnType; let repositoryClient: RepositoryEsClient; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); repositoryClient = createRepositoryEsClient(client); retryCallClusterMock.mockClear(); }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c94151f8cee17..21ef66230f698 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -158,7 +158,7 @@ export type AppenderConfigType = TypeOf; // @public export function assertNever(x: never): never; -// @public (undocumented) +// @public @deprecated (undocumented) export interface AssistanceAPIResponse { // (undocumented) indices: { @@ -168,7 +168,7 @@ export interface AssistanceAPIResponse { }; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface AssistantAPIClientParams extends GenericParams { // (undocumented) method: 'GET'; @@ -622,7 +622,7 @@ export interface DeleteDocumentResponse { _version: number; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationAPIClientParams extends GenericParams { // (undocumented) method: 'GET'; @@ -630,7 +630,7 @@ export interface DeprecationAPIClientParams extends GenericParams { path: '/_migration/deprecations'; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationAPIResponse { // (undocumented) cluster_settings: DeprecationInfo[]; @@ -642,7 +642,7 @@ export interface DeprecationAPIResponse { node_settings: DeprecationInfo[]; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationInfo { // (undocumented) details?: string; @@ -679,6 +679,14 @@ export type ElasticsearchClient = Omit & { + pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; + requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; + ssl?: Partial; + keepAlive?: boolean; +}; + // @public export class ElasticsearchConfig { constructor(rawConfig: ElasticsearchConfigType); @@ -715,6 +723,8 @@ export interface ElasticsearchServiceSetup { // @public (undocumented) export interface ElasticsearchServiceStart { + readonly client: IClusterClient; + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; // @deprecated (undocumented) legacy: { readonly createClient: (type: string, clientConfig?: Partial) => ILegacyCustomClusterClient; @@ -895,6 +905,12 @@ export interface HttpServiceStart { // @public export type IBasePath = Pick; +// @public +export interface IClusterClient { + readonly asInternalUser: ElasticsearchClient; + asScoped: (request: ScopeableRequest) => IScopedClusterClient; +} + // @public export interface IContextContainer> { createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; @@ -914,6 +930,11 @@ export interface ICspConfig { readonly warnLegacyBrowsers: boolean; } +// @public +export interface ICustomClusterClient extends IClusterClient { + close: () => Promise; +} + // @public export interface IKibanaResponse { // (undocumented) @@ -935,13 +956,13 @@ export interface IKibanaSocket { getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; } -// @public +// @public @deprecated export type ILegacyClusterClient = Pick; -// @public +// @public @deprecated export type ILegacyCustomClusterClient = Pick; -// @public +// @public @deprecated export type ILegacyScopedClusterClient = Pick; // @public (undocumented) @@ -956,7 +977,7 @@ export interface ImageValidation { // @public export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; -// @public (undocumented) +// @public @deprecated (undocumented) export interface IndexSettingsDeprecationInfo { // (undocumented) [indexName: string]: DeprecationInfo[]; @@ -997,6 +1018,12 @@ export type ISavedObjectsRepository = Pick; +// @public +export interface IScopedClusterClient { + readonly asCurrentUser: ElasticsearchClient; + readonly asInternalUser: ElasticsearchClient; +} + // @public export function isRelativeUrl(candidatePath: string): boolean; @@ -1044,6 +1071,7 @@ export class KibanaRequest; + completed$: Observable; } // @public @@ -1086,7 +1114,7 @@ export const kibanaResponseFactory: { // @public export type KnownHeaders = KnownKeys; -// @public (undocumented) +// @public @deprecated (undocumented) export interface LegacyAPICaller { // (undocumented) (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: LegacyCallAPIOptions): ReturnType; @@ -1330,15 +1358,13 @@ export interface LegacyAPICaller { (endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } -// @public +// @public @deprecated export interface LegacyCallAPIOptions { signal?: AbortSignal; wrap401Errors?: boolean; } -// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IClusterClient" -// -// @public (undocumented) +// @public @deprecated export class LegacyClusterClient implements ILegacyClusterClient { constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFactory: () => AuditorFactory, getAuthHeaders?: GetAuthHeaders); asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; @@ -1360,7 +1386,7 @@ export interface LegacyConfig { set(config: LegacyVars): void; } -// @public (undocumented) +// @public @deprecated (undocumented) export type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; @@ -1368,7 +1394,7 @@ export type LegacyElasticsearchClientConfig = Pick; }; -// @public (undocumented) +// @public export interface LegacyElasticsearchError extends Boom { // (undocumented) [code]?: string; @@ -1401,9 +1427,7 @@ export class LegacyInternals implements ILegacyInternals { export interface LegacyRequest extends Request { } -// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IScopedClusterClient" -// -// @public (undocumented) +// @public @deprecated export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); callAsCurrentUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; @@ -1559,10 +1583,10 @@ export interface LogRecord { export interface MetricsServiceSetup { } -// @public (undocumented) +// @public @deprecated (undocumented) export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -// @public (undocumented) +// @public @deprecated (undocumented) export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; // @public @@ -1812,6 +1836,7 @@ export interface RequestHandlerContext { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 2dae7f8f38f23..aff749ca97534 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -275,6 +275,7 @@ export class Server { typeRegistry: coreStart.savedObjects.getTypeRegistry(), }, elasticsearch: { + client: coreStart.elasticsearch.client.asScoped(req), legacy: { client: coreStart.elasticsearch.legacy.client.asScoped(req), }, diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index 8facbb709cc5c..449fc4e75fce8 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -22,7 +22,7 @@ pidfile="/var/run/kibana/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name [ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name -export KIBANA_PATH_CONF +export KBN_PATH_CONF export NODE_OPTIONS [ -z "$nice" ] && nice=0 diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index efc42405688d4..60172a3106276 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -79,7 +79,7 @@ export const DEV_ONLY_LICENSE_WHITELIST = ['MPL-2.0']; // Globally overrides a license for a given package@version export const LICENSE_OVERRIDES = { - 'jsts@1.1.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts + 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint // TODO can be removed if the https://github.com/jindw/xmldom/issues/239 is released diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 864bf7515053c..404ad67174681 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -105,6 +105,7 @@ export const IGNORE_DIRECTORY_GLOBS = [ 'test/functional/fixtures/es_archiver/visualize_source-filters', 'packages/kbn-pm/src/utils/__fixtures__/*', 'x-pack/dev-tools', + 'packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack', ]; /** diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 0221c7e0ea085..2cd780d21f681 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -19,6 +19,7 @@ import Joi from 'joi'; import boom from 'boom'; +import { defaultsDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { wrapAuthConfig } from '../../wrap_auth_config'; import { getKibanaInfoForStats } from '../../lib'; @@ -120,10 +121,9 @@ export function registerStatsApi(usageCollection, server, config, kbnServer) { }, }; } else { - accum = { - ...accum, - [usageKey]: usage[usageKey], - }; + // I don't think we need to it this for the above conditions, but do it for most as it will + // match the behavior done in monitoring/bulk_uploader + defaultsDeep(accum, { [usageKey]: usage[usageKey] }); } return accum; diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 41628a2264193..74973887ae9c1 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -34,7 +34,7 @@ import { IS_KIBANA_DISTRIBUTABLE } from '../legacy/utils'; import { fromRoot } from '../core/server/utils'; import { PUBLIC_PATH_PLACEHOLDER } from './public_path_placeholder'; -const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config'); +const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config.js'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); const EMPTY_MODULE_PATH = require.resolve('./intentionally_empty_module.js'); diff --git a/test/plugin_functional/services/supertest.ts b/src/plugins/dashboard/config.ts similarity index 66% rename from test/plugin_functional/services/supertest.ts rename to src/plugins/dashboard/config.ts index 6b7dc26248c06..ff968a51679e0 100644 --- a/test/plugin_functional/services/supertest.ts +++ b/src/plugins/dashboard/config.ts @@ -16,13 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { format as formatUrl } from 'url'; -import { FtrProviderContext } from 'test/functional/ftr_provider_context'; -import supertestAsPromised from 'supertest-as-promised'; +import { schema, TypeOf } from '@kbn/config-schema'; -export function KibanaSupertestProvider({ getService }: FtrProviderContext) { - const config = getService('config'); - const kibanaServerUrl = formatUrl(config.get('servers.kibana')); - return supertestAsPromised(kibanaServerUrl); -} +export const configSchema = schema.object({ + allowByValueEmbeddables: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index f0b57fec169fd..f1319665d258b 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -94,6 +94,10 @@ declare module '../../share/public' { export type DashboardUrlGenerator = UrlGeneratorContract; +interface DashboardFeatureFlagConfig { + allowByValueEmbeddables: boolean; +} + interface SetupDependencies { data: DataPublicPluginSetup; embeddable: EmbeddableSetup; @@ -125,6 +129,7 @@ export interface DashboardStart { embeddableType: string; }) => void | undefined; dashboardUrlGenerator?: DashboardUrlGenerator; + dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; } @@ -411,6 +416,7 @@ export class DashboardPlugin getSavedDashboardLoader: () => savedDashboardLoader, addEmbeddableToDashboard: this.addEmbeddableToDashboard.bind(this, core), dashboardUrlGenerator: this.dashboardUrlGenerator, + dashboardFeatureFlagConfig: this.initializerContext.config.get(), DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), diff --git a/src/plugins/dashboard/server/index.ts b/src/plugins/dashboard/server/index.ts index 9719586001c59..3ef7abba5776b 100644 --- a/src/plugins/dashboard/server/index.ts +++ b/src/plugins/dashboard/server/index.ts @@ -17,8 +17,16 @@ * under the License. */ -import { PluginInitializerContext } from '../../../core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../core/server'; import { DashboardPlugin } from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + allowByValueEmbeddables: true, + }, + schema: configSchema, +}; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 7ad2f9edd3325..d35a6a5bbb9a9 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -22,6 +22,7 @@ import { CatTasksParams } from 'elasticsearch'; import { CatThreadPoolParams } from 'elasticsearch'; import { ClearScrollParams } from 'elasticsearch'; import { Client } from 'elasticsearch'; +import { ClientOptions } from '@elastic/elasticsearch'; import { ClusterAllocationExplainParams } from 'elasticsearch'; import { ClusterGetSettingsParams } from 'elasticsearch'; import { ClusterHealthParams } from 'elasticsearch'; diff --git a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts index 64544a335c911..4369234a3ce9a 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts @@ -31,15 +31,6 @@ export function extractNanos(timeFieldValue: string = ''): string { return fractionSeconds.length !== 9 ? fractionSeconds.padEnd(9, '0') : fractionSeconds; } -/** - * extract the nanoseconds as string of a given ISO formatted timestamp - */ -export function convertIsoToNanosAsStr(isoValue: string): string { - const nanos = extractNanos(isoValue); - const millis = convertIsoToMillis(isoValue); - return `${millis}${nanos.substr(3, 6)}`; -} - /** * convert an iso formatted string to number of milliseconds since * 1970-01-01T00:00:00.000Z diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index d4ee9e0e0f287..24ac19a7e3bc3 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { convertIsoToNanosAsStr } from './date_conversion'; import { SurrDocType, EsHitRecordList, EsHitRecord } from '../context'; export type EsQuerySearchAfter = [string | number, string | number]; @@ -38,15 +37,10 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - const afterTimeValue = nanoSeconds - ? convertIsoToNanosAsStr(afterTimeDoc.fields[timeFieldName][0]) - : afterTimeDoc.sort[0]; + const afterTimeValue = nanoSeconds ? afterTimeDoc._source[timeFieldName] : afterTimeDoc.sort[0]; return [afterTimeValue, afterTimeDoc.sort[1]]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string - return [ - nanoSeconds ? convertIsoToNanosAsStr(anchor.fields[timeFieldName][0]) : anchor.sort[0], - anchor.sort[1], - ]; + return [nanoSeconds ? anchor._source[timeFieldName] : anchor.sort[0], anchor.sort[1]]; } diff --git a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts index d598f28a0ad12..0c1a44d7845cf 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts @@ -155,7 +155,7 @@ export function getVisualizeUrl( params: { field: field.name, size: parseInt(aggsTermSize, 10), - orderBy: '2', + orderBy: '1', }, }; } @@ -169,7 +169,7 @@ export function getVisualizeUrl( query: state.query, vis: { type, - aggs: [{ schema: 'metric', type: 'count', id: '2' }, agg], + aggs: [{ schema: 'metric', type: 'count', id: '1' }, agg], }, } as any), }, diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 3163c4bde4704..c9694ad7b9423 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -4,7 +4,6 @@ "server": false, "ui": true, "requiredPlugins": [ - "data", "inspector", "uiActions" ], diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 48e5483124704..6b451e71522c5 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -102,8 +102,6 @@ const createStartContract = (): Start => { getAttributeService: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), - filtersAndTimeRangeFromContext: jest.fn(), - filtersFromContext: jest.fn(), }; return startContract; }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 508c82c4247ed..319cbf8ec44b4 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -17,13 +17,7 @@ * under the License. */ import React from 'react'; -import { - DataPublicPluginSetup, - DataPublicPluginStart, - Filter, - TimeRange, - esFilters, -} from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { getSavedObjectFinder } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -44,9 +38,6 @@ import { IEmbeddable, EmbeddablePanel, SavedObjectEmbeddableInput, - ChartActionContext, - isRangeSelectTriggerContext, - isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { AttributeService } from './lib/embeddables/attribute_service'; @@ -92,18 +83,6 @@ export interface EmbeddableStart { type: string ) => AttributeService; - /** - * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. - */ - filtersFromContext: (context: ChartActionContext) => Promise; - - /** - * Returns possible time range and filters that can be constructed from {@link ChartActionContext} object. - */ - filtersAndTimeRangeFromContext: ( - context: ChartActionContext - ) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>; - EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -155,41 +134,6 @@ export class EmbeddablePublicPlugin implements Plugin { - try { - if (isRangeSelectTriggerContext(context)) - return await data.actions.createFiltersFromRangeSelectAction(context.data); - if (isValueClickTriggerContext(context)) - return await data.actions.createFiltersFromValueClickAction(context.data); - // eslint-disable-next-line no-console - console.warn("Can't extract filters from action.", context); - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Error extracting filters from action. Returning empty filter list.', error); - } - return []; - }; - - const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async ( - context - ) => { - const filters = await filtersFromContext(context); - - if (!context.data.timeFieldName) return { filters }; - - const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - context.data.timeFieldName, - filters - ); - - return { - filters: restOfFilters, - timeRange: timeRangeFilter - ? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter) - : undefined, - }; - }; - const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({ embeddable, hideHeader, @@ -216,8 +160,6 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), - filtersFromContext, - filtersAndTimeRangeFromContext, getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/expressions/common/expression_functions/specs/theme.ts b/src/plugins/expressions/common/expression_functions/specs/theme.ts index 8b1b2a1d7bd06..1db25b79076c5 100644 --- a/src/plugins/expressions/common/expression_functions/specs/theme.ts +++ b/src/plugins/expressions/common/expression_functions/specs/theme.ts @@ -46,7 +46,7 @@ export const theme: ExpressionFunctionTheme = { variable: { aliases: ['_'], help: i18n.translate('expressions.functions.theme.args.variableHelpText', { - defaultMessage: 'theme variable to read.', + defaultMessage: 'Name of the theme variable to read.', }), required: true, types: ['string'], diff --git a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts index 7a95709ac28ba..fa9ace1a36c69 100644 --- a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts @@ -22,6 +22,6 @@ import { Trigger } from '.'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = { id: APPLY_FILTER_TRIGGER, - title: 'Filter click', + title: 'Apply filter', description: 'Triggered when user applies filter to an embeddable.', }; diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index a9b542af68c9d..dd748ea2d3815 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -42,5 +42,7 @@ export const tsvbTelemetrySavedObjectType: SavedObjectsType = { migrations: { '7.7.0': flow(resetCount), '7.8.0': flow(resetCount), + '7.9.0': flow(resetCount), + '7.10.0': flow(resetCount), }, }; diff --git a/tasks/config/run.js b/tasks/config/run.js index 9ac8f72d56d4a..132b51765b3ed 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -210,7 +210,7 @@ module.exports = function () { args: [ 'scripts/functional_tests', '--config', - 'test/plugin_functional/config.js', + 'test/plugin_functional/config.ts', '--bail', '--debug', ], diff --git a/test/api_integration/services/index.ts b/test/api_integration/services/index.ts index 782ea271869ba..d024943bef792 100644 --- a/test/api_integration/services/index.ts +++ b/test/api_integration/services/index.ts @@ -19,7 +19,6 @@ import { services as commonServices } from '../../common/services'; -// @ts-ignore not TS yet import { KibanaSupertestProvider, ElasticsearchSupertestProvider } from './supertest'; export const services = { diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index 89769caaea253..cdf2d6c04be83 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -30,8 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // FLAKY/FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos', () => { + describe('context view for date_nanos', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 6329f6c431e6a..8fe08d13af0aa 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -30,10 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // skipped due to a recent change in ES that caused search_after queries with data containing - // custom timestamp formats like in the testdata to fail - // https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos with custom timestamp', () => { + describe('context view for date_nanos with custom timestamp', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); await esArchiver.loadIfNeeded('date_nanos_custom'); diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index e202dcb7e2af7..b0db6c149e41a 100644 --- a/test/functional/apps/discover/_field_visualize.ts +++ b/test/functional/apps/discover/_field_visualize.ts @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const log = getService('log'); const queryBar = getService('queryBar'); - const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'visualize']); const defaultSettings = { defaultIndex: 'logstash-*', }; @@ -48,6 +48,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + it('should be able to visualize a field and save the visualization', async () => { + await PageObjects.discover.findFieldByName('type'); + log.debug('visualize a type field'); + await PageObjects.discover.clickFieldListItemVisualize('type'); + await PageObjects.visualize.saveVisualizationExpectSuccess('Top 5 server types'); + }); + it('should visualize a field in area chart', async () => { await PageObjects.discover.findFieldByName('phpmemory'); log.debug('visualize a phpmemory field'); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 8f69bf629ce28..c558d9e2d8a31 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -17,11 +17,9 @@ * under the License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function DiscoverPageProvider({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); const find = getService('find'); @@ -51,9 +49,16 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async saveSearch(searchName: string) { - log.debug('saveSearch'); await this.clickSaveSearchButton(); - await testSubjects.setValue('savedObjectTitle', searchName); + // preventing an occasional flakiness when the saved object wasn't set and the form can't be submitted + await retry.waitFor( + `saved search title is set to ${searchName} and save button is clickable`, + async () => { + const saveButton = await testSubjects.find('confirmSaveSavedObjectButton'); + await testSubjects.setValue('savedObjectTitle', searchName); + return (await saveButton.getAttribute('disabled')) !== 'true'; + } + ); await testSubjects.click('confirmSaveSavedObjectButton'); await header.waitUntilLoadingHasFinished(); // LeeDr - this additional checking for the saved search name was an attempt @@ -61,9 +66,8 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider // that the next action wouldn't have to retry. But it doesn't really solve // that issue. But it does typically take about 3 retries to // complete with the expected searchName. - await retry.try(async () => { - const name = await this.getCurrentQueryName(); - expect(name).to.be(searchName); + await retry.waitFor(`saved search was persisted with name ${searchName}`, async () => { + return (await this.getCurrentQueryName()) === searchName; }); } @@ -96,11 +100,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider // We need this try loop here because previous actions in Discover like // saving a search cause reloading of the page and the "Open" menu item goes stale. - await retry.try(async () => { + await retry.waitFor('saved search panel is opened', async () => { await this.clickLoadSavedSearchButton(); await header.waitUntilLoadingHasFinished(); isOpen = await testSubjects.exists('loadSearchForm'); - expect(isOpen).to.be(true); + return isOpen === true; }); } diff --git a/test/plugin_functional/README.md b/test/plugin_functional/README.md index 476c08408658c..075d321917c39 100644 --- a/test/plugin_functional/README.md +++ b/test/plugin_functional/README.md @@ -17,9 +17,9 @@ To run these tests during development you can use the following commands: ``` # Start the test server (can continue running) -node scripts/functional_tests_server.js --config test/plugin_functional/config.js +node scripts/functional_tests_server.js --config test/plugin_functional/config.ts # Start a test run -node scripts/functional_test_runner.js --config test/plugin_functional/config.js +node scripts/functional_test_runner.js --config test/plugin_functional/config.ts ``` ## Run Kibana with a test plugin @@ -42,7 +42,7 @@ If you wish to load up specific es archived data for your test, you can do so vi Another option, which will automatically use any specific settings the test environment may rely on, is to boot up the functional test server pointing to the plugin configuration file. ``` -node scripts/functional_tests_server --config test/plugin_functional/config.js +node scripts/functional_tests_server --config test/plugin_functional/config.ts ``` *Note:* you may still need to use the es_archiver script to boot up any required data. diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.ts similarity index 94% rename from test/plugin_functional/config.js rename to test/plugin_functional/config.ts index f51fb5e1bade4..c611300eade10 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.ts @@ -16,12 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import path from 'path'; import fs from 'fs'; -import { services } from './services'; -export default async function ({ readConfigFile }) { +export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Find all folders in ./plugins since we treat all them as plugin folder @@ -42,7 +41,6 @@ export default async function ({ readConfigFile }) { ], services: { ...functionalConfig.get('services'), - ...services, }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json new file mode 100644 index 0000000000000..a7674881e8ba0 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "elasticsearch_client_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json new file mode 100644 index 0000000000000..02c2955f26c86 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json @@ -0,0 +1,15 @@ +{ + "name": "elasticsearch_client_plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts new file mode 100644 index 0000000000000..3801e33a2cf3e --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ElasticsearchClientPlugin } from './plugin'; + +export const plugin = () => new ElasticsearchClientPlugin(); diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts new file mode 100644 index 0000000000000..5e018ca7818d3 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Plugin, CoreSetup, CoreStart, ICustomClusterClient } from 'src/core/server'; + +export class ElasticsearchClientPlugin implements Plugin { + private client?: ICustomClusterClient; + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + router.get( + { path: '/api/elasticsearch_client_plugin/context/ping', validate: false }, + async (context, req, res) => { + const { body } = await context.core.elasticsearch.client.asInternalUser.ping(); + return res.ok({ body }); + } + ); + router.get( + { path: '/api/elasticsearch_client_plugin/contract/ping', validate: false }, + async (context, req, res) => { + const [coreStart] = await core.getStartServices(); + const { body } = await coreStart.elasticsearch.client.asInternalUser.ping(); + return res.ok({ body }); + } + ); + router.get( + { path: '/api/elasticsearch_client_plugin/custom_client/ping', validate: false }, + async (context, req, res) => { + const { body } = await this.client!.asInternalUser.ping(); + return res.ok({ body }); + } + ); + } + + public start(core: CoreStart) { + this.client = core.elasticsearch.createClient('my-custom-client-test'); + } + public stop() {} +} diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json new file mode 100644 index 0000000000000..d0751f31ecc5e --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "server/**/*.ts", + "../../../../typings/**/*" + ], + "exclude": [] +} diff --git a/test/plugin_functional/services/index.ts b/test/plugin_functional/services/index.ts index dd2b25e14fe17..453cfc5a8636f 100644 --- a/test/plugin_functional/services/index.ts +++ b/test/plugin_functional/services/index.ts @@ -16,14 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; -import { FtrProviderContext } from 'test/functional/ftr_provider_context'; -import { KibanaSupertestProvider } from './supertest'; +import { FtrProviderContext } from '../../functional/ftr_provider_context'; -export const services = { - supertest: KibanaSupertestProvider, -}; - -export type PluginFunctionalProviderContext = FtrProviderContext & - GenericFtrProviderContext; +export type PluginFunctionalProviderContext = FtrProviderContext; diff --git a/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts b/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts new file mode 100644 index 0000000000000..9b9efc261126f --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { PluginFunctionalProviderContext } from '../../services'; +import '../../plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + describe('elasticsearch client', () => { + it('server plugins have access to elasticsearch client via request context', async () => { + await supertest.get('/api/elasticsearch_client_plugin/context/ping').expect(200, 'true'); + }); + it('server plugins have access to elasticsearch client via core contract', async () => { + await supertest.get('/api/elasticsearch_client_plugin/contract/ping').expect(200, 'true'); + }); + it('server plugins can create a custom elasticsearch client', async () => { + await supertest + .get('/api/elasticsearch_client_plugin/custom_client/ping') + .expect(200, 'true'); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 8f54ec6c0f4cd..99ac6dc9b8474 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -22,6 +22,7 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ loadTestFile }: PluginFunctionalProviderContext) { describe('core plugins', () => { loadTestFile(require.resolve('./applications')); + loadTestFile(require.resolve('./elasticsearch_client')); loadTestFile(require.resolve('./legacy_plugins')); loadTestFile(require.resolve('./server_plugins')); loadTestFile(require.resolve('./ui_plugins')); diff --git a/test/plugin_functional/test_suites/core_plugins/top_nav.js b/test/plugin_functional/test_suites/core_plugins/top_nav.ts similarity index 88% rename from test/plugin_functional/test_suites/core_plugins/top_nav.js rename to test/plugin_functional/test_suites/core_plugins/top_nav.ts index 5c46e3d7f76db..6d2c6b7f85d28 100644 --- a/test/plugin_functional/test_suites/core_plugins/top_nav.js +++ b/test/plugin_functional/test_suites/core_plugins/top_nav.ts @@ -17,8 +17,10 @@ * under the License. */ import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getService, getPageObjects }) { +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); const browser = getService('browser'); diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index da5348749f668..ec3dbd919fed6 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -15,7 +15,7 @@ */ def withDefaultPrComments(closure) { catchErrors { - // sendCommentOnError() needs to know if comments are enabled, so lets track it with a global + // kibanaPipeline.notifyOnError() needs to know if comments are enabled, so lets track it with a global // isPr() just ensures this functionality is skipped for non-PR builds buildState.set('PR_COMMENTS_ENABLED', isPr()) catchErrors { @@ -59,19 +59,6 @@ def sendComment(isFinal = false) { } } -def sendCommentOnError(Closure closure) { - try { - closure() - } catch (ex) { - // If this is the first failed step, it's likely that the error hasn't propagated up far enough to mark the build as a failure - currentBuild.result = 'FAILURE' - catchErrors { - sendComment(false) - } - throw ex - } -} - // Checks whether or not this currently executing build was triggered via a PR in the elastic/kibana repo def isPr() { return !!(env.ghprbPullId && env.ghprbPullLink && env.ghprbPullLink =~ /\/elastic\/kibana\//) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 410578886a01d..94bfe983b4653 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -16,6 +16,25 @@ def withPostBuildReporting(Closure closure) { } } +def notifyOnError(Closure closure) { + try { + closure() + } catch (ex) { + // If this is the first failed step, it's likely that the error hasn't propagated up far enough to mark the build as a failure + currentBuild.result = 'FAILURE' + catchErrors { + githubPr.sendComment(false) + } + catchErrors { + // an empty map is a valid config, but is falsey, so let's use .has() + if (buildState.has('SLACK_NOTIFICATION_CONFIG')) { + slackNotifications.sendFailedBuild(buildState.get('SLACK_NOTIFICATION_CONFIG')) + } + } + throw ex + } +} + def functionalTestProcess(String name, Closure closure) { return { processNumber -> def kibanaPort = "61${processNumber}1" @@ -35,7 +54,7 @@ def functionalTestProcess(String name, Closure closure) { "JOB=${name}", "KBN_NP_PLUGINS_BUILT=true", ]) { - githubPr.sendCommentOnError { + notifyOnError { closure() } } @@ -182,7 +201,7 @@ def bash(script, label) { } def doSetup() { - githubPr.sendCommentOnError { + notifyOnError { retryWithDelay(2, 15) { try { runbld("./test/scripts/jenkins_setup.sh", "Setup Build Environment and Dependencies") @@ -199,13 +218,13 @@ def doSetup() { } def buildOss() { - githubPr.sendCommentOnError { + notifyOnError { runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") } } def buildXpack() { - githubPr.sendCommentOnError { + notifyOnError { runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") } } diff --git a/vars/slackNotifications.groovy b/vars/slackNotifications.groovy index 30f86e6d6f0ad..02aad14d8ba3f 100644 --- a/vars/slackNotifications.groovy +++ b/vars/slackNotifications.groovy @@ -105,16 +105,26 @@ def getDefaultDisplayName() { return "${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}" } -def getDefaultContext() { - def duration = currentBuild.durationString.replace(' and counting', '') +def getDefaultContext(config = [:]) { + def progressMessage = "" + if (config && !config.isFinal) { + progressMessage = "In-progress" + } else { + def duration = currentBuild.durationString.replace(' and counting', '') + progressMessage = "${buildUtils.getBuildStatus().toLowerCase().capitalize()} after ${duration}" + } return contextBlock([ - "${buildUtils.getBuildStatus().toLowerCase().capitalize()} after ${duration}", + progressMessage, "", ].join(' · ')) } -def getStatusIcon() { +def getStatusIcon(config = [:]) { + if (config && !config.isFinal) { + return ':hourglass_flowing_sand:' + } + def status = buildUtils.getBuildStatus() if (status == 'UNSTABLE') { return ':yellow_heart:' @@ -124,7 +134,7 @@ def getStatusIcon() { } def getBackupMessage(config) { - return "${getStatusIcon()} ${config.title}\n\nFirst attempt at sending this notification failed. Please check the build." + return "${getStatusIcon(config)} ${config.title}\n\nFirst attempt at sending this notification failed. Please check the build." } def sendFailedBuild(Map params = [:]) { @@ -135,19 +145,32 @@ def sendFailedBuild(Map params = [:]) { color: 'danger', icon: ':jenkins:', username: 'Kibana Operations', - context: getDefaultContext(), + isFinal: false, ] + params - def title = "${getStatusIcon()} ${config.title}" - def message = "${getStatusIcon()} ${config.message}" + config.context = config.context ?: getDefaultContext(config) + + def title = "${getStatusIcon(config)} ${config.title}" + def message = "${getStatusIcon(config)} ${config.message}" def blocks = [markdownBlock(title)] getFailedBuildBlocks().each { blocks << it } blocks << dividerBlock() blocks << config.context + def channel = config.channel + def timestamp = null + + def previousResp = buildState.get('SLACK_NOTIFICATION_RESPONSE') + if (previousResp) { + // When using `timestamp` to update a previous message, you have to use the channel ID from the previous response + channel = previousResp.channelId + timestamp = previousResp.ts + } + def resp = slackSend( - channel: config.channel, + channel: channel, + timestamp: timestamp, username: config.username, iconEmoji: config.icon, color: config.color, @@ -156,7 +179,7 @@ def sendFailedBuild(Map params = [:]) { ) if (!resp) { - slackSend( + resp = slackSend( channel: config.channel, username: config.username, iconEmoji: config.icon, @@ -165,6 +188,10 @@ def sendFailedBuild(Map params = [:]) { blocks: [markdownBlock(getBackupMessage(config))] ) } + + if (resp) { + buildState.set('SLACK_NOTIFICATION_RESPONSE', resp) + } } def onFailure(Map options = [:]) { @@ -172,6 +199,7 @@ def onFailure(Map options = [:]) { def status = buildUtils.getBuildStatus() if (status != "SUCCESS") { catchErrors { + options.isFinal = true sendFailedBuild(options) } } @@ -179,6 +207,16 @@ def onFailure(Map options = [:]) { } def onFailure(Map options = [:], Closure closure) { + if (options.disabled) { + catchError { + closure() + } + + return + } + + buildState.set('SLACK_NOTIFICATION_CONFIG', options) + // try/finally will NOT work here, because the build status will not have been changed to ERROR when the finally{} block executes catchError { closure() diff --git a/vars/workers.groovy b/vars/workers.groovy index 74ce86516e863..f5a28c97c6812 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -126,7 +126,7 @@ def intake(jobName, String script) { return { ci(name: jobName, size: 's-highmem', ramDisk: true) { withEnv(["JOB=${jobName}"]) { - githubPr.sendCommentOnError { + kibanaPipeline.notifyOnError { runbld(script, "Execute ${jobName}") } } diff --git a/x-pack/package.json b/x-pack/package.json index 3a9b3ca606de6..dcba01a771fd5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -75,6 +75,7 @@ "@types/hoist-non-react-statics": "^3.3.1", "@types/history": "^4.7.3", "@types/jest": "^25.2.3", + "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", "@types/js-search": "^1.4.0", "@types/js-yaml": "^3.11.1", @@ -119,10 +120,13 @@ "@types/xml2js": "^0.4.5", "@types/stats-lite": "^2.2.0", "@types/pretty-ms": "^5.0.0", + "@types/webpack-env": "^1.15.2", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", + "autoprefixer": "^9.7.4", "axios": "^0.19.0", "babel-jest": "^25.5.1", + "babel-loader": "^8.0.6", "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", "base64-js": "^1.3.1", "base64url": "^3.0.1", @@ -156,9 +160,11 @@ "jest-cli": "^25.5.4", "jest-styled-components": "^7.0.2", "jsdom": "13.1.0", + "jsondiffpatch": "0.4.1", "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", + "mini-css-extract-plugin": "0.8.0", "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.1", "mochawesome": "^4.1.0", @@ -168,6 +174,9 @@ "node-fetch": "^2.6.0", "null-loader": "^3.0.0", "pixelmatch": "^5.1.0", + "postcss": "^7.0.32", + "postcss-loader": "^3.0.0", + "postcss-prefix-selector": "^1.7.2", "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-is": "^16.8.0", @@ -212,8 +221,12 @@ "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", + "@turf/bbox": "6.0.1", + "@turf/bbox-polygon": "6.0.1", "@turf/boolean-contains": "6.0.1", "@turf/circle": "6.0.1", + "@turf/distance": "6.0.1", + "@turf/helpers": "6.0.1", "angular": "^1.7.9", "angular-resource": "1.7.9", "angular-sanitize": "1.7.9", @@ -308,7 +321,6 @@ "pluralize": "3.1.0", "pngjs": "3.4.0", "polished": "^1.9.2", - "postcss-prefix-selector": "^1.7.2", "prop-types": "^15.6.0", "proper-lockfile": "^3.2.0", "puid": "1.0.7", @@ -362,7 +374,6 @@ "tinymath": "1.2.1", "topojson-client": "3.0.0", "tslib": "^2.0.0", - "turf": "3.0.14", "typescript-fsa": "^3.0.0", "typescript-fsa-reducers": "^1.2.1", "ui-select": "0.19.8", @@ -386,4 +397,4 @@ "cypress-multi-reporters" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 9e0a3e3d0d889..07270b572a4be 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -13,19 +13,18 @@ export interface ServiceAnomalyStats { jobId?: string; } -export const MLErrorMessages: Record = { - INSUFFICIENT_LICENSE: i18n.translate( - 'xpack.apm.anomaly_detection.error.insufficient_license', +export const ML_ERRORS = { + INVALID_LICENSE: i18n.translate( + 'xpack.apm.anomaly_detection.error.invalid_license', { - defaultMessage: - 'You must have a platinum license to use Anomaly Detection', + defaultMessage: `To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.`, } ), MISSING_READ_PRIVILEGES: i18n.translate( 'xpack.apm.anomaly_detection.error.missing_read_privileges', { defaultMessage: - 'You must have "read" privileges to Machine Learning in order to view Anomaly Detection jobs', + 'You must have "read" privileges to Machine Learning and APM in order to view Anomaly Detection jobs', } ), MISSING_WRITE_PRIVILEGES: i18n.translate( @@ -47,16 +46,4 @@ export const MLErrorMessages: Record = { defaultMessage: 'Machine learning is not available in the selected space', } ), - UNEXPECTED: i18n.translate('xpack.apm.anomaly_detection.error.unexpected', { - defaultMessage: 'An unexpected error occurred', - }), }; - -export enum ErrorCode { - INSUFFICIENT_LICENSE = 'INSUFFICIENT_LICENSE', - MISSING_READ_PRIVILEGES = 'MISSING_READ_PRIVILEGES', - MISSING_WRITE_PRIVILEGES = 'MISSING_WRITE_PRIVILEGES', - ML_NOT_AVAILABLE = 'ML_NOT_AVAILABLE', - ML_NOT_AVAILABLE_IN_SPACE = 'ML_NOT_AVAILABLE_IN_SPACE', - UNEXPECTED = 'UNEXPECTED', -} diff --git a/x-pack/plugins/apm/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts index 3e8b0ba0e8b5e..cd8bcaa1de237 100644 --- a/x-pack/plugins/apm/common/processor_event.ts +++ b/x-pack/plugins/apm/common/processor_event.ts @@ -8,4 +8,18 @@ export enum ProcessorEvent { transaction = 'transaction', error = 'error', metric = 'metric', + span = 'span', + onboarding = 'onboarding', + sourcemap = 'sourcemap', } +/** + * Processor events that are searchable in the UI via the query bar. + * + * Some client-sideroutes will define 1 or more processor events that + * will be used to fetch the dynamic index pattern for the query bar. + */ + +export type UIProcessorEvent = + | ProcessorEvent.transaction + | ProcessorEvent.error + | ProcessorEvent.metric; diff --git a/x-pack/plugins/apm/common/projections.ts b/x-pack/plugins/apm/common/projections.ts new file mode 100644 index 0000000000000..a5fd9d3951cc9 --- /dev/null +++ b/x-pack/plugins/apm/common/projections.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum Projection { + services = 'services', + transactionGroups = 'transactionGroups', + traces = 'traces', + transactions = 'transactions', + metrics = 'metrics', + errorGroups = 'errorGroups', + serviceNodes = 'serviceNodes', + rumOverview = 'rumOverview', +} diff --git a/x-pack/plugins/apm/common/projections/services.ts b/x-pack/plugins/apm/common/projections/services.ts deleted file mode 100644 index 809caeeaf6088..0000000000000 --- a/x-pack/plugins/apm/common/projections/services.ts +++ /dev/null @@ -1,64 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - Setup, - SetupUIFilters, - SetupTimeRange, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../server/lib/helpers/setup_request'; -import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; - -export function getServicesProjection({ - setup, - noEvents, -}: { - setup: Setup & SetupTimeRange & SetupUIFilters; - noEvents?: boolean; -}) { - const { start, end, uiFiltersES, indices } = setup; - - return { - ...(noEvents - ? {} - : { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], - }), - body: { - size: 0, - query: { - bool: { - filter: [ - ...(noEvents - ? [] - : [ - { - terms: { - [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'], - }, - }, - ]), - { range: rangeFilter(start, end) }, - ...uiFiltersES, - ], - }, - }, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index fe2303d645ec9..92ea044720531 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -14,7 +14,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useFetcher } from '../../../hooks/useFetcher'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -79,7 +79,7 @@ function ErrorGroupOverview() { params: { serviceName, }, - projection: PROJECTION.ERROR_GROUPS, + projection: Projection.errorGroups, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index b09c03f853aa9..b2f15dbb11341 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -83,7 +83,8 @@ interface Props { } export function Home({ tab }: Props) { - const { config } = useApmPluginContext(); + const { config, core } = useApmPluginContext(); + const canAccessML = !!core.application.capabilities.ml?.canAccessML; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab @@ -105,9 +106,11 @@ export function Home({ tab }: Props) { - - - + {canAccessML && ( + + + + )} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 9b88202b2e5ef..8d1959ec14d15 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -13,7 +13,7 @@ import { } from '@elastic/eui'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { RumDashboard } from './RumDashboard'; import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -28,7 +28,7 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], - projection: PROJECTION.RUM_OVERVIEW, + projection: Projection.rumOverview, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 9af6a8d988c11..9b01f9ebb7e99 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -16,7 +16,7 @@ import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { MetricsChart } from '../../shared/charts/MetricsChart'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; interface ServiceMetricsProps { @@ -36,7 +36,7 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) { serviceName, serviceNodeName, }, - projection: PROJECTION.METRICS, + projection: Projection.metrics, showCount: false, }), [serviceName, serviceNodeName] diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 5537a73d228e8..3cde48aa483cb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; @@ -46,7 +46,7 @@ function ServiceNodeOverview() { params: { serviceName, }, - projection: PROJECTION.SERVICE_NODES, + projection: Projection.serviceNodes, }), [serviceName] ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 7d05ae90afb87..7146e471a7f82 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -15,7 +15,7 @@ import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; @@ -88,7 +88,7 @@ export function ServiceOverview() { const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['host', 'agentName'], - projection: PROJECTION.SERVICES, + projection: Projection.services, }), [] ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index cb2090d1cbe2b..a594edb32b083 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -20,7 +20,7 @@ import { EuiEmptyPrompt, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { MLErrorMessages } from '../../../../../common/anomaly_detection'; +import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { createJobs } from './create_jobs'; @@ -64,8 +64,8 @@ export function AddEnvironments({ return ( {MLErrorMessages.MISSING_WRITE_PRIVILEGES}} + iconType="alert" + body={<>{ML_ERRORS.MISSING_WRITE_PRIVILEGES}} /> ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts index acea38732b40a..2e2c2ccbad7cf 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; -import { MLErrorMessages } from '../../../../../common/anomaly_detection'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; const errorToastTitle = i18n.translate( @@ -27,7 +26,7 @@ export async function createJobs({ toasts: NotificationsStart['toasts']; }) { try { - const res = await callApmApi({ + await callApmApi({ pathname: '/api/apm/settings/anomaly-detection/jobs', method: 'POST', params: { @@ -35,23 +34,11 @@ export async function createJobs({ }, }); - // a known error occurred - if (res?.errorCode) { - toasts.addDanger({ - title: errorToastTitle, - text: MLErrorMessages[res.errorCode], - }); - return false; - } - - // job created successfully toasts.addSuccess({ title: successToastTitle, text: getSuccessToastMessage(environments), }); return true; - - // an unknown/unexpected error occurred } catch (error) { toasts.addDanger({ title: errorToastTitle, diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index dab30761c6ebe..9c04caf61022a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui'; -import { MLErrorMessages } from '../../../../../common/anomaly_detection'; +import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; @@ -25,12 +25,11 @@ export type AnomalyDetectionApiResponse = APIReturnType< const DEFAULT_VALUE: AnomalyDetectionApiResponse = { jobs: [], hasLegacyJobs: false, - errorCode: undefined, }; export function AnomalyDetection() { const plugin = useApmPluginContext(); - const canGetJobs = !!plugin.core.application.capabilities.ml.canGetJobs; + const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); @@ -49,15 +48,7 @@ export function AnomalyDetection() { if (!hasValidLicense) { return ( - + ); } @@ -66,8 +57,8 @@ export function AnomalyDetection() { return ( {MLErrorMessages.MISSING_READ_PRIVILEGES}} + iconType="alert" + body={<>{ML_ERRORS.MISSING_READ_PRIVILEGES}} /> ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 8494004ae5639..05ea585108c69 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -16,10 +16,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - MLErrorMessages, - ErrorCode, -} from '../../../../../common/anomaly_detection'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -66,7 +62,7 @@ interface Props { onAddEnvironments: () => void; } export function JobsList({ data, status, onAddEnvironments }: Props) { - const { jobs, hasLegacyJobs, errorCode } = data; + const { jobs, hasLegacyJobs } = data; return ( @@ -115,10 +111,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { @@ -129,13 +122,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { ); } -function getNoItemsMessage({ - status, - errorCode, -}: { - status: FETCH_STATUS; - errorCode?: ErrorCode; -}) { +function getNoItemsMessage({ status }: { status: FETCH_STATUS }) { // loading state const isLoading = status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; @@ -143,11 +130,6 @@ function getNoItemsMessage({ return ; } - // A known error occured. Show specific error message - if (errorCode) { - return MLErrorMessages[errorCode]; - } - // An unexpected error occurred. Show default error message if (status === FETCH_STATUS.FAILURE) { return i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index bd2ea706e492d..cb4726244e50c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -16,8 +16,11 @@ import { import { HomeLink } from '../../shared/Links/apm/HomeLink'; import { useLocation } from '../../../hooks/useLocation'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function Settings(props: { children: ReactNode }) { + const plugin = useApmPluginContext(); + const canAccessML = !!plugin.core.application.capabilities.ml?.canAccessML; const { search, pathname } = useLocation(); return ( <> @@ -48,17 +51,25 @@ export function Settings(props: { children: ReactNode }) { '/settings/agent-configuration' ), }, - { - name: i18n.translate( - 'xpack.apm.settings.anomalyDetection', - { - defaultMessage: 'Anomaly detection', - } - ), - id: '4', - href: getAPMHref('/settings/anomaly-detection', search), - isSelected: pathname === '/settings/anomaly-detection', - }, + ...(canAccessML + ? [ + { + name: i18n.translate( + 'xpack.apm.settings.anomalyDetection', + { + defaultMessage: 'Anomaly detection', + } + ), + id: '4', + href: getAPMHref( + '/settings/anomaly-detection', + search + ), + isSelected: + pathname === '/settings/anomaly-detection', + }, + ] + : []), { name: i18n.translate('xpack.apm.settings.customizeApp', { defaultMessage: 'Customize app', diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cdebb3aac129b..06b4459fb56eb 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -11,7 +11,7 @@ import { TraceList } from './TraceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TracesAPIResponse = APIReturnType<'/api/apm/traces'>; @@ -48,7 +48,7 @@ export function TraceOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'host', 'containerId', 'podName'], - projection: PROJECTION.TRACES, + projection: Projection.traces, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index c4d5be5874215..0dc2f607b1ef2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -27,7 +27,7 @@ import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; @@ -52,7 +52,7 @@ export function TransactionDetails() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'serviceVersion'], - projection: PROJECTION.TRANSACTIONS, + projection: Projection.transactions, params: { transactionName, transactionType, diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 98702fe3686ff..d9bd3e59d281f 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -35,7 +35,7 @@ import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; @@ -103,7 +103,7 @@ export function TransactionOverview() { serviceName, transactionType, }, - projection: PROJECTION.TRANSACTION_GROUPS, + projection: Projection.transactionGroups, }), [serviceName, transactionType] ); diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 502f5f0034b5f..6c605886e6e00 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -9,7 +9,7 @@ import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; -// @ts-ignore +// @ts-expect-error import { Typeahead } from './Typeahead'; import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 359468073f7f4..ca02abc395992 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -33,8 +33,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:"transaction" AND transaction.id:"8b60bd32ecc6e150" AND trace.id:"8b60bd32ecc6e1506735a8b6cfcf175c"'))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"'))"` ); }); @@ -50,8 +50,8 @@ describe('DiscoverLinks', () => { '?rangeFrom=now/w&rangeTo=now&refreshPaused=true&refreshInterval=0', } as Location); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:"test-span-id"'))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:\\"test-span-id\\"'))"` ); }); @@ -72,8 +72,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:"service-name" AND error.grouping_key:"grouping-key"'),sort:('@timestamp':desc))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\"'),sort:('@timestamp':desc))"` ); }); @@ -95,8 +95,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:"service-name" AND error.grouping_key:"grouping-key" AND some:kuery-string'),sort:('@timestamp':desc))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\" AND some:kuery-string'),sort:('@timestamp':desc))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index 39082c2639a2c..9ba4aab0e23d9 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -22,7 +22,7 @@ describe('MLJobLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))"` ); }); it('should produce the correct URL with jobId, serviceName, and transactionType', async () => { @@ -41,7 +41,7 @@ describe('MLJobLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request),zoom:(from:now/w,to:now-4h)))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index b4187b2f797ab..da345e35c10b1 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -21,6 +21,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:true,value:'0'),time:(from:now-5h,to:now-2h))"` + `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts new file mode 100644 index 0000000000000..28daae7fd830e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref'; + +jest.mock('../../../../hooks/useApmPluginContext', () => ({ + useApmPluginContext: () => ({ + core: { http: { basePath: { prepend: (url: string) => url } } }, + }), +})); + +jest.mock('../../../../hooks/useLocation', () => ({ + useLocation: () => ({ + search: + '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', + }), +})); + +describe('useTimeSeriesExplorerHref', () => { + it('correctly encodes time range values', async () => { + const href = useTimeSeriesExplorerHref({ + jobId: 'apm-production-485b-high_mean_transaction_duration', + serviceName: 'opbeans-java', + transactionType: 'request', + }); + + expect(href).toMatchInlineSnapshot( + `"/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request),zoom:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z')))"` + ); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts index 625b9205b6ce0..459ee8f0282ff 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts @@ -22,12 +22,14 @@ export function useTimeSeriesExplorerHref({ }) { const { core } = useApmPluginContext(); const location = useLocation(); + const { time, refreshInterval } = getTimepickerRisonData(location.search); const search = querystring.stringify( { _g: rison.encode({ ml: { jobIds: [jobId] }, - ...getTimepickerRisonData(location.search), + time, + refreshInterval, }), ...(serviceName && transactionType ? { @@ -37,6 +39,7 @@ export function useTimeSeriesExplorerHref({ 'service.name': serviceName, 'transaction.type': transactionType, }, + zoom: time, }, }), } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx index 2149cb676f0d8..585ab22b5fb27 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx @@ -4,51 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -import { showAlert } from './AnomalyDetectionSetupLink'; - -const dataWithJobs = { - hasLegacyJobs: false, - jobs: [ - { job_id: 'job1', environment: 'staging' }, - { job_id: 'job2', environment: 'production' }, - ], -}; -const dataWithoutJobs = ({ jobs: [] } as unknown) as any; - -describe('#showAlert', () => { - describe('when an environment is selected', () => { - it('should return true when there are no jobs', () => { - const result = showAlert(dataWithoutJobs, 'testing'); - expect(result).toBe(true); - }); - it('should return true when environment is not included in the jobs', () => { - const result = showAlert(dataWithJobs, 'testing'); - expect(result).toBe(true); +import React from 'react'; +import { render, fireEvent, wait } from '@testing-library/react'; +import { MissingJobsAlert } from './AnomalyDetectionSetupLink'; +import * as hooks from '../../../../hooks/useFetcher'; + +async function renderTooltipAnchor({ + jobs, + environment, +}: { + jobs: Array<{ job_id: string; environment: string }>; + environment?: string; +}) { + // mock api response + jest.spyOn(hooks, 'useFetcher').mockReturnValue({ + data: { jobs }, + status: hooks.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + + const { baseElement, container } = render( + + ); + + // hover tooltip anchor if it exists + const toolTipAnchor = container.querySelector('.euiToolTipAnchor') as any; + if (toolTipAnchor) { + fireEvent.mouseOver(toolTipAnchor); + + // wait for tooltip text to be in the DOM + await wait(() => { + const toolTipText = baseElement.querySelector('.euiToolTipPopover') + ?.textContent; + expect(toolTipText).not.toBe(undefined); }); - it('should return false when environment is included in the jobs', () => { - const result = showAlert(dataWithJobs, 'staging'); - expect(result).toBe(false); + } + + const toolTipText = baseElement.querySelector('.euiToolTipPopover') + ?.textContent; + + return { toolTipText, toolTipAnchor }; +} + +describe('MissingJobsAlert', () => { + describe('when no jobs exist', () => { + it('shows a warning', async () => { + const { toolTipText, toolTipAnchor } = await renderTooltipAnchor({ + jobs: [], + }); + + expect(toolTipAnchor).toBeInTheDocument(); + expect(toolTipText).toBe( + 'Anomaly detection is not yet enabled. Click to continue setup.' + ); }); }); - describe('there is no environment selected (All)', () => { - it('should return true when there are no jobs', () => { - const result = showAlert(dataWithoutJobs, undefined); - expect(result).toBe(true); + describe('when no jobs exists for the selected environment', () => { + it('shows a warning', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [{ environment: 'production', job_id: 'my_job_id' }], + environment: 'staging', + }); + + expect(toolTipAnchor).toBeInTheDocument(); + expect(toolTipText).toBe( + 'Anomaly detection is not yet enabled for the environment "staging". Click to continue setup.' + ); }); - it('should return false when there are any number of jobs', () => { - const result = showAlert(dataWithJobs, undefined); - expect(result).toBe(false); + }); + + describe('when a job exists for the selected environment', () => { + it('does not show a warning', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [{ environment: 'production', job_id: 'my_job_id' }], + environment: 'production', + }); + + expect(toolTipAnchor).not.toBeInTheDocument(); + expect(toolTipText).toBe(undefined); }); }); - describe('when a known error occurred', () => { - it('should return false', () => { - const data = ({ - errorCode: 'MISSING_READ_PRIVILEGES', - } as unknown) as any; - const result = showAlert(data, undefined); - expect(result).toBe(false); + describe('when at least one job exists and no environment is selected', () => { + it('does not show a warning', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [{ environment: 'production', job_id: 'my_job_id' }], + }); + + expect(toolTipAnchor).not.toBeInTheDocument(); + expect(toolTipText).toBe(undefined); }); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx index e989244d43148..a80dcca4a107d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx @@ -6,43 +6,73 @@ import React from 'react'; import { EuiButtonEmpty, EuiToolTip, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ErrorCode } from '../../../../../common/anomaly_detection'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMLink } from './APMLink'; import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { useLicense } from '../../../../hooks/useLicense'; export type AnomalyDetectionApiResponse = APIReturnType< '/api/apm/settings/anomaly-detection', 'GET' >; -const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false, errorCode: undefined }; +const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; export function AnomalyDetectionSetupLink() { const { uiFilters } = useUrlParams(); const environment = uiFilters.environment; + const plugin = useApmPluginContext(); + const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; + const license = useLicense(); + const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); + return ( + + + {ANOMALY_DETECTION_LINK_LABEL} + + + {canGetJobs && hasValidLicense ? ( + + ) : null} + + ); +} + +export function MissingJobsAlert({ environment }: { environment?: string }) { const { data = DEFAULT_DATA, status } = useFetcher( (callApmApi) => callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), [], - { preservePreviousData: false } + { preservePreviousData: false, showToastOnError: false } ); - const isFetchSuccess = status === FETCH_STATUS.SUCCESS; + + if (status !== FETCH_STATUS.SUCCESS) { + return null; + } + + const isEnvironmentSelected = !!environment; + + // there are jobs for at least one environment + if (!isEnvironmentSelected && data.jobs.length > 0) { + return null; + } + + // there are jobs for the selected environment + if ( + isEnvironmentSelected && + data.jobs.some((job) => environment === job.environment) + ) { + return null; + } return ( - - - {ANOMALY_DETECTION_LINK_LABEL} - - {isFetchSuccess && showAlert(data, environment) && ( - - - - )} - + + + ); } @@ -56,7 +86,7 @@ function getTooltipText(environment?: string) { return i18n.translate( 'xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText', { - defaultMessage: `Anomaly detection is not yet enabled for the "{currentEnvironment}" environment. Click to continue setup.`, + defaultMessage: `Anomaly detection is not yet enabled for the environment "{currentEnvironment}". Click to continue setup.`, values: { currentEnvironment: getEnvironmentLabel(environment) }, } ); @@ -66,21 +96,3 @@ const ANOMALY_DETECTION_LINK_LABEL = i18n.translate( 'xpack.apm.anomalyDetectionSetup.linkLabel', { defaultMessage: `Anomaly detection` } ); - -export function showAlert( - { jobs = [], errorCode }: AnomalyDetectionApiResponse, - environment: string | undefined -) { - // don't show warning if the user is missing read privileges - if (errorCode === ErrorCode.MISSING_READ_PRIVILEGES) { - return false; - } - - return ( - // No job exists, or - jobs.length === 0 || - // no job exists for the selected environment - (environment !== undefined && - jobs.every((job) => environment !== job.environment)) - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts new file mode 100644 index 0000000000000..8dd662339b61a --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getTimepickerRisonData } from './rison_helpers'; + +describe('getTimepickerRisonData', () => { + it('returns object of timepicker range and refresh interval values', async () => { + const locationSearch = `?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true`; + const timepickerValues = getTimepickerRisonData(locationSearch); + + expect(timepickerValues).toMatchInlineSnapshot(` + Object { + "refreshInterval": Object { + "pause": true, + "value": 10000, + }, + "time": Object { + "from": "2020-07-29T17:27:29.000Z", + "to": "2020-07-29T18:45:00.000Z", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts index 8b4d891dba83b..cab822b42be56 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts @@ -22,18 +22,16 @@ export function getTimepickerRisonData(currentSearch: Location['search']) { const currentQuery = toQuery(currentSearch); return { time: { - from: currentQuery.rangeFrom - ? encodeURIComponent(currentQuery.rangeFrom) - : '', - to: currentQuery.rangeTo ? encodeURIComponent(currentQuery.rangeTo) : '', + from: currentQuery.rangeFrom || '', + to: currentQuery.rangeTo || '', }, refreshInterval: { pause: currentQuery.refreshPaused - ? String(currentQuery.refreshPaused) - : '', + ? Boolean(currentQuery.refreshPaused) + : true, value: currentQuery.refreshInterval - ? String(currentQuery.refreshInterval) - : '', + ? parseInt(currentQuery.refreshInterval, 10) + : 0, }, }; } diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index fedf96b4cc4ea..ba700e68b59bc 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -17,10 +17,10 @@ import styled from 'styled-components'; import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; interface Props { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; showCount?: boolean; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts index 186fc082ce5fe..e057e9c034615 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -68,7 +68,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], @@ -139,7 +139,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], @@ -209,7 +209,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], diff --git a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 61632700b81d8..5b167e8160ffa 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -7,7 +7,7 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; -// @ts-ignore +// @ts-expect-error import CustomPlot from '../CustomPlot'; import { asDecimal, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts index d9781400f2272..65514ff71d02b 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -7,10 +7,13 @@ import { compact, pickBy } from 'lodash'; import datemath from '@elastic/datemath'; import { IUrlParams } from './types'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; interface PathParams { - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; serviceName?: string; errorGroupId?: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts index 78fe662b88d75..7b50a705afa33 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts @@ -6,7 +6,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { UIProcessorEvent } from '../../../common/processor_event'; export type IUrlParams = { detailTab?: string; @@ -32,6 +32,6 @@ export type IUrlParams = { pageSize?: number; serviceNodeName?: string; searchTerm?: string; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; traceIdLink?: string; } & Partial>; diff --git a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts index 64f333d72f0f5..0b4978acdfcb1 100644 --- a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts @@ -5,10 +5,10 @@ */ import { useFetcher } from './useFetcher'; -import { ProcessorEvent } from '../../common/processor_event'; +import { UIProcessorEvent } from '../../common/processor_event'; export function useDynamicIndexPattern( - processorEvent: ProcessorEvent | undefined + processorEvent: UIProcessorEvent | undefined ) { const { data, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 3354e676cf323..45ede7e7f2607 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -17,7 +17,7 @@ import { import { history } from '../utils/history'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { PROJECTION } from '../../common/projections/typings'; +import { Projection } from '../../common/projections'; import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; @@ -35,7 +35,7 @@ export function useLocalUIFilters({ filterNames, params, }: { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; }) { diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 418312743c324..e750102de2baa 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -99,10 +99,9 @@ export function expectTextsInDocument(output: any, texts: string[]) { } interface MockSetup { - dynamicIndexPattern: any; start: number; end: number; - client: any; + apmEventClient: any; internalClient: any; config: APMConfig; uiFiltersES: ESFilter[]; @@ -148,7 +147,7 @@ export async function inspectSearchParams( const mockSetup = { start: 1528113600000, end: 1528977600000, - client: { search: spy } as any, + apmEventClient: { search: spy } as any, internalClient: { search: spy } as any, config: new Proxy({}, { get: () => 'myIndex' }) as APMConfig, uiFiltersES: [{ term: { 'my.custom.ui.filter': 'foo-bar' } }], diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts deleted file mode 100644 index 993dcf4c5354b..0000000000000 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts +++ /dev/null @@ -1,16 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ErrorCode, MLErrorMessages } from '../../../common/anomaly_detection'; - -export class AnomalyDetectionError extends Error { - constructor(public code: ErrorCode) { - super(MLErrorMessages[code]); - - this.name = this.constructor.name; - Error.captureStackTrace(this, this.constructor); - } -} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index e5338ac9f5797..7bcd945d890ad 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -7,7 +7,8 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; -import { ErrorCode } from '../../../common/anomaly_detection'; +import Boom from 'boom'; +import { ML_ERRORS } from '../../../common/anomaly_detection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; import { @@ -16,7 +17,6 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; -import { AnomalyDetectionError } from './anomaly_detection_error'; export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< typeof createAnomalyDetectionJobs @@ -29,16 +29,12 @@ export async function createAnomalyDetectionJobs( const { ml, indices } = setup; if (!ml) { - throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE); + throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE_IN_SPACE); - } - - if (!mlCapabilities.isPlatinumOrTrialLicense) { - throw new AnomalyDetectionError(ErrorCode.INSUFFICIENT_LICENSE); + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } logger.info( @@ -55,13 +51,10 @@ export async function createAnomalyDetectionJobs( const failedJobs = jobResponses.filter(({ success }) => !success); if (failedJobs.length > 0) { - const failedJobIds = failedJobs.map(({ id }) => id).join(', '); - logger.error( - `Failed to create anomaly detection ML jobs for: [${failedJobIds}]:` + const errors = failedJobs.map(({ id, error }) => ({ id, error })); + throw new Error( + `An error occurred while creating ML jobs: ${JSON.stringify(errors)}` ); - failedJobs.forEach(({ error }) => logger.error(JSON.stringify(error))); - - throw new AnomalyDetectionError(ErrorCode.UNEXPECTED); } return jobResponses; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 62d4243a06028..05f41cdfdffd4 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -5,24 +5,21 @@ */ import { Logger } from 'kibana/server'; -import { ErrorCode } from '../../../common/anomaly_detection'; +import Boom from 'boom'; +import { ML_ERRORS } from '../../../common/anomaly_detection'; import { Setup } from '../helpers/setup_request'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; -import { AnomalyDetectionError } from './anomaly_detection_error'; export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { const { ml } = setup; + if (!ml) { - return []; + throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE_IN_SPACE); - } - - if (!mlCapabilities.isPlatinumOrTrialLicense) { - throw new AnomalyDetectionError(ErrorCode.INSUFFICIENT_LICENSE); + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } const response = await getMlJobsWithAPMGroup(ml); diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts index 999d28309121a..ed66236726b9f 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import Boom from 'boom'; +import { ML_ERRORS } from '../../../common/anomaly_detection'; import { Setup } from '../helpers/setup_request'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; @@ -12,7 +15,12 @@ export async function hasLegacyJobs(setup: Setup) { const { ml } = setup; if (!ml) { - return false; + throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); + } + + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } const response = await getMlJobsWithAPMGroup(ml); diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap index b943102b39de8..da2309afa07cf 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap @@ -2,6 +2,13 @@ exports[`getAllEnvironments fetches all environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -15,15 +22,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -34,16 +32,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`getAllEnvironments fetches all environments with includeMissing 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -57,15 +57,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -76,10 +67,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; 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 9b17033a1f2a5..423b87cb78c3c 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 @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; @@ -21,7 +21,7 @@ export async function getAllEnvironments({ setup: Setup; includeMissing?: boolean; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; // omit filter for service.name if "All" option is selected const serviceNameFilter = serviceName @@ -29,21 +29,18 @@ export async function getAllEnvironments({ : []; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ...serviceNameFilter, - ], + filter: [...serviceNameFilter], }, }, aggs: { @@ -58,7 +55,7 @@ export async function getAllEnvironments({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const environments = resp.aggregations?.environments.buckets.map( (bucket) => bucket.key as string diff --git a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index 982ad558dc91d..63b6c9cde4d0d 100644 --- a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error queries fetches a single error group 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "query": Object { "bool": Object { @@ -11,11 +16,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "error.grouping_key": "groupId", @@ -57,12 +57,16 @@ Object { }, ], }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -104,11 +108,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups when sortField = latestOccurrenceAt 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -180,11 +183,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -204,6 +202,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap index b71b2d697126a..ea142ca2acc00 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error distribution queries fetches an error distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -19,11 +24,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -48,12 +48,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error distribution queries fetches an error distribution with a group id 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -71,11 +75,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -105,6 +104,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap index d336d71424750..085bedf774c46 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should make the correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -21,11 +26,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -50,7 +50,6 @@ Array [ }, "size": 0, }, - "index": "apm-*", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 5f23a9329a583..e0df4d7744610 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { getBuckets } from '../get_buckets'; import { APMConfig } from '../../../..'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let clientSpy: jest.Mock; @@ -29,7 +29,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { + apmEventClient: { search: clientSpy, } as any, internalClient: { @@ -66,8 +66,6 @@ describe('timeseriesFetcher', () => { it('should limit query results to error documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([{ term: { [PROCESSOR_EVENT]: 'error' } }]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.error]); }); }); diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index db36ad1ede91c..de6df15354e79 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../../common/utils/range_filter'; @@ -28,9 +28,8 @@ export async function getBuckets({ bucketSize: number; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -41,7 +40,9 @@ export async function getBuckets({ } const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, query: { @@ -65,7 +66,7 @@ export async function getBuckets({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const buckets = (resp.aggregations?.distribution.buckets || []).map( (bucket) => ({ diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 3d20f84ccfbc2..b23c955b57183 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_SAMPLED, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, @@ -32,17 +31,18 @@ export async function getErrorGroup({ groupId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { size: 1, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [ERROR_GROUP_ID]: groupId } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -57,7 +57,7 @@ export async function getErrorGroup({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const error = resp.hits.hits[0]?._source; const transactionId = error?.transaction?.id; const traceId = error?.trace?.id; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index ad216de271f37..ab1c2149be343 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -13,14 +13,13 @@ import { ERROR_LOG_MESSAGE, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getErrorGroupsProjection } from '../../../common/projections/errors'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getErrorGroupsProjection } from '../../projections/errors'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SortOptions } from '../../../typings/elasticsearch/aggregations'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -38,7 +37,7 @@ export async function getErrorGroups({ sortDirection?: 'asc' | 'desc'; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; @@ -92,23 +91,7 @@ export async function getErrorGroups({ }, }); - interface SampleError { - '@timestamp': APMError['@timestamp']; - error: { - log?: { - message: string; - }; - exception?: Array<{ - handled?: boolean; - message?: string; - type?: string; - }>; - culprit: APMError['error']['culprit']; - grouping_key: APMError['error']['grouping_key']; - }; - } - - const resp = await client.search(params); + const resp = await apmEventClient.search(params); // aggregations can be undefined when no matching indices are found. // this is an exception rather than the rule so the ES type does not account for this. diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts new file mode 100644 index 0000000000000..c475640595227 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-console */ + +import chalk from 'chalk'; +import { + LegacyAPICaller, + KibanaRequest, +} from '../../../../../../../src/core/server'; + +function formatObj(obj: Record) { + return JSON.stringify(obj, null, 2); +} + +export async function callClientWithDebug({ + apiCaller, + operationName, + params, + debug, + request, +}: { + apiCaller: LegacyAPICaller; + operationName: string; + params: Record; + debug: boolean; + request: KibanaRequest; +}) { + const startTime = process.hrtime(); + + let res: any; + let esError = null; + try { + res = apiCaller(operationName, params); + } catch (e) { + // catch error and throw after outputting debug info + esError = e; + } + + if (debug) { + const highlightColor = esError ? 'bgRed' : 'inverse'; + const diff = process.hrtime(startTime); + const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const routeInfo = `${request.route.method.toUpperCase()} ${ + request.route.path + }`; + + console.log( + chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + ); + + if (operationName === 'search') { + console.log(`GET ${params.index}/_${operationName}`); + console.log(formatObj(params.body)); + } else { + console.log(chalk.bold('ES operation:'), operationName); + + console.log(chalk.bold('ES query:')); + console.log(formatObj(params)); + } + console.log(`\n`); + } + + if (esError) { + throw esError; + } + + return res; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts new file mode 100644 index 0000000000000..494cd6cbf0eec --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep } from 'lodash'; +import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fieldnames'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; + +/* + Adds a range query to the ES request to exclude legacy data +*/ + +export function addFilterToExcludeLegacyData( + params: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } +) { + const nextParams = cloneDeep(params); + + // add filter for omitting pre-7.x data + nextParams.body.query.bool.filter.push({ + range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, + }); + + return nextParams; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts new file mode 100644 index 0000000000000..2bfd3c94ed34c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ValuesType } from 'utility-types'; +import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESSearchResponse, +} from '../../../../../typings/elasticsearch'; +import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data'; +import { callClientWithDebug } from '../call_client_with_debug'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { unpackProcessorEvents } from './unpack_processor_events'; + +export type APMEventESSearchRequest = Omit & { + apm: { + events: ProcessorEvent[]; + }; +}; + +type TypeOfProcessorEvent = { + [ProcessorEvent.error]: APMError; + [ProcessorEvent.transaction]: Transaction; + [ProcessorEvent.span]: Span; + [ProcessorEvent.metric]: APMBaseDoc; + [ProcessorEvent.onboarding]: unknown; + [ProcessorEvent.sourcemap]: unknown; +}[T]; + +type ESSearchRequestOf = Omit< + TParams, + 'apm' +> & { index: string[] | string }; + +type TypedSearchResponse< + TParams extends APMEventESSearchRequest +> = ESSearchResponse< + TypeOfProcessorEvent>, + ESSearchRequestOf +>; + +export type APMEventClient = ReturnType; + +export function createApmEventClient({ + context, + request, + indices, + options: { includeFrozen } = { includeFrozen: false }, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; + indices: ApmIndicesConfig; + options: { + includeFrozen: boolean; + }; +}) { + const client = context.core.elasticsearch.legacy.client; + + return { + search( + params: TParams, + { includeLegacyData } = { includeLegacyData: false } + ): Promise> { + const withProcessorEventFilter = unpackProcessorEvents(params, indices); + + const withPossibleLegacyDataFilter = !includeLegacyData + ? addFilterToExcludeLegacyData(withProcessorEventFilter) + : withProcessorEventFilter; + + return callClientWithDebug({ + apiCaller: client.callAsCurrentUser, + operationName: 'search', + params: { + ...withPossibleLegacyDataFilter, + ignore_throttled: !includeFrozen, + }, + request, + debug: context.params.query._debug, + }); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts new file mode 100644 index 0000000000000..d35403ad35d94 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { uniq, defaultsDeep, cloneDeep } from 'lodash'; +import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; +import { APMEventESSearchRequest } from '.'; +import { + ApmIndicesConfig, + ApmIndicesName, +} from '../../../settings/apm_indices/get_apm_indices'; + +export const processorEventIndexMap: Record = { + [ProcessorEvent.transaction]: 'apm_oss.transactionIndices', + [ProcessorEvent.span]: 'apm_oss.spanIndices', + [ProcessorEvent.metric]: 'apm_oss.metricsIndices', + [ProcessorEvent.error]: 'apm_oss.errorIndices', + [ProcessorEvent.sourcemap]: 'apm_oss.sourcemapIndices', + [ProcessorEvent.onboarding]: 'apm_oss.onboardingIndices', +}; + +export function unpackProcessorEvents( + request: APMEventESSearchRequest, + indices: ApmIndicesConfig +) { + const { apm, ...params } = request; + + const index = uniq( + apm.events.map((event) => indices[processorEventIndexMap[event]]) + ); + + const withFilterForProcessorEvent: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } = defaultsDeep(cloneDeep(params), { + body: { + query: { + bool: { + filter: [], + }, + }, + }, + }); + + withFilterForProcessorEvent.body.query.bool.filter.push({ + terms: { + [PROCESSOR_EVENT]: apm.events, + }, + }); + + return { + index, + ...withFilterForProcessorEvent, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts new file mode 100644 index 0000000000000..072391606d574 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IndexDocumentParams, + IndicesCreateParams, + DeleteDocumentResponse, + DeleteDocumentParams, +} from 'elasticsearch'; +import { KibanaRequest } from 'src/core/server'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { + ESSearchResponse, + ESSearchRequest, +} from '../../../../../typings/elasticsearch'; +import { callClientWithDebug } from '../call_client_with_debug'; + +// `type` was deprecated in 7.0 +export type APMIndexDocumentParams = Omit, 'type'>; + +export type APMInternalClient = ReturnType; + +export function createInternalESClient({ + context, + request, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; +}) { + const { callAsInternalUser } = context.core.elasticsearch.legacy.client; + + const callEs = (operationName: string, params: Record) => { + return callClientWithDebug({ + apiCaller: callAsInternalUser, + operationName, + params, + request, + debug: context.params.query._debug, + }); + }; + + return { + search: async < + TDocument = unknown, + TSearchRequest extends ESSearchRequest = ESSearchRequest + >( + params: TSearchRequest + ): Promise> => { + return callEs('search', params); + }, + index: (params: APMIndexDocumentParams) => { + return callEs('index', params); + }, + delete: ( + params: Omit + ): Promise => { + return callEs('delete', params); + }, + indicesCreate: (params: IndicesCreateParams) => { + return callEs('indices.create', params); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts deleted file mode 100644 index 61c9d751bf533..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isApmIndex } from './es_client'; - -describe('isApmIndex', () => { - const apmIndices = [ - 'apm-*-metric-*', - 'apm-*-onboarding-*', - 'apm-*-span-*', - 'apm-*-transaction-*', - 'apm-*-error-*', - ]; - describe('when indexParam is a string', () => { - it('should return true if it matches any of the items in apmIndices', () => { - const indexParam = 'apm-*-transaction-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it('should return false if it does not match any of the items in `apmIndices`', () => { - const indexParam = '.ml-anomalies-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is an array', () => { - it('should return true if all values in `indexParam` matches values in `apmIndices`', () => { - const indexParam = ['apm-*-transaction-*', 'apm-*-span-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it("should return false if some of the values don't match with `apmIndices`", () => { - const indexParam = ['apm-*-transaction-*', '.ml-anomalies-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is neither a string or an array', () => { - it('should return false', () => { - [true, false, undefined].forEach((indexParam) => { - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts deleted file mode 100644 index 2d730933e2473..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ /dev/null @@ -1,228 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-console */ -import { - IndexDocumentParams, - SearchParams, - IndicesCreateParams, - DeleteDocumentResponse, - DeleteDocumentParams, -} from 'elasticsearch'; -import { cloneDeep, isString, merge } from 'lodash'; -import { KibanaRequest } from 'src/core/server'; -import chalk from 'chalk'; -import { - ESSearchRequest, - ESSearchResponse, -} from '../../../typings/elasticsearch'; -import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { pickKeys } from '../../../common/utils/pick_keys'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -// `type` was deprecated in 7.0 -export type APMIndexDocumentParams = Omit, 'type'>; - -export interface IndexPrivileges { - has_all_requested: boolean; - index: Record; -} - -interface IndexPrivilegesParams { - index: Array<{ - names: string[] | string; - privileges: string[]; - }>; -} - -export function isApmIndex( - apmIndices: string[], - indexParam: SearchParams['index'] -) { - if (isString(indexParam)) { - return apmIndices.includes(indexParam); - } else if (Array.isArray(indexParam)) { - // return false if at least one of the indices is not an APM index - return indexParam.every((index) => apmIndices.includes(index)); - } - return false; -} - -function addFilterForLegacyData( - apmIndices: string[], - params: ESSearchRequest, - { includeLegacyData = false } = {} -): SearchParams { - // search across all data (including data) - if (includeLegacyData || !isApmIndex(apmIndices, params.index)) { - return params; - } - - const nextParams = merge( - { - body: { - query: { - bool: { - filter: [], - }, - }, - }, - }, - cloneDeep(params) - ); - - // add filter for omitting pre-7.x data - nextParams.body.query.bool.filter.push({ - range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, - }); - - return nextParams; -} - -// add additional params for search (aka: read) requests -function getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - includeLegacyData, -}: { - context: APMRequestHandlerContext; - params: ESSearchRequest; - indices: ApmIndicesConfig; - includeFrozen: boolean; - includeLegacyData?: boolean; -}) { - // Get indices for legacy data filter (only those which apply) - const apmIndices = Object.values( - pickKeys( - indices, - 'apm_oss.sourcemapIndices', - 'apm_oss.errorIndices', - 'apm_oss.onboardingIndices', - 'apm_oss.spanIndices', - 'apm_oss.transactionIndices', - 'apm_oss.metricsIndices' - ) - ); - return { - ...addFilterForLegacyData(apmIndices, params, { includeLegacyData }), // filter out pre-7.0 data - ignore_throttled: !includeFrozen, // whether to query frozen indices or not - }; -} - -interface APMOptions { - includeLegacyData: boolean; -} - -interface ClientCreateOptions { - clientAsInternalUser?: boolean; - indices: ApmIndicesConfig; - includeFrozen: boolean; -} - -export type ESClient = ReturnType; - -function formatObj(obj: Record) { - return JSON.stringify(obj, null, 2); -} - -export function getESClient( - context: APMRequestHandlerContext, - request: KibanaRequest, - { clientAsInternalUser = false, indices, includeFrozen }: ClientCreateOptions -) { - const { - callAsCurrentUser, - callAsInternalUser, - } = context.core.elasticsearch.legacy.client; - - async function callEs(operationName: string, params: Record) { - const startTime = process.hrtime(); - - let res: any; - let esError = null; - try { - res = clientAsInternalUser - ? await callAsInternalUser(operationName, params) - : await callAsCurrentUser(operationName, params); - } catch (e) { - // catch error and throw after outputting debug info - esError = e; - } - - if (context.params.query._debug) { - const highlightColor = esError ? 'bgRed' : 'inverse'; - const diff = process.hrtime(startTime); - const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; - const routeInfo = `${request.route.method.toUpperCase()} ${ - request.route.path - }`; - - console.log( - chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) - ); - - if (operationName === 'search') { - console.log(`GET ${params.index}/_${operationName}`); - console.log(formatObj(params.body)); - } else { - console.log(chalk.bold('ES operation:'), operationName); - - console.log(chalk.bold('ES query:')); - console.log(formatObj(params)); - } - console.log(`\n`); - } - - if (esError) { - throw esError; - } - - return res; - } - - return { - search: async < - TDocument = unknown, - TSearchRequest extends ESSearchRequest = {} - >( - params: TSearchRequest, - apmOptions?: APMOptions - ): Promise> => { - const nextParams = await getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - ...apmOptions, - }); - - return callEs('search', nextParams); - }, - index: (params: APMIndexDocumentParams) => { - return callEs('index', params); - }, - delete: ( - params: Omit - ): Promise => { - return callEs('delete', params); - }, - indicesCreate: (params: IndicesCreateParams) => { - return callEs('indices.create', params); - }, - hasPrivileges: ( - params: IndexPrivilegesParams - ): Promise => { - return callEs('transport.request', { - method: 'POST', - path: '/_security/user/_has_privileges', - body: params, - }); - }, - }; -} diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 5a4bc62b87486..d8dbd8273f476 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -7,6 +7,8 @@ import { setupRequest } from './setup_request'; import { APMConfig } from '../..'; import { APMRequestHandlerContext } from '../../routes/typings'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; jest.mock('../settings/apm_indices/get_apm_indices', () => ({ getApmIndices: async () => ({ @@ -93,163 +95,175 @@ function getMockRequest() { } describe('setupRequest', () => { - it('should call callWithRequest with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*', body: { foo: 'bar' } } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - it('should call callWithInternalUser with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { internalClient } = await setupRequest(mockContext, mockRequest); - await internalClient.search({ - index: 'apm-*', - body: { foo: 'bar' }, - } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsInternalUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - describe('observer.version_major filter', () => { - describe('if index is apm-*', () => { - it('should merge `observer.version_major` filter with existing boolean filters', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, - }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ + describe('with default args', () => { + it('calls callWithRequest', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { events: [ProcessorEvent.transaction] }, + body: { foo: 'bar' }, + }); + expect( + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', query: { bool: { filter: [ - { term: 'someTerm' }, + { terms: { 'processor.event': ['transaction'] } }, { range: { 'observer.version_major': { gte: 7 } } }, ], }, }, - }); + }, + ignore_throttled: true, }); + }); - it('should add `observer.version_major` filter if none exists', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*' }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }); + it('calls callWithInternalUser', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { internalClient } = await setupRequest(mockContext, mockRequest); + await internalClient.search({ + index: ['apm-*'], + body: { foo: 'bar' }, + } as any); + expect( + mockContext.core.elasticsearch.legacy.client.callAsInternalUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', + }, }); + }); + }); - it('should not add `observer.version_major` filter if `includeLegacyData=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search( - { - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + describe('with a bool filter', () => { + it('adds a range filter for `observer.version_major` to the existing filter', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { term: 'someTerm' }, + { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], }, - { - includeLegacyData: true, - } - ); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { bool: { filter: [{ term: 'someTerm' }] } }, - }); + }, }); }); - it('if index is not an APM index, it should not add `observer.version_major` filter', async () => { + it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => { const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: '.ml-*', - body: { - query: { bool: { filter: [{ term: 'someTerm' }] } }, + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search( + { + apm: { + events: [ProcessorEvent.error], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, }, - }); + { + includeLegacyData: true, + } + ); const params = mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock .calls[0][1]; expect(params.body).toEqual({ query: { bool: { - filter: [{ term: 'someTerm' }], + filter: [ + { term: 'someTerm' }, + { + terms: { + [PROCESSOR_EVENT]: ['error'], + }, + }, + ], }, }, }); }); }); +}); - describe('ignore_throttled', () => { - it('should set `ignore_throttled=true` if `includeFrozen=false`', async () => { - const { mockContext, mockRequest } = getMockRequest(); +describe('without a bool filter', () => { + it('adds a range filter for `observer.version_major`', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { terms: { [PROCESSOR_EVENT]: ['error'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], + }, + }, + }); + }); +}); - // mock includeFrozen to return false - mockContext.core.uiSettings.client.get.mockResolvedValue(false); +describe('with includeFrozen=false', () => { + it('sets `ignore_throttled=true`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return false + mockContext.core.uiSettings.client.get.mockResolvedValue(false); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(true); + await apmEventClient.search({ + apm: { + events: [], + }, }); - it('should set `ignore_throttled=false` if `includeFrozen=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(true); + }); +}); - // mock includeFrozen to return true - mockContext.core.uiSettings.client.get.mockResolvedValue(true); +describe('with includeFrozen=true', () => { + it('sets `ignore_throttled=false`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return true + mockContext.core.uiSettings.client.get.mockResolvedValue(true); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(false); + await apmEventClient.search({ + apm: { events: [] }, }); + + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(false); }); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 6f381d4945ab4..ddad2eb2d22dc 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -13,11 +13,17 @@ import { ApmIndicesConfig, } from '../settings/apm_indices/get_apm_indices'; import { ESFilter } from '../../../typings/elasticsearch'; -import { ESClient } from './es_client'; import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; import { APMRequestHandlerContext } from '../../routes/typings'; -import { getESClient } from './es_client'; import { ProcessorEvent } from '../../../common/processor_event'; +import { + APMEventClient, + createApmEventClient, +} from './create_es_client/create_apm_event_client'; +import { + APMInternalClient, + createInternalESClient, +} from './create_es_client/create_internal_es_client'; function decodeUiFilters(uiFiltersEncoded?: string) { if (!uiFiltersEncoded) { @@ -30,8 +36,8 @@ function decodeUiFilters(uiFiltersEncoded?: string) { // https://github.com/microsoft/TypeScript/issues/34933 export interface Setup { - client: ESClient; - internalClient: ESClient; + apmEventClient: APMEventClient; + internalClient: APMInternalClient; ml?: ReturnType; config: APMConfig; indices: ApmIndicesConfig; @@ -78,22 +84,19 @@ export async function setupRequest( context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); - const createClientOptions = { - indices, - includeFrozen, - }; - const uiFiltersES = decodeUiFilters(query.uiFilters); const coreSetupRequest = { indices, - client: getESClient(context, request, { - clientAsInternalUser: false, - ...createClientOptions, + apmEventClient: createApmEventClient({ + context, + request, + indices, + options: { includeFrozen }, }), - internalClient: getESClient(context, request, { - clientAsInternalUser: true, - ...createClientOptions, + internalClient: createInternalESClient({ + context, + request, }), ml: getMlSetup(context, request), config, diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index ee03e77de3580..cb30c6c064848 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -11,7 +11,10 @@ import { IIndexPattern, } from '../../../../../../src/plugins/data/server'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; import { APMRequestHandlerContext } from '../../routes/typings'; const cache = new LRU({ @@ -27,7 +30,7 @@ export const getDynamicIndexPattern = async ({ }: { context: APMRequestHandlerContext; indices: ApmIndicesConfig; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; }) => { const patternIndices = getPatternIndices(indices, processorEvent); const indexPatternTitle = patternIndices.join(','); @@ -75,17 +78,17 @@ export const getDynamicIndexPattern = async ({ function getPatternIndices( indices: ApmIndicesConfig, - processorEvent?: ProcessorEvent + processorEvent?: UIProcessorEvent ) { const indexNames = processorEvent ? [processorEvent] - : ['transaction' as const, 'metric' as const, 'error' as const]; + : [ProcessorEvent.transaction, ProcessorEvent.metric, ProcessorEvent.error]; const indicesMap = { - transaction: indices['apm_oss.transactionIndices'], - metric: indices['apm_oss.metricsIndices'], - error: indices['apm_oss.errorIndices'], + [ProcessorEvent.transaction]: indices['apm_oss.transactionIndices'], + [ProcessorEvent.metric]: indices['apm_oss.metricsIndices'], + [ProcessorEvent.error]: indices['apm_oss.errorIndices'], }; - return indexNames.map((name) => indicesMap[name]); + return indexNames.map((name) => indicesMap[name as UIProcessorEvent]); } diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index d8119ac96a536..b88c90a213c67 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`metrics queries with a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -66,11 +71,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -95,12 +95,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -155,11 +159,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -189,12 +188,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -251,11 +254,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -290,12 +288,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -350,11 +352,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -384,12 +381,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -434,11 +435,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -468,12 +464,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -538,11 +538,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -573,12 +568,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -633,11 +632,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -673,12 +667,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -735,11 +733,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -780,12 +773,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -840,11 +837,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -880,12 +872,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -930,11 +926,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -970,12 +961,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -1040,11 +1035,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1064,12 +1054,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -1124,11 +1118,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1153,12 +1142,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -1215,11 +1208,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1249,12 +1237,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -1309,11 +1301,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1338,12 +1325,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -1388,11 +1379,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1417,6 +1403,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 3ed6e4a944b51..e5c573ba1ec02 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -18,8 +18,8 @@ import { } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; import { ChartBase } from '../../../types'; -import { getMetricsProjection } from '../../../../../../common/projections/metrics'; -import { mergeProjection } from '../../../../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../../../../projections/metrics'; +import { mergeProjection } from '../../../../../projections/util/merge_projection'; import { AGENT_NAME, LABEL_NAME, @@ -42,7 +42,7 @@ export async function fetchAndTransformGcMetrics({ chartBase: ChartBase; fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const { bucketSize } = getBucketSize(start, end, 'auto'); @@ -105,7 +105,7 @@ export async function fetchAndTransformGcMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); const { aggregations } = response; diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 895920a9b6c7d..f6e201b395c37 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -5,7 +5,6 @@ */ import { Unionize, Overwrite } from 'utility-types'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange, @@ -14,9 +13,10 @@ import { import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { ChartBase } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; -import { getMetricsProjection } from '../../../common/projections/metrics'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../projections/metrics'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; type MetricsAggregationMap = Unionize<{ min: AggregationOptionsByType['min']; @@ -28,7 +28,7 @@ type MetricsAggregationMap = Unionize<{ type MetricAggs = Record; export type GenericMetricsRequest = Overwrite< - ESSearchRequest, + APMEventESSearchRequest, { body: { aggs: { @@ -65,7 +65,7 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const projection = getMetricsProjection({ setup, @@ -91,7 +91,7 @@ export async function fetchAndTransformMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); return transformDataToMetricsChart(response, chartBase); } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts index 4c4d058c7139d..8a1f3cb0e0149 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts @@ -6,10 +6,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { - SERVICE_NAME, - PROCESSOR_EVENT, -} from '../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getServiceCount({ @@ -17,36 +14,27 @@ export async function getServiceCount({ }: { setup: Setup & SetupTimeRange; }) { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { range: rangeFilter(start, end) }, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.transaction, - ProcessorEvent.metric, - ], - }, - }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); return aggregations?.serviceCount.value || 0; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts index 0d1a4274c16dc..116b37a395299 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts @@ -10,7 +10,6 @@ */ import { rangeFilter } from '../../../common/utils/range_filter'; import { Coordinates } from '../../../../observability/public'; -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ProcessorEvent } from '../../../common/processor_event'; @@ -21,18 +20,17 @@ export async function getTransactionCoordinates({ setup: Setup & SetupTimeRange; bucketSize: string; }): Promise { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; - const { aggregations } = await client.search({ - index: indices['apm_oss.transactionIndices'], + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { range: rangeFilter(start, end) }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index fc7445ab4a225..66d82b9f88355 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -3,41 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; export async function hasData({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; try { const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, terminateAfter: 1, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.metric, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response.hits.total.value > 0; } catch (e) { return false; diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 602eb88ba8940..c5264373ea495 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`rum client dashboard queries fetches client metrics 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "backEnd": Object { @@ -34,11 +39,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page load distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "durPercentiles": Object { @@ -101,11 +105,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -126,12 +125,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page view trends 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "pageViews": Object { @@ -154,11 +157,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -179,12 +177,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches rum services 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -206,11 +208,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -231,6 +228,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index 8b3f733fc402a..194c136e2b3d0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -45,9 +45,9 @@ export async function getClientMetrics({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { backEnd, domInteractive, pageViews } = response.aggregations!; // Divide by 1000 to convert ms into seconds diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index e847a87264759..2a0c709ea9235 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -57,12 +57,12 @@ export async function getPageLoadDistribution({ }, }); - const { client } = setup; + const { apmEventClient } = setup; const { aggregations, hits: { total }, - } = await client.search(params); + } = await apmEventClient.search(params); if (total.value === 0) { return null; @@ -130,9 +130,9 @@ const getPercentilesDistribution = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDist = aggregations?.loadDistribution.values ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 30b2677d3c217..23169ddaca534 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -56,9 +56,9 @@ export async function getPageViewTrends({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.pageViews.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index ea9d701e64c3d..ffb06e649b9be 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -16,6 +17,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution'; @@ -53,11 +55,11 @@ export const getPageLoadDistBreakdown = async ( }); const params = mergeProjection(projection, { + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { breakdowns: { terms: { @@ -67,7 +69,7 @@ export const getPageLoadDistBreakdown = async ( aggs: { page_dist: { percentile_ranks: { - field: 'transaction.duration.us', + field: TRANSACTION_DURATION, values: stepValues, keyed: false, hdr: { @@ -81,9 +83,9 @@ export const getPageLoadDistBreakdown = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDistBreakdowns = aggregations?.breakdowns.buckets; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts index 5957a25239307..9bfa109f00faf 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; export async function getRumServices({ setup, @@ -38,9 +38,9 @@ export async function getRumServices({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.services.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index a14affb6eeec5..3681923b484b0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -55,9 +55,9 @@ export async function getVisitorBreakdown({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { browsers, os, devices } = response.aggregations!; return { diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts index 08c8aba5f0207..14047f4bacea9 100644 --- a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - PROCESSOR_EVENT, - TRACE_ID, -} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { TRACE_ID } from '../../../common/elasticsearch_fieldnames'; import { ConnectionNode, ExternalConnectionNode, @@ -18,23 +16,17 @@ export async function fetchServicePathsFromTraceIds( setup: Setup, traceIds: string[] ) { - const { indices, client } = setup; + const { apmEventClient } = setup; const serviceMapParams = { - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ - { - terms: { - [PROCESSOR_EVENT]: ['span', 'transaction'], - }, - }, { terms: { [TRACE_ID]: traceIds, @@ -212,7 +204,7 @@ export async function fetchServicePathsFromTraceIds( }, }; - const serviceMapFromTraceIdsScriptResponse = await client.search( + const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search( serviceMapParams ); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 3e5ef5eb37b02..03716382af859 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -4,14 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from 'kibana/server'; +import Boom from 'boom'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseReturnType } from '../../../typings/common'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; -import { ServiceAnomalyStats } from '../../../common/anomaly_detection'; -import { APM_ML_JOB_GROUP } from '../anomaly_detection/constants'; +import { + ServiceAnomalyStats, + ML_ERRORS, +} from '../../../common/anomaly_detection'; +import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; @@ -31,29 +35,15 @@ export async function getServiceAnomalies({ const { ml, start, end } = setup; if (!ml) { - logger.warn('Anomaly detection plugin is not available.'); - return DEFAULT_ANOMALIES; + throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } + const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - logger.warn('Anomaly detection feature is not enabled for the space.'); - return DEFAULT_ANOMALIES; - } - if (!mlCapabilities.isPlatinumOrTrialLicense) { - logger.warn( - 'Unable to create anomaly detection jobs due to insufficient license.' - ); - return DEFAULT_ANOMALIES; - } - - let mlJobIds: string[] = []; - try { - mlJobIds = await getMLJobIds(ml, environment); - } catch (error) { - logger.error(error); - return DEFAULT_ANOMALIES; + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } + const mlJobIds = await getMLJobIds(ml, environment); const params = { body: { size: 0, @@ -92,7 +82,9 @@ export async function getServiceAnomalies({ }, }, }; + const response = await ml.mlSystem.mlAnomalySearch(params); + return { mlJobIds, serviceAnomalies: transformResponseToServiceAnomalies( @@ -147,7 +139,7 @@ export async function getMLJobIds( ml: Required['ml'], environment?: string ) { - const response = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP); + const response = await getMlJobsWithAPMGroup(ml); // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings` // and checking that it is compatable. const mlJobs = response.jobs.filter( diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index ea2bb14efdfc7..b162c3b61d928 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -10,8 +10,8 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getServicesProjection } from '../../../common/projections/services'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServicesProjection } from '../../projections/services'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; @@ -118,9 +118,9 @@ async function getServicesData(options: IEnvOptions) { }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); return ( response.aggregations?.services.buckets.map((bucket) => { @@ -142,11 +142,14 @@ export async function getServiceMap(options: IEnvOptions) { const { logger } = options; const anomaliesPromise: Promise = getServiceAnomalies( options + + // always catch error to avoid breaking service maps if there is a problem with ML ).catch((error) => { logger.warn(`Unable to retrieve anomalies for service maps.`); logger.error(error); return DEFAULT_ANOMALIES; }); + const [connectionData, servicesData, anomalies] = await Promise.all([ getConnectionData(options), getServicesData(options), diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index 1e0d001340edf..d1c99d778c8f0 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -12,7 +12,7 @@ describe('getServiceMapServiceNodeInfo', () => { describe('with no results', () => { it('returns null data', async () => { const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 0 } }, @@ -49,7 +49,7 @@ describe('getServiceMapServiceNodeInfo', () => { }); const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 1 } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 0f7136d6d74a4..330d38739a063 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -6,13 +6,12 @@ import { UIFilters } from '../../../typings/ui_filters'; import { + SERVICE_NAME, + TRANSACTION_DURATION, TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -109,17 +108,18 @@ async function getTransactionStats({ avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }> { - const { indices, client } = setup; + const { apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ ...filter, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { terms: { [TRANSACTION_TYPE]: [ @@ -135,8 +135,9 @@ async function getTransactionStats({ aggs: { duration: { avg: { field: TRANSACTION_DURATION } } }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); const docCount = response.hits.total.value; + return { avgTransactionDuration: response.aggregations?.duration.value ?? null, avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, @@ -147,18 +148,17 @@ async function getCpuStats({ setup, filter, }: TaskParameters): Promise<{ avgCpuUsage: number | null }> { - const { indices, client } = setup; + const { apmEventClient } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, - { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }, - ]), + filter: [...filter, { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }], }, }, aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } }, @@ -172,17 +172,19 @@ async function getMemoryStats({ setup, filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { - const { client, indices } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const { apmEventClient } = setup; + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: 'metric' } }, + filter: [ + ...filter, { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ]), + ], }, }, aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 11c3a00f32980..d6d681f24ab85 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq, take, sortBy } from 'lodash'; +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, TRACE_ID, @@ -26,18 +26,13 @@ export async function getTraceSampleIds({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices, config } = setup; + const { start, end, apmEventClient, config } = setup; const rangeQuery = { range: rangeFilter(start, end) }; const query = { bool: { filter: [ - { - term: { - [PROCESSOR_EVENT]: 'span', - }, - }, { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE, @@ -67,7 +62,9 @@ export async function getTraceSampleIds({ const samplerShardSize = traceIdBucketSize * 10; const params = { - index: [indices['apm_oss.spanIndices']], + apm: { + events: [ProcessorEvent.span], + }, body: { size: 0, query, @@ -126,9 +123,7 @@ export async function getTraceSampleIds({ }, }; - const tracesSampleResponse = await client.search( - params - ); + const tracesSampleResponse = await apmEventClient.search(params); // make sure at least one trace per composite/connection bucket // is queried diff --git a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap index 3935ecda42db9..87aca0d056909 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`service node queries fetches metadata for a service node 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -30,11 +35,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches metadata for unidentified service nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -93,11 +97,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches services nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nodes": Object { @@ -174,11 +177,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -197,6 +195,5 @@ Object { }, }, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index de66c242815a4..a83aba192dba9 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -9,8 +9,8 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { METRIC_PROCESS_CPU_PERCENT, @@ -26,7 +26,7 @@ const getServiceNodes = async ({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) => { - const { client } = setup; + const { apmEventClient } = setup; const projection = getServiceNodesProjection({ setup, serviceName }); @@ -66,7 +66,7 @@ const getServiceNodes = async ({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); if (!response.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 0fc1f89a3723b..ca86c1d93fa6e 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -2,48 +2,32 @@ exports[`services queries fetches the agent status 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "metric", + "sourcemap", + "transaction", + ], + }, "body": Object { - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "error", - "metric", - "sourcemap", - "transaction", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; exports[`services queries fetches the legacy data status 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "observer.version_major": Object { @@ -56,13 +40,19 @@ Object { }, "size": 0, }, - "index": "myIndex", "terminateAfter": 1, } `; exports[`services queries fetches the service agent name 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "transaction", + "metric", + ], + }, "body": Object { "aggs": Object { "agents": Object { @@ -80,15 +70,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "error", - "transaction", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -103,11 +84,6 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; @@ -115,6 +91,11 @@ Object { exports[`services queries fetches the service items 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -148,20 +129,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", - "size": 0, }, Object { + "apm": Object { + "events": Array [ + "metric", + "error", + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -198,27 +179,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "metric", - "error", - "transaction", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -245,19 +217,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -284,19 +255,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "metric", + "transaction", + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -330,31 +302,22 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, ] `; exports[`services queries fetches the service transaction types 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "types": Object { @@ -372,13 +335,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -393,6 +349,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 6a8aaf8dca8a6..ad3f47d443b87 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { isNumber } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SetupTimeRange, Setup } from '../../helpers/setup_request'; import { ESFilter } from '../../../../typings/elasticsearch'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; @@ -24,23 +24,24 @@ export async function getDerivedServiceAnnotations({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, ...getEnvironmentUiFilterES(environment), ]; const versions = ( - await client.search({ - index: indices['apm_oss.transactionIndices'], + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ range: rangeFilter(start, end) }), + filter: [...filter, { range: rangeFilter(start, end) }], }, }, aggs: { @@ -59,17 +60,15 @@ export async function getDerivedServiceAnnotations({ } const annotations = await Promise.all( versions.map(async (version) => { - const response = await client.search({ - index: indices['apm_oss.transactionIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ - term: { - [SERVICE_VERSION]: version, - }, - }), + filter: [...filter, { term: { [SERVICE_VERSION]: version } }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index 8d75d746c7fca..a95c27df0e502 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, AGENT_NAME, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -15,24 +15,23 @@ export async function getServiceAgentName( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { - terms: { [PROCESSOR_EVENT]: ['error', 'transaction', 'metric'] }, - }, { range: rangeFilter(start, end) }, ], }, @@ -45,7 +44,7 @@ export async function getServiceAgentName( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agents.buckets[0]?.key as string | undefined; return { agentName }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index c2d9fa6c1df39..fca472b0ce8c2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -14,8 +14,8 @@ import { CONTAINER_ID, } from '../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; export async function getServiceNodeMetadata({ serviceName, @@ -26,7 +26,7 @@ export async function getServiceNodeMetadata({ serviceNodeName: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; const query = mergeProjection( getServiceNodesProjection({ @@ -55,7 +55,7 @@ export async function getServiceNodeMetadata({ } ); - const response = await client.search(query); + const response = await apmEventClient.search(query); return { host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index d88be4055dc21..6c6e03ab0b46f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; @@ -15,17 +15,18 @@ export async function getServiceTransactionTypes( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, { range: rangeFilter(start, end) }, ], }, @@ -38,7 +39,7 @@ export async function getServiceTransactionTypes( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const transactionTypes = aggregations?.types.buckets.map((bucket) => bucket.key as string) || []; return { transactionTypes }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts index dde726c51393f..1be95967cb47a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts @@ -4,33 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - OBSERVER_VERSION_MAJOR, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { OBSERVER_VERSION_MAJOR } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; // returns true if 6.x data is found export async function getLegacyDataStatus(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, - { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }, - ], + filter: [{ range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }], }, }, }, }; - const resp = await client.search(params, { includeLegacyData: true }); + const resp = await apmEventClient.search(params, { includeLegacyData: true }); const hasLegacyData = resp.hits.total.value > 0; return hasLegacyData; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 14772e77fe1c2..d888b43b63fac 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -10,7 +10,7 @@ import { SetupTimeRange, SetupUIFilters, } from '../../helpers/setup_request'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; import { getTransactionDurationAverages, getAgentNames, @@ -25,7 +25,7 @@ export type ServicesItemsProjection = ReturnType; export async function getServicesItems(setup: ServicesItemsSetup) { const params = { - projection: getServicesProjection({ setup, noEvents: true }), + projection: getServicesProjection({ setup }), setup, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index de699028f5675..ddce3b667a603 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -5,12 +5,11 @@ */ import { - PROCESSOR_EVENT, TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, } from '../../../../common/elasticsearch_fieldnames'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; +import { mergeProjection } from '../../../projections/util/merge_projection'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ServicesItemsSetup, @@ -31,22 +30,15 @@ export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; + const { apmEventClient } = setup; - const response = await client.search( + const response = await apmEventClient.search( mergeProjection(projection, { - size: 0, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { - query: { - bool: { - filter: projection.body.query.bool.filter.concat({ - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }), - }, - }, + size: 0, aggs: { services: { terms: { @@ -82,32 +74,18 @@ export const getAgentNames = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.error, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.metric, - ProcessorEvent.error, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -136,11 +114,7 @@ export const getAgentNames = async ({ return aggregations.services.buckets.map((bucket) => ({ serviceName: bucket.key as string, - agentName: (bucket.agent_name.hits.hits[0]?._source as { - agent: { - name: string; - }; - }).agent.name, + agentName: bucket.agent_name.hits.hits[0]?._source.agent.name, })); }; @@ -148,24 +122,14 @@ export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -199,24 +163,14 @@ export const getErrorRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.error, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -250,32 +204,18 @@ export const getEnvironments = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.transaction, + ProcessorEvent.error, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.transaction, - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts index 42f53fc93fa60..eed9f2588152d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts @@ -4,43 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; // Note: this logic is duplicated in tutorials/apm/envs/on_prem export async function hasHistoricalAgentData(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - indices['apm_oss.sourcemapIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.metric, + ProcessorEvent.sourcemap, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - 'error', - 'metric', - 'sourcemap', - 'transaction', - ], - }, - }, - ], - }, - }, }, }; - const resp = await client.search(params); - const hasHistorialAgentData = resp.hits.total.value > 0; - return hasHistorialAgentData; + const resp = await apmEventClient.search(params); + return resp.hits.total.value > 0; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 24a1840bc0ab8..2b465a0f87475 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -115,6 +115,13 @@ Object { exports[`agent configuration queries getServiceNames fetches service names 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -124,28 +131,8 @@ Object { }, }, }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 4d61e1e9ae284..86aeb95e165a0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -10,7 +10,7 @@ import { AgentConfiguration, AgentConfigurationIntake, } from '../../../../common/agent_configuration/configuration_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateConfiguration({ configurationId, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts index 39674ee57abf6..9f0e65d492a8f 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { AGENT_NAME } from '../../../../common/elasticsearch_fieldnames'; export async function getAgentNameByService({ @@ -18,25 +16,22 @@ export async function getAgentNameByService({ serviceName: string; setup: Setup; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - { term: { [SERVICE_NAME]: serviceName } }, - ], + filter: [{ term: { [SERVICE_NAME]: serviceName } }], }, }, aggs: { @@ -47,7 +42,7 @@ export async function getAgentNameByService({ }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agent_names.buckets[0]?.key; return agentName as string | undefined; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 068bb30ddcf79..8b6c1d82beab0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -4,37 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; export async function getServiceNames({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -46,7 +37,7 @@ export async function getServiceNames({ setup }: { setup: Setup }) { }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const serviceNames = resp.aggregations?.services.buckets .map((bucket) => bucket.key as string) diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap index a91641b592526..0649c8c38d29a 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap @@ -2,15 +2,15 @@ exports[`custom link get transaction fetches with all filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "terms": Object { "service.name": Array [ @@ -43,7 +43,6 @@ Object { }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } @@ -51,20 +50,18 @@ Object { exports[`custom link get transaction fetches without filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { - "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, - ], + "filter": Array [], }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts index 16a694c04c485..48b115619283c 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts @@ -8,9 +8,9 @@ import { CustomLink, CustomLinkES, } from '../../../../common/custom_link/custom_link_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; import { Setup } from '../../helpers/setup_request'; import { toESFormat } from './helper'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateCustomLink({ customLinkId, diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts index e3becc040580f..9bf489e768a4b 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import * as t from 'io-ts'; -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { filterOptionsRt } from './custom_link_types'; @@ -18,7 +16,7 @@ export async function getTransaction({ setup: Setup; filters?: t.TypeOf; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const esFilters = Object.entries(filters) // loops through the filters splitting the value by comma and removing white spaces @@ -32,19 +30,18 @@ export async function getTransaction({ const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, size: 1, body: { query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...esFilters, - ], + filter: esFilters, }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap index 0a9f9d38b2be7..3c521839b587e 100644 --- a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`trace queries fetches a trace 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "by_transaction_id": Object { @@ -20,11 +25,6 @@ Object { "trace.id": "foo", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -48,6 +48,5 @@ Object { }, "size": "myIndex", }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index f9374558dfeeb..17f9743ae9f00 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, TRANSACTION_DURATION, @@ -13,8 +13,6 @@ import { TRANSACTION_ID, ERROR_LOG_LEVEL, } from '../../../common/elasticsearch_fieldnames'; -import { Span } from '../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -28,19 +26,20 @@ export async function getTraceItems( traceId: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, config, indices } = setup; + const { start, end, apmEventClient, config } = setup; const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; const excludedLogLevels = ['debug', 'info', 'warning']; - const errorResponsePromise = client.search({ - index: indices['apm_oss.errorIndices'], + const errorResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ], must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, @@ -59,18 +58,16 @@ export async function getTraceItems( }, }); - const traceResponsePromise = client.search({ - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + const traceResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) }, ], should: { @@ -91,22 +88,17 @@ export async function getTraceItems( // explicit intermediary types to avoid TS "excessively deep" error PromiseValueType, PromiseValueType - // @ts-ignore ] = await Promise.all([errorResponsePromise, traceResponsePromise]); const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - const items = (traceResponse.hits.hits as Array<{ - _source: Transaction | Span; - }>).map((hit) => hit._source); + const items = traceResponse.hits.hits.map((hit) => hit._source); const errorFrequencies: { errorsPerTransaction: ErrorsPerTransaction; errorDocs: APMError[]; } = { - errorDocs: errorResponse.hits.hits.map( - ({ _source }) => _source as APMError - ), + errorDocs: errorResponse.hits.hits.map(({ _source }) => _source), errorsPerTransaction: errorResponse.aggregations?.by_transaction_id.buckets.reduce( (acc, current) => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index deca46f4ebd0c..0ea7bcf7ce8ab 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -3,6 +3,11 @@ exports[`transaction group queries fetches top traces 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -46,11 +51,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -84,10 +84,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -131,11 +135,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -152,10 +151,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -199,11 +202,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -220,7 +218,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] @@ -229,6 +226,11 @@ Array [ exports[`transaction group queries fetches top transactions 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -257,11 +259,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -298,10 +295,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -330,11 +331,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -354,10 +350,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -386,11 +386,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -410,10 +405,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -448,11 +447,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -472,7 +466,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 73bf1d01924e7..b06d1a8af3bc5 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -7,13 +7,12 @@ import { take, sortBy } from 'lodash'; import { Unionize } from 'utility-types'; import moment from 'moment'; import { joinByKey } from '../../../common/utils/join_by_key'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getTransactionGroupsProjection } from '../../projections/transaction_groups'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { Transaction } from '../../../typings/es_schemas/ui/transaction'; @@ -45,7 +44,9 @@ export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export type TransactionGroupRequestBase = ESSearchRequest & { +export type TransactionGroupRequestBase = ReturnType< + typeof getTransactionGroupsProjection +> & { body: { aggs: { transaction_groups: Unionize< diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 6a1ee8daad7c7..8fb2ceb30db85 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -5,7 +5,6 @@ */ import { mean } from 'lodash'; import { - PROCESSOR_EVENT, HTTP_RESPONSE_STATUS_CODE, TRANSACTION_NAME, TRANSACTION_TYPE, @@ -31,7 +30,7 @@ export async function getErrorRate({ transactionName?: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const transactionNamefilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -42,7 +41,6 @@ export async function getErrorRate({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: rangeFilter(start, end) }, { exists: { field: HTTP_RESPONSE_STATUS_CODE } }, ...transactionNamefilter, @@ -51,7 +49,9 @@ export async function getErrorRate({ ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -68,7 +68,7 @@ export async function getErrorRate({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const noHits = resp.hits.total.value === 0; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 59fb370113ec2..7d45f39e08a83 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -5,7 +5,6 @@ */ import { merge } from 'lodash'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { TRANSACTION_SAMPLED, TRANSACTION_DURATION, @@ -52,7 +51,7 @@ export async function getSamples({ request, setup }: MetricParams) { { '@timestamp': { order: 'desc' as const } }, ]; - const response = await setup.client.search({ + const response = await setup.apmEventClient.search({ ...params, body: { ...params.body, @@ -73,7 +72,7 @@ export async function getSamples({ request, setup }: MetricParams) { return { key: bucket.key as BucketKey, count: bucket.doc_count, - sample: bucket.sample.hits.hits[0]._source as Transaction, + sample: bucket.sample.hits.hits[0]._source, }; }); } @@ -87,7 +86,7 @@ export async function getAverages({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -108,7 +107,7 @@ export async function getSums({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -131,7 +130,7 @@ export async function getPercentiles({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index cc5900919f829..9bc4b1d69d9ac 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -2,15 +2,15 @@ exports[`transaction queries fetches a transaction 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.id": "foo", @@ -35,12 +35,16 @@ Object { }, "size": 1, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -146,11 +150,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -170,12 +169,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions for a transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -281,11 +284,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -310,12 +308,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -376,11 +378,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -405,12 +402,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -471,11 +472,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -505,12 +501,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type and transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -571,11 +571,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -610,12 +605,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "stats": Object { @@ -632,11 +631,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "baz", @@ -666,6 +660,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts index d8175a34ceb9f..278819ea20a83 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts @@ -15,7 +15,7 @@ describe('fetcher', () => { it('performs a search', async () => { const search = jest.fn(); const setup = ({ - client: { search }, + apmEventClient: { search }, indices: {}, uiFiltersES: [], } as unknown) as Setup & SetupTimeRange & SetupUIFilters; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index b4d98ec41fc2d..f68082dfaa1e1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -7,7 +7,6 @@ import { ESFilter } from '../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, USER_AGENT_NAME, @@ -23,7 +22,7 @@ import { ProcessorEvent } from '../../../../common/processor_event'; export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { - const { end, client, indices, start, uiFiltersES } = options.setup; + const { end, apmEventClient, start, uiFiltersES } = options.setup; const { serviceName, transactionName } = options; const { intervalString } = getBucketSize(start, end, 'auto'); @@ -32,7 +31,6 @@ export function fetcher(options: Options) { : []; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { range: rangeFilter(start, end) }, @@ -41,7 +39,9 @@ export function fetcher(options: Options) { ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -80,5 +80,5 @@ export function fetcher(options: Options) { }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index ea6213f64ee36..9bb42d2fa7aad 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { CLIENT_GEO_COUNTRY_ISO_CODE, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_TYPE, @@ -29,12 +29,14 @@ export async function getTransactionAvgDurationByCountry({ serviceName: string; transactionName?: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] : []; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { @@ -42,7 +44,6 @@ export async function getTransactionAvgDurationByCountry({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, ...transactionNameFilter, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } }, { range: rangeFilter(start, end) }, @@ -66,7 +67,7 @@ export async function getTransactionAvgDurationByCountry({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); if (!resp.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 85d4eab448c72..3c1618ed7715f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -26,7 +26,7 @@ function getMockSetup(esResponse: any) { return { start: 0, end: 500000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 3c48c14c2a471..7248399d1f93f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -5,6 +5,7 @@ */ import { flatten, orderBy, last } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, SPAN_SUBTYPE, @@ -13,7 +14,6 @@ import { TRANSACTION_TYPE, TRANSACTION_NAME, TRANSACTION_BREAKDOWN_COUNT, - PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { Setup, @@ -36,7 +36,7 @@ export async function getTransactionBreakdown({ transactionName?: string; transactionType: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const subAggs = { sum_all_self_times: { @@ -82,7 +82,6 @@ export async function getTransactionBreakdown({ const filters = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ]; @@ -92,7 +91,9 @@ export async function getTransactionBreakdown({ } const params = { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { @@ -110,7 +111,7 @@ export async function getTransactionBreakdown({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const formatBucket = ( aggs: diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap index 25ebb15fd73e8..7bc60a7fc7f1a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should call client with correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -64,11 +69,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -98,7 +98,6 @@ Array [ }, "size": 0, }, - "index": "myIndex", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index fb357040f5781..09e1287f032f5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { ESResponse, timeseriesFetcher } from './fetcher'; import { APMConfig } from '../../../../../server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let res: ESResponse; @@ -21,7 +21,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, @@ -54,15 +54,7 @@ describe('timeseriesFetcher', () => { it('should restrict results to only transaction documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([ - { - term: { - [PROCESSOR_EVENT]: 'transaction', - }, - } as any, - ]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.transaction]); }); it('should return correct response', () => { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 8e19af926ce02..1498c22e327d6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESFilter } from '../../../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -34,11 +34,10 @@ export function timeseriesFetcher({ transactionName: string | undefined; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const { intervalString } = getBucketSize(start, end, 'auto'); const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -54,7 +53,9 @@ export function timeseriesFetcher({ } const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter } }, @@ -95,5 +96,5 @@ export function timeseriesFetcher({ }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index 3f8bf635712be..bfe72bf7c00f9 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { ProcessorEvent } from '../../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRACE_ID, TRANSACTION_DURATION, @@ -32,17 +31,18 @@ export async function bucketFetcher( bucketSize: number, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { range: rangeFilter(start, end) }, @@ -85,7 +85,7 @@ export async function bucketFetcher( }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response; } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index 8289113fddae9..139dac3df1171 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -23,17 +23,18 @@ export async function getDistributionMax( transactionType: string, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { @@ -59,6 +60,6 @@ export async function getDistributionMax( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.aggregations ? resp.aggregations.stats.max : null; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index a7de93a3bf650..9aa1a8f4de87f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, @@ -27,16 +25,17 @@ export async function getTransaction({ traceId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const params = { - index: indices['apm_oss.transactionIndices'], + const resp = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 1, query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [TRANSACTION_ID]: transactionId } }, { term: { [TRACE_ID]: traceId } }, { range: rangeFilter(start, end) }, @@ -44,8 +43,7 @@ export async function getTransaction({ }, }, }, - }; + }); - const resp = await client.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts index ad4f58d85d188..8ba61c6c726a4 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; @@ -17,9 +15,12 @@ export async function getRootTransactionByTraceId( traceId: string, setup: Setup ) { - const { client, indices } = setup; + const { apmEventClient } = setup; + const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 1, query: { @@ -35,16 +36,13 @@ export async function getRootTransactionByTraceId( }, }, ], - filter: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ], + filter: [{ term: { [TRACE_ID]: traceId } }], }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return { transaction: resp.hits.hits[0]?._source, }; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap index 30e75f46ad5e7..d94b766aee6a8 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`ui filter queries fetches environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -14,15 +21,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -42,16 +40,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`ui filter queries fetches environments without a service name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -64,15 +64,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -87,10 +78,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index 3fca30634be6a..98f00bf8e6555 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -18,12 +18,9 @@ export async function getEnvironments( setup: Setup & SetupTimeRange, serviceName?: string ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const filter: ESFilter[] = [ - { terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } }, - { range: rangeFilter(start, end) }, - ]; + const filter: ESFilter[] = [{ range: rangeFilter(start, end) }]; if (serviceName) { filter.push({ @@ -32,11 +29,13 @@ export async function getEnvironments( } const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, body: { size: 0, query: { @@ -55,7 +54,7 @@ export async function getEnvironments( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const aggs = resp.aggregations; const environmentsBuckets = aggs?.environments.buckets || []; 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/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap index e6b6a9a52adfe..5f38432719280 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/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "by_terms": Object { @@ -28,15 +35,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -56,10 +54,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; 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/ui_filters/local_ui_filters/get_local_filter_query.ts index e892284fd87cd..cfbd79d37c041 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/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -5,8 +5,8 @@ */ import { omit } from 'lodash'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; -import { Projection } from '../../../../common/projections/typings'; +import { mergeProjection } from '../../../projections/util/merge_projection'; +import { Projection } from '../../../projections/typings'; import { UIFilters } from '../../../../typings/ui_filters'; import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_es'; import { localUIFilters, LocalUIFilterName } from './config'; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index 3833b93c8d1f7..12c02679d0859 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -5,7 +5,7 @@ */ import { cloneDeep, orderBy } from 'lodash'; import { UIFilters } from '../../../../typings/ui_filters'; -import { Projection } from '../../../../common/projections/typings'; +import { Projection } from '../../../projections/typings'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { getLocalFilterQuery } from './get_local_filter_query'; import { Setup } from '../../helpers/setup_request'; @@ -26,7 +26,7 @@ export async function getLocalUIFilters({ uiFilters: UIFilters; localFilterNames: LocalUIFilterName[]; }) { - const { client } = setup; + const { apmEventClient } = setup; const projectionWithoutAggs = cloneDeep(projection); @@ -40,7 +40,7 @@ export async function getLocalUIFilters({ localUIFilterName: name, }); - const response = await client.search(query); + const response = await apmEventClient.search(query); const filter = localUIFilters[name]; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts index ac61910968850..92ee67de49314 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -9,7 +9,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../../../public/utils/testHelpers'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; describe('local ui filter queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/common/projections/errors.ts b/x-pack/plugins/apm/server/projections/errors.ts similarity index 70% rename from x-pack/plugins/apm/common/projections/errors.ts rename to x-pack/plugins/apm/server/projections/errors.ts index 390a8a0968102..49a0e9f479d26 100644 --- a/x-pack/plugins/apm/common/projections/errors.ts +++ b/x-pack/plugins/apm/server/projections/errors.ts @@ -8,15 +8,13 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, ERROR_GROUP_ID, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getErrorGroupsProjection({ setup, @@ -25,16 +23,17 @@ export function getErrorGroupsProjection({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; return { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ], diff --git a/x-pack/plugins/apm/common/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts similarity index 72% rename from x-pack/plugins/apm/common/projections/metrics.ts rename to x-pack/plugins/apm/server/projections/metrics.ts index 45998bfe82e96..eb80a6bc73248 100644 --- a/x-pack/plugins/apm/common/projections/metrics.ts +++ b/x-pack/plugins/apm/server/projections/metrics.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, - PROCESSOR_EVENT, SERVICE_NODE_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; -import { SERVICE_NODE_NAME_MISSING } from '../service_nodes'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { SERVICE_NODE_NAME_MISSING } from '../../common/service_nodes'; +import { ProcessorEvent } from '../../common/processor_event'; function getServiceNodeNameFilters(serviceNodeName?: string) { if (!serviceNodeName) { @@ -40,18 +38,19 @@ export function getMetricsProjection({ serviceName: string; serviceNodeName?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...getServiceNodeNameFilters(serviceNodeName), ...uiFiltersES, ]; return { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { diff --git a/x-pack/plugins/apm/common/projections/rum_overview.ts b/x-pack/plugins/apm/server/projections/rum_overview.ts similarity index 71% rename from x-pack/plugins/apm/common/projections/rum_overview.ts rename to x-pack/plugins/apm/server/projections/rum_overview.ts index b1218546d09ff..4588ec2a0451f 100644 --- a/x-pack/plugins/apm/common/projections/rum_overview.ts +++ b/x-pack/plugins/apm/server/projections/rum_overview.ts @@ -8,22 +8,21 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../elasticsearch_fieldnames'; -import { rangeFilter } from '../utils/range_filter'; +import { TRANSACTION_TYPE } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getRumOverviewProjection({ setup, }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: 'page-load' } }, { // Adding this filter to cater for some inconsistent rum data @@ -36,7 +35,9 @@ export function getRumOverviewProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/service_nodes.ts b/x-pack/plugins/apm/server/projections/service_nodes.ts similarity index 88% rename from x-pack/plugins/apm/common/projections/service_nodes.ts rename to x-pack/plugins/apm/server/projections/service_nodes.ts index 1bc68f51a26ed..87fe815a12d0d 100644 --- a/x-pack/plugins/apm/common/projections/service_nodes.ts +++ b/x-pack/plugins/apm/server/projections/service_nodes.ts @@ -8,9 +8,8 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { SERVICE_NODE_NAME } from '../elasticsearch_fieldnames'; +import { SERVICE_NODE_NAME } from '../../common/elasticsearch_fieldnames'; import { mergeProjection } from './util/merge_projection'; import { getMetricsProjection } from './metrics'; diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts new file mode 100644 index 0000000000000..18fa79f31d6f3 --- /dev/null +++ b/x-pack/plugins/apm/server/projections/services.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Setup, + SetupUIFilters, + SetupTimeRange, +} from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; + +export function getServicesProjection({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES } = setup; + + return { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [{ range: rangeFilter(start, end) }, ...uiFiltersES], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + }, + }, + }, + }, + }; +} diff --git a/x-pack/plugins/apm/common/projections/transaction_groups.ts b/x-pack/plugins/apm/server/projections/transaction_groups.ts similarity index 86% rename from x-pack/plugins/apm/common/projections/transaction_groups.ts rename to x-pack/plugins/apm/server/projections/transaction_groups.ts index 1708d89aad4ec..8aa085cccf82a 100644 --- a/x-pack/plugins/apm/common/projections/transaction_groups.ts +++ b/x-pack/plugins/apm/server/projections/transaction_groups.ts @@ -8,10 +8,11 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { TRANSACTION_NAME, PARENT_ID } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { + TRANSACTION_NAME, + PARENT_ID, +} from '../../common/elasticsearch_fieldnames'; import { Options } from '../../server/lib/transaction_groups/fetcher'; import { getTransactionsProjection } from './transactions'; import { mergeProjection } from './util/merge_projection'; diff --git a/x-pack/plugins/apm/common/projections/transactions.ts b/x-pack/plugins/apm/server/projections/transactions.ts similarity index 76% rename from x-pack/plugins/apm/common/projections/transactions.ts rename to x-pack/plugins/apm/server/projections/transactions.ts index b6cd73ca9aaad..f428a76a8b0cb 100644 --- a/x-pack/plugins/apm/common/projections/transactions.ts +++ b/x-pack/plugins/apm/server/projections/transactions.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, TRANSACTION_TYPE, - PROCESSOR_EVENT, TRANSACTION_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getTransactionsProjection({ setup, @@ -30,7 +28,7 @@ export function getTransactionsProjection({ transactionName?: string; transactionType?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -45,7 +43,6 @@ export function getTransactionsProjection({ const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, ...transactionNameFilter, ...transactionTypeFilter, ...serviceNameFilter, @@ -54,7 +51,9 @@ export function getTransactionsProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts similarity index 56% rename from x-pack/plugins/apm/common/projections/typings.ts rename to x-pack/plugins/apm/server/projections/typings.ts index 693795b09e1d0..77a5beaf54605 100644 --- a/x-pack/plugins/apm/common/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchRequest, ESSearchBody } from '../../typings/elasticsearch'; +import { ESSearchBody } from '../../typings/elasticsearch'; import { AggregationOptionsByType, AggregationInputMap, } from '../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; -export type Projection = Omit & { +export type Projection = Omit & { body: Omit & { aggs?: { [key: string]: { @@ -20,14 +21,3 @@ export type Projection = Omit & { }; }; }; - -export enum PROJECTION { - SERVICES = 'services', - TRANSACTION_GROUPS = 'transactionGroups', - TRACES = 'traces', - TRANSACTIONS = 'transactions', - METRICS = 'metrics', - ERROR_GROUPS = 'errorGroups', - SERVICE_NODES = 'serviceNodes', - RUM_OVERVIEW = 'rumOverview', -} diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts similarity index 73% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts index 33727fcb9c735..aa02c8898d218 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts @@ -10,10 +10,19 @@ describe('mergeProjection', () => { it('overrides arrays', () => { expect( mergeProjection( - { body: { query: { bool: { must: [{ terms: ['a'] }] } } } }, - { body: { query: { bool: { must: [{ term: 'b' }] } } } } + { + apm: { events: [] }, + body: { query: { bool: { must: [{ terms: ['a'] }] } } }, + }, + { + apm: { events: [] }, + body: { query: { bool: { must: [{ term: 'b' }] } } }, + } ) ).toEqual({ + apm: { + events: [], + }, body: { query: { bool: { @@ -32,8 +41,11 @@ describe('mergeProjection', () => { const termsAgg = { terms: { field: 'bar' } }; expect( mergeProjection( - { body: { query: {}, aggs: { foo: termsAgg } } }, + { apm: { events: [] }, body: { query: {}, aggs: { foo: termsAgg } } }, { + apm: { + events: [], + }, body: { aggs: { foo: { ...termsAgg, aggs: { bar: { terms: { field: 'baz' } } } }, @@ -42,6 +54,9 @@ describe('mergeProjection', () => { } ) ).toEqual({ + apm: { + events: [], + }, body: { query: {}, aggs: { diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts similarity index 82% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index 9dc1c815bf169..ea7267dd337c2 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -6,15 +6,13 @@ import { mergeWith, isPlainObject, cloneDeep } from 'lodash'; import { DeepPartial } from 'utility-types'; import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; -import { - ESSearchRequest, - ESSearchBody, -} from '../../../../typings/elasticsearch'; +import { ESSearchBody } from '../../../../typings/elasticsearch'; import { Projection } from '../../typings'; +import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; type PlainObject = Record; -type SourceProjection = Omit, 'body'> & { +type SourceProjection = Omit, 'body'> & { body: Omit, 'aggs'> & { aggs?: AggregationInputMap; }; diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 218d47fcf9bb4..ac25f22751f2f 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -5,58 +5,38 @@ */ import * as t from 'io-ts'; -import { ErrorCode } from '../../../common/anomaly_detection'; -import { PromiseReturnType } from '../../../typings/common'; -import { InsufficientMLCapabilities } from '../../../../ml/server'; +import Boom from 'boom'; +import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createRoute } from '../create_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; -import { AnomalyDetectionError } from '../../lib/anomaly_detection/anomaly_detection_error'; - -type Jobs = PromiseReturnType; - -function getMlErrorCode(e: Error) { - // Missing privileges - if (e instanceof InsufficientMLCapabilities) { - return ErrorCode.MISSING_READ_PRIVILEGES; - } - - if (e instanceof AnomalyDetectionError) { - return e.code; - } - - // unexpected error - return ErrorCode.UNEXPECTED; -} // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ method: 'GET', path: '/api/apm/settings/anomaly-detection', + options: { + tags: ['access:apm', 'access:ml:canGetJobs'], + }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - try { - const [jobs, legacyJobs] = await Promise.all([ - getAnomalyDetectionJobs(setup, context.logger), - hasLegacyJobs(setup), - ]); - return { - jobs, - hasLegacyJobs: legacyJobs, - }; - } catch (e) { - const mlErrorCode = getMlErrorCode(e); - context.logger.warn(`Error while retrieving ML jobs: "${e.message}"`); - return { - jobs: [] as Jobs, - hasLegacyJobs: false, - errorCode: mlErrorCode, - }; + const license = context.licensing.license; + if (!license.isActive || !license.hasAtLeast('platinum')) { + throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } + + const [jobs, legacyJobs] = await Promise.all([ + getAnomalyDetectionJobs(setup, context.logger), + hasLegacyJobs(setup), + ]); + return { + jobs, + hasLegacyJobs: legacyJobs, + }; }, })); @@ -65,7 +45,7 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ method: 'POST', path: '/api/apm/settings/anomaly-detection/jobs', options: { - tags: ['access:apm', 'access:apm_write'], + tags: ['access:apm', 'access:apm_write', 'access:ml:canCreateJob'], }, params: { body: t.type({ @@ -76,15 +56,12 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ const { environments } = context.params.body; const setup = await setupRequest(context, request); - try { - await createAnomalyDetectionJobs(setup, environments, context.logger); - } catch (e) { - const mlErrorCode = getMlErrorCode(e); - context.logger.warn(`Error while creating ML job: "${e.message}"`); - return { - errorCode: mlErrorCode, - }; + const license = context.licensing.license; + if (!license.isActive || !license.hasAtLeast('platinum')) { + throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } + + await createAnomalyDetectionJobs(setup, environments, context.logger); }, })); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index b1815e88d2917..c95719da881ea 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -45,7 +45,12 @@ export interface Route< method?: TMethod; params?: TParams; options?: { - tags: Array<'access:apm' | 'access:apm_write'>; + tags: Array< + | 'access:apm' + | 'access:apm_write' + | 'access:ml:canGetJobs' + | 'access:ml:canCreateJob' + >; }; handler: (kibanaContext: { context: APMRequestHandlerContext>; diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index a47d72751dfc4..864f5033c9d62 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -13,23 +13,23 @@ import { SetupTimeRange, } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; -import { Projection } from '../../common/projections/typings'; +import { Projection } from '../projections/typings'; import { localUIFilterNames, LocalUIFilterName, } from '../lib/ui_filters/local_ui_filters/config'; import { getUiFiltersES } from '../lib/helpers/convert_ui_filters/get_ui_filters_es'; import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; -import { getServicesProjection } from '../../common/projections/services'; -import { getTransactionGroupsProjection } from '../../common/projections/transaction_groups'; -import { getMetricsProjection } from '../../common/projections/metrics'; -import { getErrorGroupsProjection } from '../../common/projections/errors'; -import { getTransactionsProjection } from '../../common/projections/transactions'; +import { getServicesProjection } from '../projections/services'; +import { getTransactionGroupsProjection } from '../projections/transaction_groups'; +import { getMetricsProjection } from '../projections/metrics'; +import { getErrorGroupsProjection } from '../projections/errors'; +import { getTransactionsProjection } from '../projections/transactions'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { jsonRt } from '../../common/runtime_types/json_rt'; -import { getServiceNodesProjection } from '../../common/projections/service_nodes'; -import { getRumOverviewProjection } from '../../common/projections/rum_overview'; +import { getServiceNodesProjection } from '../projections/service_nodes'; +import { getRumOverviewProjection } from '../projections/rum_overview'; export const uiFiltersEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/ui_filters/environments', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index a090f09a76ea2..23fbc912d739a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -7,7 +7,6 @@ import squel from 'squel'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index 5ac91bec849c2..2e053f9084296 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -6,7 +6,6 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/__snapshots__/advanced_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/__snapshots__/advanced_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/advanced_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/advanced_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/advanced_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__stories__/advanced_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/dropdown_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/dropdown_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/__snapshots__/time_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/time_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/time_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/time_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__stories__/time_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/__snapshots__/metric.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/__snapshots__/metric.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/metric.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/metric/component/__stories__/metric.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js index 02c86afd7182b..5684c8c4602b5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js @@ -75,6 +75,7 @@ export const shape = () => ({ domNode.removeChild(oldShape); } + domNode.style.lineHeight = 0; domNode.appendChild(shapeSvg); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/date_format.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/date_format.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/date_format.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/date_format.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/__snapshots__/number_format.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/number_format.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__stories__/number_format.stories.tsx diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 9b1d60f38eb5e..03d6ade7bea69 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -913,6 +913,13 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.toolbar.workpadManagerCloseButtonLabel', { defaultMessage: 'Close', }), + getErrorMessage: (message: string) => + i18n.translate('xpack.canvas.toolbar.errorMessage', { + defaultMessage: 'TOOLBAR ERROR: {message}', + values: { + message, + }, + }), }, ToolbarTray: { getCloseTrayAriaLabel: () => diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx deleted file mode 100644 index 1cd7562b59c47..0000000000000 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx +++ /dev/null @@ -1,110 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-console */ - -/* - This Provider is temporary. See https://github.com/elastic/kibana/pull/69357 -*/ - -import React, { FC } from 'react'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; -import thunkMiddleware from 'redux-thunk'; -import { Provider as ReduxProvider } from 'react-redux'; - -// @ts-expect-error untyped local -import { appReady } from '../../../../public/state/middleware/app_ready'; -// @ts-expect-error untyped local -import { resolvedArgs } from '../../../../public/state/middleware/resolved_args'; - -// @ts-expect-error untyped local -import { getRootReducer } from '../../../../public/state/reducers'; - -// @ts-expect-error Untyped local -import { getDefaultWorkpad } from '../../../../public/state/defaults'; -import { State, AssetType } from '../../../../types'; - -export const AIRPLANE: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'airplane', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -export const MARKER: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'marker', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -export const state: State = { - app: { - basePath: '/', - ready: true, - serverFunctions: [], - }, - assets: { - AIRPLANE, - MARKER, - }, - transient: { - canUserWrite: true, - zoomScale: 1, - elementStats: { - total: 0, - ready: 0, - pending: 0, - error: 0, - }, - inFlight: false, - fullScreen: false, - selectedTopLevelNodes: [], - resolvedArgs: {}, - refresh: { - interval: 0, - }, - autoplay: { - enabled: false, - interval: 10000, - }, - }, - persistent: { - schemaVersion: 2, - workpad: getDefaultWorkpad(), - }, -}; - -// @ts-expect-error untyped local -import { elementsRegistry } from '../../../lib/elements_registry'; -import { image } from '../../../../canvas_plugin_src/elements/image'; -elementsRegistry.register(image); - -export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( - action -) => { - const previousState = store.getState(); - const returnValue = dispatch(action); - const newState = store.getState(); - - console.group(action.type || '(thunk)'); - console.log('Previous State', previousState); - console.log('New State', newState); - console.groupEnd(); - - return returnValue; -}; - -export const Provider: FC = ({ children }) => { - const middleware = applyMiddleware(thunkMiddleware); - const reducer = getRootReducer(state); - const store = createStore(reducer, state, middleware); - store.dispatch = patchDispatch(store, store.dispatch); - - return {children}; -}; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx new file mode 100644 index 0000000000000..0b99bbce50288 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { reduxDecorator, getAddonPanelParameters } from '../../../../storybook'; +import { Asset, AssetComponent } from '../'; +import { AIRPLANE, MARKER, assets } from './assets'; + +storiesOf('components/Assets/Asset', module) + .addDecorator((story) =>
{story()}
) + .addDecorator(reduxDecorator({ assets })) + .addParameters(getAddonPanelParameters()) + .add('redux: Asset', () => { + return ; + }) + .add('airplane', () => ( + + )) + .add('marker', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx similarity index 50% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx rename to x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx index 1434ef60cf0d8..673c66734b39a 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx @@ -4,35 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; -import React from 'react'; +import { reduxDecorator, getAddonPanelParameters } from '../../../../storybook'; import { AssetManager, AssetManagerComponent } from '../'; - -import { Provider, AIRPLANE, MARKER } from './provider'; +import { assets } from './assets'; storiesOf('components/Assets/AssetManager', module) - .add('redux: AssetManager', () => ( - - - - )) + .addDecorator(reduxDecorator({ assets })) + .addParameters(getAddonPanelParameters()) + .add('redux: AssetManager', () => ) .add('no assets', () => ( - - - + )) .add('two assets', () => ( - - - + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts new file mode 100644 index 0000000000000..3b5576667ed26 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AssetType } from '../../../../types'; + +export const AIRPLANE: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'airplane', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const MARKER: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'marker', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const assets = [AIRPLANE, MARKER]; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx index a04d37cf7f9fc..ed000741bc542 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx @@ -17,7 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useNotifyService } from '../../services'; import { ConfirmModal } from '../confirm_modal'; import { Clipboard } from '../clipboard'; @@ -38,11 +38,10 @@ interface Props { } export const Asset: FC = ({ asset, onCreate, onDelete }) => { - const { services } = useKibana(); + const { success } = useNotifyService(); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); - const onCopy = (result: boolean) => - result && services.canvas.notify.success(`Copied '${asset.id}' to clipboard`); + const onCopy = (result: boolean) => result && success(`Copied '${asset.id}' to clipboard`); const confirmModal = ( void; + onSelect: (id: string, embeddableType: string) => void; + availableEmbeddables: string[]; +} + +export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { + const services = useServices(); + const { embeddables, platform } = services; + const { getEmbeddableFactories } = embeddables; + const { getSavedObjects, getUISettings } = platform; + + const onAddPanel = (id: string, savedObjectType: string, name: string) => { + const embeddableFactories = getEmbeddableFactories(); + + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories).find((embeddableFactory) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); + + const foundEmbeddableType = found ? found.type : 'unknown'; + + onSelect(id, foundEmbeddableType); + }; + + const embeddableFactories = getEmbeddableFactories(); + + const availableSavedObjects = Array.from(embeddableFactories) + .filter((factory) => { + return availableEmbeddables.includes(factory.type); + }) + .map((factory) => factory.savedObjectMetaData) + .filter>(function ( + maybeSavedObjectMetaData + ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { + return maybeSavedObjectMetaData !== undefined; + }); + + return ( + + + +

{strings.getTitleText()}

+
+
+ + + +
+ ); +}; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 0b5bd8adf8cb9..8c84e3d7a85d8 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -4,75 +4,107 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; -import { - SavedObjectFinderUi, - SavedObjectMetaData, -} from '../../../../../../src/plugins/saved_objects/public/'; -import { ComponentStrings } from '../../../i18n'; -import { useServices } from '../../services'; - -const { AddEmbeddableFlyout: strings } = ComponentStrings; - -export interface Props { - onClose: () => void; - onSelect: (id: string, embeddableType: string) => void; - availableEmbeddables: string[]; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { compose } from 'recompose'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component'; +// @ts-expect-error untyped local +import { addElement } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; + +const allowedEmbeddables = { + [EmbeddableTypes.map]: (id: string) => { + return `savedMap id="${id}" | render`; + }, + [EmbeddableTypes.lens]: (id: string) => { + return `savedLens id="${id}" | render`; + }, + [EmbeddableTypes.visualization]: (id: string) => { + return `savedVisualization id="${id}" | render`; + }, + /* + [EmbeddableTypes.search]: (id: string) => { + return `filters | savedSearch id="${id}" | render`; + },*/ +}; + +interface StateProps { + pageId: string; +} + +interface DispatchProps { + addEmbeddable: (pageId: string, partialElement: { expression: string }) => void; } -export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { - const services = useServices(); - const { embeddables, platform } = services; - const { getEmbeddableFactories } = embeddables; - const { getSavedObjects, getUISettings } = platform; +// FIX: Missing state type +const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); - const onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = getEmbeddableFactories(); +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => + dispatch(addElement(pageId, partialElement)), +}); - // Find the embeddable type from the saved object type - const found = Array.from(embeddableFactories).find((embeddableFactory) => { - return Boolean( - embeddableFactory.savedObjectMetaData && - embeddableFactory.savedObjectMetaData.type === savedObjectType - ); - }); +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { pageId, ...remainingStateProps } = stateProps; + const { addEmbeddable } = dispatchProps; - const foundEmbeddableType = found ? found.type : 'unknown'; + return { + ...remainingStateProps, + ...ownProps, + onSelect: (id: string, type: string): void => { + const partialElement = { + expression: `markdown "Could not find embeddable for type ${type}" | render`, + }; + if (allowedEmbeddables[type]) { + partialElement.expression = allowedEmbeddables[type](id); + } - onSelect(id, foundEmbeddableType); + addEmbeddable(pageId, partialElement); + ownProps.onClose(); + }, }; - - const embeddableFactories = getEmbeddableFactories(); - - const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return availableEmbeddables.includes(factory.type); - }) - .map((factory) => factory.savedObjectMetaData) - .filter>(function ( - maybeSavedObjectMetaData - ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { - return maybeSavedObjectMetaData !== undefined; - }); - - return ( - - - -

{strings.getTitleText()}

-
-
- - - -
- ); }; + +export class EmbeddableFlyoutPortal extends React.Component { + el?: HTMLElement; + + constructor(props: ComponentProps) { + super(props); + + this.el = document.createElement('div'); + } + componentDidMount() { + const body = document.querySelector('body'); + if (body && this.el) { + body.appendChild(this.el); + } + } + + componentWillUnmount() { + const body = document.querySelector('body'); + + if (body && this.el) { + body.removeChild(this.el); + } + } + + render() { + if (this.el) { + return ReactDOM.createPortal( + , + this.el + ); + } + } +} + +export const AddEmbeddablePanel = compose void }>( + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts new file mode 100644 index 0000000000000..a7fac10b0c02d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EmbeddableFlyoutPortal, AddEmbeddablePanel } from './flyout'; +export { + AddEmbeddableFlyout as AddEmbeddableFlyoutComponent, + Props as AddEmbeddableFlyoutComponentProps, +} from './flyout.component'; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx deleted file mode 100644 index 62a073daf4c59..0000000000000 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ /dev/null @@ -1,113 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { compose } from 'recompose'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { AddEmbeddableFlyout, Props } from './flyout'; -// @ts-expect-error untyped local -import { addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; - -const allowedEmbeddables = { - [EmbeddableTypes.map]: (id: string) => { - return `savedMap id="${id}" | render`; - }, - [EmbeddableTypes.lens]: (id: string) => { - return `savedLens id="${id}" | render`; - }, - [EmbeddableTypes.visualization]: (id: string) => { - return `savedVisualization id="${id}" | render`; - }, - /* - [EmbeddableTypes.search]: (id: string) => { - return `filters | savedSearch id="${id}" | render`; - },*/ -}; - -interface StateProps { - pageId: string; -} - -interface DispatchProps { - addEmbeddable: (pageId: string, partialElement: { expression: string }) => void; -} - -// FIX: Missing state type -const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => - dispatch(addElement(pageId, partialElement)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: Props -): Props => { - const { pageId, ...remainingStateProps } = stateProps; - const { addEmbeddable } = dispatchProps; - - return { - ...remainingStateProps, - ...ownProps, - onSelect: (id: string, type: string): void => { - const partialElement = { - expression: `markdown "Could not find embeddable for type ${type}" | render`, - }; - if (allowedEmbeddables[type]) { - partialElement.expression = allowedEmbeddables[type](id); - } - - addEmbeddable(pageId, partialElement); - ownProps.onClose(); - }, - }; -}; - -export class EmbeddableFlyoutPortal extends React.Component { - el?: HTMLElement; - - constructor(props: Props) { - super(props); - - this.el = document.createElement('div'); - } - componentDidMount() { - const body = document.querySelector('body'); - if (body && this.el) { - body.appendChild(this.el); - } - } - - componentWillUnmount() { - const body = document.querySelector('body'); - - if (body && this.el) { - body.removeChild(this.el); - } - } - - render() { - if (this.el) { - return ReactDOM.createPortal( - , - this.el - ); - } - } -} - -export const AddEmbeddablePanel = compose void }>( - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(EmbeddableFlyoutPortal); diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.stories.storyshot b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.stories.storyshot rename to x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.stories.tsx b/x-pack/plugins/canvas/public/components/expression_input/__stories__/expression_input.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.stories.tsx rename to x-pack/plugins/canvas/public/components/expression_input/__stories__/expression_input.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.stories.storyshot rename to x-pack/plugins/canvas/public/components/item_grid/__stories__/__snapshots__/item_grid.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.stories.tsx b/x-pack/plugins/canvas/public/components/item_grid/__stories__/item_grid.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/item_grid/__examples__/item_grid.stories.tsx rename to x-pack/plugins/canvas/public/components/item_grid/__stories__/item_grid.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot rename to x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/keyboard_shortcuts_doc.stories.tsx b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/keyboard_shortcuts_doc.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/keyboard_shortcuts_doc.stories.tsx rename to x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/keyboard_shortcuts_doc.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/navbar/navbar.scss b/x-pack/plugins/canvas/public/components/navbar/navbar.scss deleted file mode 100644 index 7b490822763d2..0000000000000 --- a/x-pack/plugins/canvas/public/components/navbar/navbar.scss +++ /dev/null @@ -1,7 +0,0 @@ -.canvasNavbar { - width: 100%; - height: $euiSizeXL * 2; - background-color: darken($euiColorLightestShade, 5%); - position: relative; - z-index: 200; -} diff --git a/x-pack/plugins/canvas/public/components/page_manager/index.ts b/x-pack/plugins/canvas/public/components/page_manager/index.ts index d19540cd6a687..abe7a4a3a5bb1 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/index.ts +++ b/x-pack/plugins/canvas/public/components/page_manager/index.ts @@ -4,28 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { PageManager as Component } from './page_manager'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - pages: getPages(state), - selectedPage: getSelectedPage(state), - workpadId: getWorkpad(state).id, - workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onAddPage: () => dispatch(pageActions.addPage()), - onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), - onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), -}); - -export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); +export { PageManager } from './page_manager'; +export { PageManager as PageManagerComponent } from './page_manager.component'; diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx rename to x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts new file mode 100644 index 0000000000000..a92f7c6b4c352 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { PageManager as Component } from './page_manager.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + pages: getPages(state), + selectedPage: getSelectedPage(state), + workpadId: getWorkpad(state).id, + workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onAddPage: () => dispatch(pageActions.addPage()), + onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), + onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), +}); + +export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_preview/index.ts b/x-pack/plugins/canvas/public/components/page_preview/index.ts index 25d3254595d2e..22e3861eb9652 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/index.ts +++ b/x-pack/plugins/canvas/public/components/page_preview/index.ts @@ -4,21 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { isWriteable } from '../../state/selectors/workpad'; -import { PagePreview as Component } from './page_preview'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), -}); - -export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); +export { PagePreview } from './page_preview'; +export { PagePreview as PagePreviewComponent } from './page_preview.component'; diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx rename to x-pack/plugins/canvas/public/components/page_preview/page_preview.component.tsx diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts b/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts new file mode 100644 index 0000000000000..8768a2fc169ef --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { isWriteable } from '../../state/selectors/workpad'; +import { PagePreview as Component } from './page_preview.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), +}); + +export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot rename to x-pack/plugins/canvas/public/components/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx b/x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx rename to x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx index 7939c1d04631a..c5fe7074fea0b 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx +++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx @@ -5,7 +5,6 @@ */ import React, { useState, useEffect, useRef, FC, useCallback } from 'react'; -import { useDebounce } from 'react-use'; import { useNotifyService } from '../../services'; import { RenderToDom } from '../render_to_dom'; @@ -73,7 +72,7 @@ export const RenderWithFn: FC = ({ firstRender.current = true; }, [domNode]); - useDebounce(() => handlers.current.resize({ height, width }), 150, [height, width]); + useEffect(() => handlers.current.resize({ height, width }), [height, width]); useEffect( () => () => { diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_controls.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/element_grid.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_controls.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_controls.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx similarity index 96% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx index 4941d8cb2efa7..a811a296f2e7b 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { SavedElementsModal } from '../saved_elements_modal'; +import { SavedElementsModal } from '../saved_elements_modal.component'; import { testCustomElements } from './fixtures/test_elements'; import { CustomElement } from '../../../../types'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts index da2955c146193..46faf8d14f9b5 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -4,130 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { compose, withState } from 'recompose'; -import { camelCase } from 'lodash'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import * as customElementService from '../../lib/custom_element_service'; -import { withServices, WithServicesProps } from '../../services'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../state/actions/transient'; -// @ts-expect-error untyped local -import { insertNodes } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { SavedElementsModal as Component, Props as ComponentProps } from './saved_elements_modal'; -import { State, PositionedElement, CustomElement } from '../../../types'; - -const customElementAdded = 'elements-custom-added'; - -interface OwnProps { - onClose: () => void; -} - -interface OwnPropsWithState extends OwnProps { - customElements: CustomElement[]; - setCustomElements: (customElements: CustomElement[]) => void; - search: string; - setSearch: (search: string) => void; -} - -interface DispatchProps { - selectToplevelNodes: (nodes: PositionedElement[]) => void; - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; -} - -interface StateProps { - pageId: string; -} - -const mapStateToProps = (state: State): StateProps => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes - .filter((e: PositionedElement): boolean => !e.position.parent) - .map((e: PositionedElement): string => e.id) - ) - ), - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: OwnPropsWithState & WithServicesProps -): ComponentProps => { - const { pageId } = stateProps; - const { onClose, search, setCustomElements } = ownProps; - - const findCustomElements = async () => { - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - }; - - return { - ...ownProps, - // add custom element to the page - addCustomElement: (customElement: CustomElement) => { - const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) - dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - onClose(); - trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); - }, - // custom element search - findCustomElements: async (text?: string) => { - try { - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't find custom elements`, - }); - } - }, - // remove custom element - removeCustomElement: async (id: string) => { - try { - await customElementService.remove(id); - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't delete custom elements`, - }); - } - }, - // update custom element - updateCustomElement: async (id: string, name: string, description: string, image: string) => { - try { - await customElementService.update(id, { - name: camelCase(name), - displayName: name, - image, - help: description, - }); - await findCustomElements(); - } catch (err) { - ownProps.services.notify.error(err, { - title: `Couldn't update custom elements`, - }); - } - }, - }; -}; - -export const SavedElementsModal = compose( - withServices, - withState('search', 'setSearch', ''), - withState('customElements', 'setCustomElements', []), - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(Component); +export { SavedElementsModal } from './saved_elements_modal'; +export { + SavedElementsModal as SavedElementsModalComponent, + Props as SavedElementsModalComponentProps, +} from './saved_elements_modal.component'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx rename to x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts new file mode 100644 index 0000000000000..a5c5a2e0adce9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { compose, withState } from 'recompose'; +import { camelCase } from 'lodash'; +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import * as customElementService from '../../lib/custom_element_service'; +import { withServices, WithServicesProps } from '../../services'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../state/actions/transient'; +// @ts-expect-error untyped local +import { insertNodes } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { + SavedElementsModal as Component, + Props as ComponentProps, +} from './saved_elements_modal.component'; +import { State, PositionedElement, CustomElement } from '../../../types'; + +const customElementAdded = 'elements-custom-added'; + +interface OwnProps { + onClose: () => void; +} + +interface OwnPropsWithState extends OwnProps { + customElements: CustomElement[]; + setCustomElements: (customElements: CustomElement[]) => void; + search: string; + setSearch: (search: string) => void; +} + +interface DispatchProps { + selectToplevelNodes: (nodes: PositionedElement[]) => void; + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; +} + +interface StateProps { + pageId: string; +} + +const mapStateToProps = (state: State): StateProps => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes + .filter((e: PositionedElement): boolean => !e.position.parent) + .map((e: PositionedElement): string => e.id) + ) + ), + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnPropsWithState & WithServicesProps +): ComponentProps => { + const { pageId } = stateProps; + const { onClose, search, setCustomElements } = ownProps; + + const findCustomElements = async () => { + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + }; + + return { + ...ownProps, + // add custom element to the page + addCustomElement: (customElement: CustomElement) => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + onClose(); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); + }, + // custom element search + findCustomElements: async (text?: string) => { + try { + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't find custom elements`, + }); + } + }, + // remove custom element + removeCustomElement: async (id: string) => { + try { + await customElementService.remove(id); + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't delete custom elements`, + }); + } + }, + // update custom element + updateCustomElement: async (id: string, name: string, description: string, image: string) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + await findCustomElements(); + } catch (err) { + ownProps.services.notify.error(err, { + title: `Couldn't update custom elements`, + }); + } + }, + }; +}; + +export const SavedElementsModal = compose( + withServices, + withState('search', 'setSearch', ''), + withState('customElements', 'setCustomElements', []), + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/shape_picker/__examples__/__snapshots__/shape_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_picker/__stories__/__snapshots__/shape_picker.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker/__examples__/__snapshots__/shape_picker.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_picker/__stories__/__snapshots__/shape_picker.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_picker/__examples__/shape_picker.stories.tsx b/x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker/__examples__/shape_picker.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_picker/__stories__/shape_picker.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/__snapshots__/shape_picker_popover.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/shape_picker_popover.stories.tsx b/x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_picker_popover/__examples__/shape_picker_popover.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_picker_popover/__stories__/shape_picker_popover.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/shape_preview/__examples__/__snapshots__/shape_preview.stories.storyshot b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_preview/__examples__/__snapshots__/shape_preview.stories.storyshot rename to x-pack/plugins/canvas/public/components/shape_preview/__stories__/__snapshots__/shape_preview.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/shape_preview/__examples__/shape_preview.stories.tsx b/x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/shape_preview/__examples__/shape_preview.stories.tsx rename to x-pack/plugins/canvas/public/components/shape_preview/__stories__/shape_preview.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/group_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/group_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/multi_element_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/__snapshots__/multi_element_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/group_settings.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar/__stories__/group_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/group_settings.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/group_settings.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar/__examples__/multi_element_settings.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar/__stories__/multi_element_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar/__examples__/multi_element_settings.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar/__stories__/multi_element_settings.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx new file mode 100644 index 0000000000000..e3f4e00f4de01 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiTabbedContent } from '@elastic/eui'; +// @ts-expect-error unconverted component +import { Datasource } from '../../datasource'; +// @ts-expect-error unconverted component +import { FunctionFormList } from '../../function_form_list'; +import { PositionedElement } from '../../../../types'; +import { ComponentStrings } from '../../../../i18n'; + +interface Props { + /** + * a Canvas element used to populate config forms + */ + element: PositionedElement; +} + +const { ElementSettings: strings } = ComponentStrings; + +export const ElementSettings: FunctionComponent = ({ element }) => { + const tabs = [ + { + id: 'edit', + name: strings.getDisplayTabLabel(), + content: ( +
+
+ +
+
+ ), + }, + { + id: 'data', + name: strings.getDataTabLabel(), + content: ( +
+ +
+ ), + }, + ]; + + return ; +}; + +ElementSettings.propTypes = { + element: PropTypes.object, +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index e3f4e00f4de01..ba7e31a25daba 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -3,53 +3,32 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import React, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -import { EuiTabbedContent } from '@elastic/eui'; -// @ts-expect-error unconverted component -import { Datasource } from '../../datasource'; -// @ts-expect-error unconverted component -import { FunctionFormList } from '../../function_form_list'; -import { PositionedElement } from '../../../../types'; -import { ComponentStrings } from '../../../../i18n'; +import React from 'react'; +import { connect } from 'react-redux'; +import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; +import { ElementSettings as Component } from './element_settings.component'; +import { State, PositionedElement } from '../../../../types'; interface Props { - /** - * a Canvas element used to populate config forms - */ - element: PositionedElement; + selectedElementId: string; } -const { ElementSettings: strings } = ComponentStrings; +const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ + element: getElementById(state, selectedElementId, getSelectedPage(state)), +}); + +interface StateProps { + element: PositionedElement | undefined; +} -export const ElementSettings: FunctionComponent = ({ element }) => { - const tabs = [ - { - id: 'edit', - name: strings.getDisplayTabLabel(), - content: ( -
-
- -
-
- ), - }, - { - id: 'data', - name: strings.getDataTabLabel(), - content: ( -
- -
- ), - }, - ]; +const renderIfElement: React.FunctionComponent = (props) => { + if (props.element) { + return ; + } - return ; + return null; }; -ElementSettings.propTypes = { - element: PropTypes.object, -}; +export const ElementSettings = connect(mapStateToProps)( + renderIfElement +); diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.ts new file mode 100644 index 0000000000000..68b90f232fb8b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElementSettings } from './element_settings'; +export { ElementSettings as ElementSettingsComponent } from './element_settings.component'; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx deleted file mode 100644 index b8d5882234899..0000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx +++ /dev/null @@ -1,34 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { connect } from 'react-redux'; -import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; -import { ElementSettings as Component } from './element_settings'; -import { State, PositionedElement } from '../../../../types'; - -interface Props { - selectedElementId: string; -} - -const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ - element: getElementById(state, selectedElementId, getSelectedPage(state)), -}); - -interface StateProps { - element: PositionedElement | undefined; -} - -const renderIfElement: React.FunctionComponent = (props) => { - if (props.element) { - return ; - } - - return null; -}; - -export const ElementSettings = connect(mapStateToProps)( - renderIfElement -); diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot rename to x-pack/plugins/canvas/public/components/sidebar_header/__stories__/__snapshots__/sidebar_header.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx rename to x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.stories.storyshot b/x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.stories.storyshot rename to x-pack/plugins/canvas/public/components/tag/__stories__/__snapshots__/tag.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tag/__examples__/tag.stories.tsx b/x-pack/plugins/canvas/public/components/tag/__stories__/tag.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tag/__examples__/tag.stories.tsx rename to x-pack/plugins/canvas/public/components/tag/__stories__/tag.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.stories.storyshot b/x-pack/plugins/canvas/public/components/tag_list/__stories__/__snapshots__/tag_list.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.stories.storyshot rename to x-pack/plugins/canvas/public/components/tag_list/__stories__/__snapshots__/tag_list.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tag_list/__examples__/tag_list.stories.tsx b/x-pack/plugins/canvas/public/components/tag_list/__stories__/tag_list.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tag_list/__examples__/tag_list.stories.tsx rename to x-pack/plugins/canvas/public/components/tag_list/__stories__/tag_list.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/__snapshots__/tool_tip_shortcut.stories.storyshot b/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/__snapshots__/tool_tip_shortcut.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/__snapshots__/tool_tip_shortcut.stories.storyshot rename to x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/__snapshots__/tool_tip_shortcut.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/tool_tip_shortcut.stories.tsx b/x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/tool_tip_shortcut.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/tool_tip_shortcut/__examples__/tool_tip_shortcut.stories.tsx rename to x-pack/plugins/canvas/public/components/tool_tip_shortcut/__stories__/tool_tip_shortcut.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx deleted file mode 100644 index 5907c932ddabb..0000000000000 --- a/x-pack/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - TODO: uncomment and fix this test to address storybook errors as a result of nested component dependencies - https://github.com/elastic/kibana/issues/58289 - */ - -/* -import { action } from '@storybook/addon-actions'; -import { storiesOf } from '@storybook/react'; -import React from 'react'; -import { Toolbar } from '../toolbar'; - -storiesOf('components/Toolbar', module) - .addDecorator(story => ( -
- {story()} -
- )) - .add('with null metric', () => ( - - )); -*/ diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/__snapshots__/toolbar.stories.storyshot b/x-pack/plugins/canvas/public/components/toolbar/__stories__/__snapshots__/toolbar.stories.storyshot new file mode 100644 index 0000000000000..eec0de3c784f1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/__snapshots__/toolbar.stories.storyshot @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/Toolbar element selected 1`] = ` +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+`; + +exports[`Storyshots components/Toolbar no element selected 1`] = ` +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx new file mode 100644 index 0000000000000..bd6ad7c8dc499 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { Toolbar } from '../toolbar.component'; + +// @ts-expect-error untyped local +import { getDefaultElement } from '../../../state/defaults'; + +storiesOf('components/Toolbar', module) + .add('no element selected', () => ( + + )) + .add('element selected', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/toolbar/index.js b/x-pack/plugins/canvas/public/components/toolbar/index.js deleted file mode 100644 index 16860063f8a45..0000000000000 --- a/x-pack/plugins/canvas/public/components/toolbar/index.js +++ /dev/null @@ -1,49 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { pure, compose, withState, getContext, withHandlers } from 'recompose'; -import { canUserWrite } from '../../state/selectors/app'; - -import { - getWorkpad, - getWorkpadName, - getSelectedPageIndex, - getSelectedElement, - isWriteable, -} from '../../state/selectors/workpad'; - -import { Toolbar as Component } from './toolbar'; - -const mapStateToProps = (state) => ({ - workpadName: getWorkpadName(state), - workpadId: getWorkpad(state).id, - totalPages: getWorkpad(state).pages.length, - selectedPageNumber: getSelectedPageIndex(state) + 1, - selectedElement: getSelectedElement(state), - isWriteable: isWriteable(state) && canUserWrite(state), -}); - -export const Toolbar = compose( - pure, - connect(mapStateToProps), - getContext({ - router: PropTypes.object, - }), - withHandlers({ - nextPage: (props) => () => { - const pageNumber = Math.min(props.selectedPageNumber + 1, props.totalPages); - props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); - }, - previousPage: (props) => () => { - const pageNumber = Math.max(1, props.selectedPageNumber - 1); - props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); - }, - }), - withState('tray', 'setTray', (props) => props.tray), - withState('showWorkpadManager', 'setShowWorkpadManager', false) -)(Component); diff --git a/x-pack/plugins/canvas/storybook/addons.js b/x-pack/plugins/canvas/public/components/toolbar/index.ts similarity index 66% rename from x-pack/plugins/canvas/storybook/addons.js rename to x-pack/plugins/canvas/public/components/toolbar/index.ts index 75bbe620c9e7b..dfa730307dafb 100644 --- a/x-pack/plugins/canvas/storybook/addons.js +++ b/x-pack/plugins/canvas/public/components/toolbar/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import '@storybook/addon-actions/register'; -import '@storybook/addon-knobs/register'; -import '@storybook/addon-console'; +export { Toolbar } from './toolbar'; +export { Toolbar as ToolbarComponent } from './toolbar.component'; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx similarity index 64% rename from x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx rename to x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index c5475b2559444..6905b3ed23d3f 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC, useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, @@ -16,72 +16,77 @@ import { EuiModalFooter, EuiButton, } from '@elastic/eui'; -import { CanvasElement } from '../../../types'; - -import { ComponentStrings } from '../../../i18n'; -// @ts-expect-error untyped local -import { Navbar } from '../navbar'; // @ts-expect-error untyped local import { WorkpadManager } from '../workpad_manager'; +import { RouterContext } from '../router'; import { PageManager } from '../page_manager'; // @ts-expect-error untyped local import { Expression } from '../expression'; import { Tray } from './tray'; +import { CanvasElement } from '../../../types'; +import { ComponentStrings } from '../../../i18n'; + const { Toolbar: strings } = ComponentStrings; -enum TrayType { - pageManager = 'pageManager', - expression = 'expression', -} +type TrayType = 'pageManager' | 'expression'; interface Props { - workpadName: string; isWriteable: boolean; - canUserWrite: boolean; - tray: TrayType | null; - setTray: (tray: TrayType | null) => void; - - previousPage: () => void; - nextPage: () => void; + selectedElement?: CanvasElement; selectedPageNumber: number; totalPages: number; - - selectedElement: CanvasElement; - - showWorkpadManager: boolean; - setShowWorkpadManager: (show: boolean) => void; + workpadId: string; + workpadName: string; } -export const Toolbar = (props: Props) => { - const { - selectedElement, - tray, - setTray, - previousPage, - nextPage, - selectedPageNumber, - workpadName, - totalPages, - showWorkpadManager, - setShowWorkpadManager, - isWriteable, - } = props; +export const Toolbar: FC = ({ + isWriteable, + selectedElement, + selectedPageNumber, + totalPages, + workpadId, + workpadName, +}) => { + const [activeTray, setActiveTray] = useState(null); + const [showWorkpadManager, setShowWorkpadManager] = useState(false); + const router = useContext(RouterContext); + + // While the tray doesn't get activated if the workpad isn't writeable, + // this effect will ensure that if the tray is open and the workpad + // changes its writeable state, the tray will close. + useEffect(() => { + if (!isWriteable && activeTray === 'expression') { + setActiveTray(null); + } + }, [isWriteable, activeTray]); - const elementIsSelected = Boolean(selectedElement); + if (!router) { + return
{strings.getErrorMessage('Router Undefined')}
; + } - const done = () => setTray(null); + const nextPage = () => { + const page = Math.min(selectedPageNumber + 1, totalPages); + router.navigateTo('loadWorkpad', { id: workpadId, page }); + }; - if (!isWriteable && tray === TrayType.expression) { - done(); - } + const previousPage = () => { + const page = Math.max(1, selectedPageNumber - 1); + router.navigateTo('loadWorkpad', { id: workpadId, page }); + }; - const showHideTray = (exp: TrayType) => { - if (tray && tray === exp) { - return done(); + const elementIsSelected = Boolean(selectedElement); + + const toggleTray = (tray: TrayType) => { + if (activeTray === tray) { + setActiveTray(null); + } else { + if (!isWriteable && tray === 'expression') { + return; + } + setActiveTray(tray); } - setTray(exp); }; const closeWorkpadManager = () => setShowWorkpadManager(false); @@ -102,13 +107,13 @@ export const Toolbar = (props: Props) => { const trays = { pageManager: , - expression: !elementIsSelected ? null : , + expression: !elementIsSelected ? null : setActiveTray(null)} />, }; return (
- {tray !== null && {trays[tray]}} - + {activeTray !== null && setActiveTray(null)}>{trays[activeTray]}} +
openWorkpadManager()}> @@ -126,7 +131,7 @@ export const Toolbar = (props: Props) => { /> - showHideTray(TrayType.pageManager)}> + toggleTray('pageManager')}> {strings.getPageButtonLabel(selectedPageNumber, totalPages)} @@ -145,7 +150,7 @@ export const Toolbar = (props: Props) => { showHideTray(TrayType.expression)} + onClick={() => toggleTray('expression')} data-test-subj="canvasExpressionEditorButton" > {strings.getEditorButtonLabel()} @@ -153,23 +158,17 @@ export const Toolbar = (props: Props) => { )} - - +
{showWorkpadManager && workpadManager}
); }; Toolbar.propTypes = { - workpadName: PropTypes.string, - tray: PropTypes.string, - setTray: PropTypes.func.isRequired, - nextPage: PropTypes.func.isRequired, - previousPage: PropTypes.func.isRequired, + isWriteable: PropTypes.bool.isRequired, + selectedElement: PropTypes.object, selectedPageNumber: PropTypes.number.isRequired, totalPages: PropTypes.number.isRequired, - selectedElement: PropTypes.object, - showWorkpadManager: PropTypes.bool.isRequired, - setShowWorkpadManager: PropTypes.func.isRequired, - isWriteable: PropTypes.bool.isRequired, + workpadId: PropTypes.string.isRequired, + workpadName: PropTypes.string.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss index 7303f43dd269f..41bc718dcfec1 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss @@ -24,3 +24,11 @@ padding: $euiSizeM; height: 100%; } + +.canvasToolbar__container { + width: 100%; + height: $euiSizeXL * 2; + background-color: darken($euiColorLightestShade, 5%); + position: relative; + z-index: 200; +} diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts b/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts new file mode 100644 index 0000000000000..f93b42cb442b8 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { canUserWrite } from '../../state/selectors/app'; + +import { + getWorkpad, + getWorkpadName, + getSelectedPageIndex, + getSelectedElement, + isWriteable, +} from '../../state/selectors/workpad'; + +import { Toolbar as ToolbarComponent } from './toolbar.component'; +import { State } from '../../../types'; + +export const Toolbar = connect((state: State) => ({ + workpadName: getWorkpadName(state), + workpadId: getWorkpad(state).id, + totalPages: getWorkpad(state).pages.length, + selectedPageNumber: getSelectedPageIndex(state) + 1, + selectedElement: getSelectedElement(state), + isWriteable: isWriteable(state) && canUserWrite(state), +}))(ToolbarComponent); diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts b/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts index 1343bc8d01e9a..18c45190cbd48 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Tray as Component } from './tray'; - -export const Tray = pure(Component); +export { Tray } from './tray'; diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx index 2c0b4e69c240b..0699d30833ecd 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode, Fragment, MouseEventHandler } from 'react'; +import React, { ReactNode, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; @@ -18,7 +18,7 @@ interface Props { export const Tray = ({ children, done }: Props) => { return ( - + <> { /> -
{children}
-
+ ); }; Tray.propTypes = { - children: PropTypes.node, - done: PropTypes.func, + children: PropTypes.node.isRequired, + done: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/delete_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/delete_var.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/edit_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/edit_var.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/edit_var.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/var_config.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/__snapshots__/var_config.stories.storyshot rename to x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/delete_var.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/delete_var.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/delete_var.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/delete_var.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/edit_var.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/edit_var.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/edit_var.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/edit_var.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/var_config/__examples__/var_config.stories.tsx b/x-pack/plugins/canvas/public/components/var_config/__stories__/var_config.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/var_config/__examples__/var_config.stories.tsx rename to x-pack/plugins/canvas/public/components/var_config/__stories__/var_config.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts index abd40731078ec..34e3d3ff4b057 100644 --- a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts @@ -4,20 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { addColor, removeColor } from '../../state/actions/workpad'; -import { getWorkpadColors } from '../../state/selectors/workpad'; - -import { WorkpadColorPicker as Component } from '../workpad_color_picker/workpad_color_picker'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - colors: getWorkpadColors(state), -}); - -const mapDispatchToProps = { - onAddColor: addColor, - onRemoveColor: removeColor, -}; - -export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); +export { WorkpadColorPicker } from './workpad_color_picker'; +export { WorkpadColorPicker as WorkpadColorPickerComponent } from './workpad_color_picker.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.tsx rename to x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts new file mode 100644 index 0000000000000..2f4b0fe7b4ec1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { addColor, removeColor } from '../../state/actions/workpad'; +import { getWorkpadColors } from '../../state/selectors/workpad'; + +import { WorkpadColorPicker as Component } from '../workpad_color_picker/workpad_color_picker.component'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + colors: getWorkpadColors(state), +}); + +const mapDispatchToProps = { + onAddColor: addColor, + onRemoveColor: removeColor, +}; + +export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_config/index.ts b/x-pack/plugins/canvas/public/components/workpad_config/index.ts index bba08d7647e9e..63db96ca5aef9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_config/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_config/index.ts @@ -4,40 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; - -import { get } from 'lodash'; -import { - sizeWorkpad as setSize, - setName, - setWorkpadCSS, - updateWorkpadVariables, -} from '../../state/actions/workpad'; - -import { getWorkpad } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { WorkpadConfig as Component } from './workpad_config'; -import { State, CanvasVariable } from '../../../types'; - -const mapStateToProps = (state: State) => { - const workpad = getWorkpad(state); - - return { - name: get(workpad, 'name'), - size: { - width: get(workpad, 'width'), - height: get(workpad, 'height'), - }, - css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), - variables: get(workpad, 'variables', []), - }; -}; - -const mapDispatchToProps = { - setSize, - setName, - setWorkpadCSS, - setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars), -}; - -export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); +export { WorkpadConfig } from './workpad_config'; +export { WorkpadConfig as WorkpadConfigComponent } from './workpad_config.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx rename to x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts new file mode 100644 index 0000000000000..e4ddf31141972 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; + +import { get } from 'lodash'; +import { + sizeWorkpad as setSize, + setName, + setWorkpadCSS, + updateWorkpadVariables, +} from '../../state/actions/workpad'; + +import { getWorkpad } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { WorkpadConfig as Component } from './workpad_config.component'; +import { State, CanvasVariable } from '../../../types'; + +const mapStateToProps = (state: State) => { + const workpad = getWorkpad(state); + + return { + name: get(workpad, 'name'), + size: { + width: get(workpad, 'width'), + height: get(workpad, 'height'), + }, + css: get(workpad, 'css', DEFAULT_WORKPAD_CSS), + variables: get(workpad, 'variables', []), + }; +}; + +const mapDispatchToProps = { + setSize, + setName, + setWorkpadCSS, + setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars), +}; + +export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/edit_menu.stories.tsx similarity index 98% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/edit_menu.stories.tsx index 8bbc3e09af4bf..be6247b0bbcab 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/edit_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { EditMenu } from '../edit_menu'; +import { EditMenu } from '../edit_menu.component'; import { PositionedElement } from '../../../../../types'; const handlers = { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts new file mode 100644 index 0000000000000..3a2264c05eb4b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, PositionedElement } from '../../../../types'; +import { getClipboardData } from '../../../lib/clipboard'; +// @ts-expect-error untyped local +import { flatten } from '../../../lib/aeroelastic/functional'; +// @ts-expect-error untyped local +import { globalStateUpdater } from '../../workpad_page/integration_utils'; +// @ts-expect-error untyped local +import { crawlTree } from '../../workpad_page/integration_utils'; +// @ts-expect-error untyped local +import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; +// @ts-expect-error untyped local +import { undoHistory, redoHistory } from '../../../state/actions/history'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../../state/actions/transient'; +import { + getSelectedPage, + getNodes, + getSelectedToplevelNodes, +} from '../../../state/selectors/workpad'; +import { + layerHandlerCreators, + clipboardHandlerCreators, + basicHandlerCreators, + groupHandlerCreators, + alignmentDistributionHandlerCreators, +} from '../../../lib/element_handler_creators'; +import { EditMenu as Component, Props as ComponentProps } from './edit_menu.component'; + +type LayoutState = any; + +type CommitFn = (type: string, payload: any) => LayoutState; + +interface OwnProps { + commit: CommitFn; +} + +const withGlobalState = ( + commit: CommitFn, + updateGlobalState: (layoutState: LayoutState) => void +) => (type: string, payload: any) => { + const newLayoutState = commit(type, payload); + if (newLayoutState.currentScene.gestureEnd) { + updateGlobalState(newLayoutState); + } +}; + +/* + * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts + */ +const mapStateToProps = (state: State) => { + const pageId = getSelectedPage(state); + const nodes = getNodes(state, pageId) as PositionedElement[]; + const selectedToplevelNodes = getSelectedToplevelNodes(state); + + const selectedPrimaryShapeObjects = selectedToplevelNodes + .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) + .filter((shape?: PositionedElement) => shape) as PositionedElement[]; + + const selectedPersistentPrimaryNodes = flatten( + selectedPrimaryShapeObjects.map((shape: PositionedElement) => + nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? + ? [shape.id] + : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) + ) + ); + + const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + const selectedNodes = selectedNodeIds + .map((id: string) => nodes.find((s) => s.id === id)) + .filter((node: PositionedElement | undefined): node is PositionedElement => { + return !!node; + }); + + return { + pageId, + selectedToplevelNodes, + selectedNodes, + state, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), + removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes.filter((e: PositionedElement) => !e.position.parent).map((e) => e.id) + ) + ), + elementLayer: (pageId: string, elementId: string, movement: number) => { + dispatch(elementLayer({ pageId, elementId, movement })); + }, + undoHistory: () => dispatch(undoHistory()), + redoHistory: () => dispatch(redoHistory()), + dispatch, +}); + +const mergeProps = ( + { state, selectedToplevelNodes, ...restStateProps }: ReturnType, + { dispatch, ...restDispatchProps }: ReturnType, + { commit }: OwnProps +) => { + const updateGlobalState = globalStateUpdater(dispatch, state); + + return { + ...restDispatchProps, + ...restStateProps, + commit: withGlobalState(commit, updateGlobalState), + groupIsSelected: + selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), + }; +}; + +export const EditMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), + withHandlers(basicHandlerCreators), + withHandlers(clipboardHandlerCreators), + withHandlers(layerHandlerCreators), + withHandlers(groupHandlerCreators), + withHandlers(alignmentDistributionHandlerCreators) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts index 8f013f70aefcd..0db425f01cccd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts @@ -4,130 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withHandlers, withProps } from 'recompose'; -import { Dispatch } from 'redux'; -import { State, PositionedElement } from '../../../../types'; -import { getClipboardData } from '../../../lib/clipboard'; -// @ts-expect-error untyped local -import { flatten } from '../../../lib/aeroelastic/functional'; -// @ts-expect-error untyped local -import { globalStateUpdater } from '../../workpad_page/integration_utils'; -// @ts-expect-error untyped local -import { crawlTree } from '../../workpad_page/integration_utils'; -// @ts-expect-error untyped local -import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; -// @ts-expect-error untyped local -import { undoHistory, redoHistory } from '../../../state/actions/history'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../../state/actions/transient'; -import { - getSelectedPage, - getNodes, - getSelectedToplevelNodes, -} from '../../../state/selectors/workpad'; -import { - layerHandlerCreators, - clipboardHandlerCreators, - basicHandlerCreators, - groupHandlerCreators, - alignmentDistributionHandlerCreators, -} from '../../../lib/element_handler_creators'; -import { EditMenu as Component, Props as ComponentProps } from './edit_menu'; - -type LayoutState = any; - -type CommitFn = (type: string, payload: any) => LayoutState; - -interface OwnProps { - commit: CommitFn; -} - -const withGlobalState = ( - commit: CommitFn, - updateGlobalState: (layoutState: LayoutState) => void -) => (type: string, payload: any) => { - const newLayoutState = commit(type, payload); - if (newLayoutState.currentScene.gestureEnd) { - updateGlobalState(newLayoutState); - } -}; - -/* - * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts - */ -const mapStateToProps = (state: State) => { - const pageId = getSelectedPage(state); - const nodes = getNodes(state, pageId) as PositionedElement[]; - const selectedToplevelNodes = getSelectedToplevelNodes(state); - - const selectedPrimaryShapeObjects = selectedToplevelNodes - .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) - .filter((shape?: PositionedElement) => shape) as PositionedElement[]; - - const selectedPersistentPrimaryNodes = flatten( - selectedPrimaryShapeObjects.map((shape: PositionedElement) => - nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? - ? [shape.id] - : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) - ) - ); - - const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); - const selectedNodes = selectedNodeIds - .map((id: string) => nodes.find((s) => s.id === id)) - .filter((node: PositionedElement | undefined): node is PositionedElement => { - return !!node; - }); - - return { - pageId, - selectedToplevelNodes, - selectedNodes, - state, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), - removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes.filter((e: PositionedElement) => !e.position.parent).map((e) => e.id) - ) - ), - elementLayer: (pageId: string, elementId: string, movement: number) => { - dispatch(elementLayer({ pageId, elementId, movement })); - }, - undoHistory: () => dispatch(undoHistory()), - redoHistory: () => dispatch(redoHistory()), - dispatch, -}); - -const mergeProps = ( - { state, selectedToplevelNodes, ...restStateProps }: ReturnType, - { dispatch, ...restDispatchProps }: ReturnType, - { commit }: OwnProps -) => { - const updateGlobalState = globalStateUpdater(dispatch, state); - - return { - ...restDispatchProps, - ...restStateProps, - commit: withGlobalState(commit, updateGlobalState), - groupIsSelected: - selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), - }; -}; - -export const EditMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), - withHandlers(basicHandlerCreators), - withHandlers(clipboardHandlerCreators), - withHandlers(layerHandlerCreators), - withHandlers(groupHandlerCreators), - withHandlers(alignmentDistributionHandlerCreators) -)(Component); +export { EditMenu } from './edit_menu'; +export { EditMenu as EditMenuComponent } from './edit_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx similarity index 98% rename from x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx index 9aca5ce33ba02..cf9b334ffe8ea 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx @@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ElementSpec } from '../../../../../types'; -import { ElementMenu } from '../element_menu'; +import { ElementMenu } from '../element_menu.component'; const testElements: { [key: string]: ElementSpec } = { areaChart: { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx new file mode 100644 index 0000000000000..6d9233aaba22b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sortBy } from 'lodash'; +import React, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ElementSpec } from '../../../../types'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { getId } from '../../../lib/get_id'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { AssetManager } from '../../asset_manager'; +import { SavedElementsModal } from '../../saved_elements_modal'; + +interface CategorizedElementLists { + [key: string]: ElementSpec[]; +} + +interface ElementTypeMeta { + [key: string]: { name: string; icon: string }; +} + +export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; + +// label and icon for the context menu item for each element type +const elementTypeMeta: ElementTypeMeta = { + chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, + filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, + image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, + other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, + progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, + shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, + text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, +}; + +const getElementType = (element: ElementSpec): string => + element && element.type && Object.keys(elementTypeMeta).includes(element.type) + ? element.type + : 'other'; + +const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { + elements = sortBy(elements, 'displayName'); + + const categories: CategorizedElementLists = { other: [] }; + + elements.forEach((element: ElementSpec) => { + const type = getElementType(element); + + if (categories[type]) { + categories[type].push(element); + } else { + categories[type] = [element]; + } + }); + + return categories; +}; + +export interface Props { + /** + * Dictionary of elements from elements registry + */ + elements: { [key: string]: ElementSpec }; + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: ElementSpec) => void; + /** + * Renders embeddable flyout + */ + renderEmbedPanel: (onClose: () => void) => JSX.Element; +} + +export const ElementMenu: FunctionComponent = ({ + elements, + addElement, + renderEmbedPanel, +}) => { + const [isAssetModalVisible, setAssetModalVisible] = useState(false); + const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); + const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); + + const hideAssetModal = () => setAssetModalVisible(false); + const showAssetModal = () => setAssetModalVisible(true); + const hideEmbedPanel = () => setEmbedPanelVisible(false); + const showEmbedPanel = () => setEmbedPanelVisible(true); + const hideSavedElementsModal = () => setSavedElementsModalVisible(false); + const showSavedElementsModal = () => setSavedElementsModalVisible(true); + + const { + chart: chartElements, + filter: filterElements, + image: imageElements, + other: otherElements, + progress: progressElements, + shape: shapeElements, + text: textElements, + } = categorizeElementsByType(Object.values(elements)); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ + name: element.displayName || element.name, + icon: element.icon, + onClick: () => { + addElement(element); + closePopover(); + }, + }); + + const elementListToMenuItems = (elementList: ElementSpec[]) => { + const type = getElementType(elementList[0]); + const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; + + if (elementList.length > 1) { + return { + name, + icon: , + panel: { + id: getId('element-type'), + title: name, + items: elementList.map(elementToMenuItem), + }, + }; + } + + return elementToMenuItem(elementList[0]); + }; + + return { + id: 0, + items: [ + elementListToMenuItems(textElements), + elementListToMenuItems(shapeElements), + elementListToMenuItems(chartElements), + elementListToMenuItems(imageElements), + elementListToMenuItems(filterElements), + elementListToMenuItems(progressElements), + elementListToMenuItems(otherElements), + { + name: strings.getMyElementsMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'saved-elements-menu-option', + icon: , + onClick: () => { + showSavedElementsModal(); + closePopover(); + }, + }, + { + name: strings.getAssetsMenuItemLabel(), + icon: , + onClick: () => { + showAssetModal(); + closePopover(); + }, + }, + { + name: strings.getEmbedObjectMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: , + onClick: () => { + showEmbedPanel(); + closePopover(); + }, + }, + ], + }; + }; + + const exportControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getElementMenuButtonLabel()} + + ); + + return ( + + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {isAssetModalVisible ? : null} + {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} + {isSavedElementsModalVisible ? : null} + + ); +}; + +ElementMenu.propTypes = { + elements: PropTypes.object, + addElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index 6d9233aaba22b..2cbe4ae5a6575 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -4,211 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; -import React, { Fragment, FunctionComponent, useState } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiButton, - EuiContextMenu, - EuiIcon, - EuiContextMenuPanelItemDescriptor, -} from '@elastic/eui'; -import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; -import { ComponentStrings } from '../../../../i18n/components'; -import { ElementSpec } from '../../../../types'; -import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; -import { getId } from '../../../lib/get_id'; -import { Popover, ClosePopoverFn } from '../../popover'; -import { AssetManager } from '../../asset_manager'; -import { SavedElementsModal } from '../../saved_elements_modal'; - -interface CategorizedElementLists { - [key: string]: ElementSpec[]; -} - -interface ElementTypeMeta { - [key: string]: { name: string; icon: string }; +import React from 'react'; +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, ElementSpec } from '../../../../types'; +// @ts-expect-error untyped local +import { elementsRegistry } from '../../../lib/elements_registry'; +import { ElementMenu as Component, Props as ComponentProps } from './element_menu.component'; +// @ts-expect-error untyped local +import { addElement } from '../../../state/actions/elements'; +import { getSelectedPage } from '../../../state/selectors/workpad'; +import { AddEmbeddablePanel } from '../../embeddable_flyout'; + +interface StateProps { + pageId: string; } -export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; - -// label and icon for the context menu item for each element type -const elementTypeMeta: ElementTypeMeta = { - chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, - filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, - image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, - other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, - progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, - shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, - text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, -}; - -const getElementType = (element: ElementSpec): string => - element && element.type && Object.keys(elementTypeMeta).includes(element.type) - ? element.type - : 'other'; - -const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { - elements = sortBy(elements, 'displayName'); - - const categories: CategorizedElementLists = { other: [] }; - - elements.forEach((element: ElementSpec) => { - const type = getElementType(element); - - if (categories[type]) { - categories[type].push(element); - } else { - categories[type] = [element]; - } - }); - - return categories; -}; - -export interface Props { - /** - * Dictionary of elements from elements registry - */ - elements: { [key: string]: ElementSpec }; - /** - * Handler for adding a selected element to the workpad - */ - addElement: (element: ElementSpec) => void; - /** - * Renders embeddable flyout - */ - renderEmbedPanel: (onClose: () => void) => JSX.Element; +interface DispatchProps { + addElement: (pageId: string) => (partialElement: ElementSpec) => void; } -export const ElementMenu: FunctionComponent = ({ - elements, - addElement, - renderEmbedPanel, -}) => { - const [isAssetModalVisible, setAssetModalVisible] = useState(false); - const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); - const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); - - const hideAssetModal = () => setAssetModalVisible(false); - const showAssetModal = () => setAssetModalVisible(true); - const hideEmbedPanel = () => setEmbedPanelVisible(false); - const showEmbedPanel = () => setEmbedPanelVisible(true); - const hideSavedElementsModal = () => setSavedElementsModalVisible(false); - const showSavedElementsModal = () => setSavedElementsModalVisible(true); - - const { - chart: chartElements, - filter: filterElements, - image: imageElements, - other: otherElements, - progress: progressElements, - shape: shapeElements, - text: textElements, - } = categorizeElementsByType(Object.values(elements)); - - const getPanelTree = (closePopover: ClosePopoverFn) => { - const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ - name: element.displayName || element.name, - icon: element.icon, - onClick: () => { - addElement(element); - closePopover(); - }, - }); - - const elementListToMenuItems = (elementList: ElementSpec[]) => { - const type = getElementType(elementList[0]); - const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; - - if (elementList.length > 1) { - return { - name, - icon: , - panel: { - id: getId('element-type'), - title: name, - items: elementList.map(elementToMenuItem), - }, - }; - } - - return elementToMenuItem(elementList[0]); - }; - - return { - id: 0, - items: [ - elementListToMenuItems(textElements), - elementListToMenuItems(shapeElements), - elementListToMenuItems(chartElements), - elementListToMenuItems(imageElements), - elementListToMenuItems(filterElements), - elementListToMenuItems(progressElements), - elementListToMenuItems(otherElements), - { - name: strings.getMyElementsMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - 'data-test-subj': 'saved-elements-menu-option', - icon: , - onClick: () => { - showSavedElementsModal(); - closePopover(); - }, - }, - { - name: strings.getAssetsMenuItemLabel(), - icon: , - onClick: () => { - showAssetModal(); - closePopover(); - }, - }, - { - name: strings.getEmbedObjectMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - icon: , - onClick: () => { - showEmbedPanel(); - closePopover(); - }, - }, - ], - }; - }; - - const exportControl = (togglePopover: React.MouseEventHandler) => ( - - {strings.getElementMenuButtonLabel()} - - ); - - return ( - - - {({ closePopover }: { closePopover: ClosePopoverFn }) => ( - - )} - - {isAssetModalVisible ? : null} - {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} - {isSavedElementsModalVisible ? : null} - - ); -}; - -ElementMenu.propTypes = { - elements: PropTypes.object, - addElement: PropTypes.func.isRequired, -}; +const mapStateToProps = (state: State) => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), +}); + +const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ + ...stateProps, + ...dispatchProps, + addElement: dispatchProps.addElement(stateProps.pageId), + // Moved this section out of the main component to enable stories + renderEmbedPanel: (onClose: () => void) => , +}); + +export const ElementMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ elements: elementsRegistry.toJS() })) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts new file mode 100644 index 0000000000000..26f81e125f6e2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElementMenu } from './element_menu'; +export { ElementMenu as ElementMenuComponent } from './element_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx deleted file mode 100644 index 264873fc994dd..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { Dispatch } from 'redux'; -import { State, ElementSpec } from '../../../../types'; -// @ts-expect-error untyped local -import { elementsRegistry } from '../../../lib/elements_registry'; -import { ElementMenu as Component, Props as ComponentProps } from './element_menu'; -// @ts-expect-error untyped local -import { addElement } from '../../../state/actions/elements'; -import { getSelectedPage } from '../../../state/selectors/workpad'; -import { AddEmbeddablePanel } from '../../embeddable_flyout'; - -interface StateProps { - pageId: string; -} - -interface DispatchProps { - addElement: (pageId: string) => (partialElement: ElementSpec) => void; -} - -const mapStateToProps = (state: State) => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), -}); - -const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ - ...stateProps, - ...dispatchProps, - addElement: dispatchProps.addElement(stateProps.pageId), - // Moved this section out of the main component to enable stories - renderEmbedPanel: (onClose: () => void) => , -}); - -export const ElementMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps(() => ({ elements: elementsRegistry.toJS() })) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/index.ts new file mode 100644 index 0000000000000..0b6f8cc06d198 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WorkpadHeader } from './workpad_header'; +export { WorkpadHeader as WorkpadHeaderComponent } from './workpad_header.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/index.tsx deleted file mode 100644 index 407b4ff932811..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; -import { setWriteable } from '../../state/actions/workpad'; -import { State } from '../../../types'; -import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header'; - -interface StateProps { - isWriteable: boolean; - canUserWrite: boolean; - selectedPage: string; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; -} - -const mapStateToProps = (state: State): StateProps => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - canUserWrite: canUserWrite(state), - selectedPage: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), -}); - -export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts index 87b926d93ccb9..8db62f5ac2d87 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts @@ -4,19 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../../../state/actions/elements'; -import { getInFlight } from '../../../state/selectors/resolved_args'; -import { State } from '../../../../types'; -import { RefreshControl as Component } from './refresh_control'; - -const mapStateToProps = (state: State) => ({ - inFlight: getInFlight(state), -}); - -const mapDispatchToProps = { - doRefresh: fetchAllRenderables, -}; - -export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); +export { RefreshControl } from './refresh_control'; +export { RefreshControl as RefreshControlComponent } from './refresh_control.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts new file mode 100644 index 0000000000000..a7f01e46927ce --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +import { getInFlight } from '../../../state/selectors/resolved_args'; +import { State } from '../../../../types'; +import { RefreshControl as Component } from './refresh_control.component'; + +const mapStateToProps = (state: State) => ({ + inFlight: getInFlight(state), +}); + +const mapDispatchToProps = { + doRefresh: fetchAllRenderables, +}; + +export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/pdf_panel.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/pdf_panel.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx similarity index 92% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx index ab9137b1676c9..e0a1f0e381fd3 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { ShareMenu } from '../share_menu'; +import { ShareMenu } from '../share_menu.component'; storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => ( { + const renderers: string[] = []; + const expressions = getRenderedWorkpadExpressions(state); + expressions.forEach((expression) => { + if (!renderFunctionNames.includes(expression)) { + renderers.push(expression); + } + }); + + return renderers; +}; + +const mapStateToProps = (state: State) => ({ + renderedWorkpad: getRenderedWorkpad(state), + unsupportedRenderers: getUnsupportedRenderers(state), + workpad: getWorkpad(state), +}); + +interface Props { + onClose: OnCloseFn; + renderedWorkpad: CanvasRenderedWorkpad; + unsupportedRenderers: string[]; + workpad: CanvasWorkpad; +} + +export const ShareWebsiteFlyout = compose>( + connect(mapStateToProps), + withKibana, + withProps( + ({ + unsupportedRenderers, + renderedWorkpad, + onClose, + workpad, + kibana, + }: Props & WithKibanaProps): ComponentProps => ({ + unsupportedRenderers, + onClose, + onCopy: () => { + kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); + }, + onDownload: (type) => { + switch (type) { + case 'share': + downloadRenderedWorkpad(renderedWorkpad); + return; + case 'shareRuntime': + downloadRuntime(kibana.services.http.basePath.get()); + return; + case 'shareZip': + const basePath = kibana.services.http.basePath.get(); + arrayBufferFetch + .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) + .then((blob) => downloadZippedRuntime(blob.data)) + .catch((err: Error) => { + kibana.services.canvas.notify.error(err, { + title: strings.getShareableZipErrorTitle(workpad.name), + }); + }); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts index 1e1eac2a1dcf3..335c5dff6ed74 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts @@ -4,95 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { - getWorkpad, - getRenderedWorkpad, - getRenderedWorkpadExpressions, -} from '../../../../state/selectors/workpad'; -import { - downloadRenderedWorkpad, - downloadRuntime, - downloadZippedRuntime, -} from '../../../../lib/download_workpad'; -import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; -import { State, CanvasWorkpad } from '../../../../../types'; -import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; -import { arrayBufferFetch } from '../../../../../common/lib/fetch'; -import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; -import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; - -import { ComponentStrings } from '../../../../../i18n/components'; -import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; -import { OnCloseFn } from '../share_menu'; -import { WithKibanaProps } from '../../../../index'; -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -const getUnsupportedRenderers = (state: State) => { - const renderers: string[] = []; - const expressions = getRenderedWorkpadExpressions(state); - expressions.forEach((expression) => { - if (!renderFunctionNames.includes(expression)) { - renderers.push(expression); - } - }); - - return renderers; -}; - -const mapStateToProps = (state: State) => ({ - renderedWorkpad: getRenderedWorkpad(state), - unsupportedRenderers: getUnsupportedRenderers(state), - workpad: getWorkpad(state), -}); - -interface Props { - onClose: OnCloseFn; - renderedWorkpad: CanvasRenderedWorkpad; - unsupportedRenderers: string[]; - workpad: CanvasWorkpad; -} - -export const ShareWebsiteFlyout = compose>( - connect(mapStateToProps), - withKibana, - withProps( - ({ - unsupportedRenderers, - renderedWorkpad, - onClose, - workpad, - kibana, - }: Props & WithKibanaProps): ComponentProps => ({ - unsupportedRenderers, - onClose, - onCopy: () => { - kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); - }, - onDownload: (type) => { - switch (type) { - case 'share': - downloadRenderedWorkpad(renderedWorkpad); - return; - case 'shareRuntime': - downloadRuntime(kibana.services.http.basePath.get()); - return; - case 'shareZip': - const basePath = kibana.services.http.basePath.get(); - arrayBufferFetch - .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) - .then((blob) => downloadZippedRuntime(blob.data)) - .catch((err: Error) => { - kibana.services.canvas.notify.error(err, { - title: strings.getShareableZipErrorTitle(workpad.name), - }); - }); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); +export { ShareWebsiteFlyout } from './flyout'; +export { ShareWebsiteFlyout as ShareWebsiteFlyoutComponent } from './flyout.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx index ea8aba688b2a6..b38226bb12a23 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx @@ -9,7 +9,7 @@ import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; import { ComponentStrings } from '../../../../../i18n/components'; -import { OnDownloadFn } from './share_website_flyout'; +import { OnDownloadFn } from './flyout'; const { ShareWebsiteRuntimeStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx index 81f559651eb25..42497fcd316fe 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx @@ -19,7 +19,7 @@ import { import { ComponentStrings } from '../../../../../i18n/components'; import { Clipboard } from '../../../clipboard'; -import { OnCopyFn } from './share_website_flyout'; +import { OnCopyFn } from './flyout'; const { ShareWebsiteSnippetsStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx index 1a5884d89d066..ac4dfe6872d3c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx @@ -9,7 +9,7 @@ import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; import { ComponentStrings } from '../../../../../i18n/components'; -import { OnDownloadFn } from './share_website_flyout'; +import { OnDownloadFn } from './flyout'; const { ShareWebsiteWorkpadStep: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts index 01bcfebc0dba9..19dc9b668e61a 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/index.ts @@ -4,95 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; -import { getWorkpad, getPages } from '../../../state/selectors/workpad'; -import { getWindow } from '../../../lib/get_window'; -import { downloadWorkpad } from '../../../lib/download_workpad'; -import { ShareMenu as Component, Props as ComponentProps } from './share_menu'; -import { getPdfUrl, createPdf } from './utils'; -import { State, CanvasWorkpad } from '../../../../types'; -import { withServices, WithServicesProps } from '../../../services'; - -import { ComponentStrings } from '../../../../i18n'; - -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -const mapStateToProps = (state: State) => ({ - workpad: getWorkpad(state), - pageCount: getPages(state).length, -}); - -const getAbsoluteUrl = (path: string) => { - const { location } = getWindow(); - - if (!location) { - return path; - } // fallback for mocked window object - - const { protocol, hostname, port } = location; - return `${protocol}//${hostname}:${port}${path}`; -}; - -interface Props { - workpad: CanvasWorkpad; - pageCount: number; -} - -export const ShareMenu = compose( - connect(mapStateToProps), - withServices, - withProps( - ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ - getExportUrl: (type) => { - if (type === 'pdf') { - const pdfUrl = getPdfUrl( - workpad, - { pageCount }, - services.platform.getBasePathInterface() - ); - return getAbsoluteUrl(pdfUrl); - } - - throw new Error(strings.getUnknownExportErrorMessage(type)); - }, - onCopy: (type) => { - switch (type) { - case 'pdf': - services.notify.info(strings.getCopyPDFMessage()); - break; - case 'reportingConfig': - services.notify.info(strings.getCopyReportingConfigMessage()); - break; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - onExport: (type) => { - switch (type) { - case 'pdf': - return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) - .then(({ data }: { data: { job: { id: string } } }) => { - services.notify.info(strings.getExportPDFMessage(), { - title: strings.getExportPDFTitle(workpad.name), - }); - - // register the job so a completion notification shows up when it's ready - jobCompletionNotifications.add(data.job.id); - }) - .catch((err: Error) => { - services.notify.error(err, { - title: strings.getExportPDFErrorTitle(workpad.name), - }); - }); - case 'json': - downloadWorkpad(workpad.id); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); +export { ShareMenu } from './share_menu'; +export { ShareMenu as ShareMenuComponent } from './share_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts new file mode 100644 index 0000000000000..85c4b14a28c13 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; +import { getWorkpad, getPages } from '../../../state/selectors/workpad'; +import { getWindow } from '../../../lib/get_window'; +import { downloadWorkpad } from '../../../lib/download_workpad'; +import { ShareMenu as Component, Props as ComponentProps } from './share_menu.component'; +import { getPdfUrl, createPdf } from './utils'; +import { State, CanvasWorkpad } from '../../../../types'; +import { withServices, WithServicesProps } from '../../../services'; + +import { ComponentStrings } from '../../../../i18n'; + +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +const mapStateToProps = (state: State) => ({ + workpad: getWorkpad(state), + pageCount: getPages(state).length, +}); + +const getAbsoluteUrl = (path: string) => { + const { location } = getWindow(); + + if (!location) { + return path; + } // fallback for mocked window object + + const { protocol, hostname, port } = location; + return `${protocol}//${hostname}:${port}${path}`; +}; + +interface Props { + workpad: CanvasWorkpad; + pageCount: number; +} + +export const ShareMenu = compose( + connect(mapStateToProps), + withServices, + withProps( + ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ + getExportUrl: (type) => { + if (type === 'pdf') { + const pdfUrl = getPdfUrl( + workpad, + { pageCount }, + services.platform.getBasePathInterface() + ); + return getAbsoluteUrl(pdfUrl); + } + + throw new Error(strings.getUnknownExportErrorMessage(type)); + }, + onCopy: (type) => { + switch (type) { + case 'pdf': + services.notify.info(strings.getCopyPDFMessage()); + break; + case 'reportingConfig': + services.notify.info(strings.getCopyReportingConfigMessage()); + break; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + onExport: (type) => { + switch (type) { + case 'pdf': + return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface()) + .then(({ data }: { data: { job: { id: string } } }) => { + services.notify.info(strings.getExportPDFMessage(), { + title: strings.getExportPDFTitle(workpad.name), + }); + + // register the job so a completion notification shows up when it's ready + jobCompletionNotifications.add(data.job.id); + }) + .catch((err: Error) => { + services.notify.error(err, { + title: strings.getExportPDFErrorTitle(workpad.name), + }); + }); + case 'json': + downloadWorkpad(workpad.id); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/__snapshots__/view_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/__snapshots__/view_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/view_menu.stories.tsx similarity index 97% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/view_menu.stories.tsx index 5b4de05da3a3d..6b033feb26021 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/__stories__/view_menu.stories.tsx @@ -6,7 +6,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { ViewMenu } from '../view_menu'; +import { ViewMenu } from '../view_menu.component'; const handlers = { setZoomScale: action('setZoomScale'), diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts index e2a05d13b017e..167b3822fd13d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -4,97 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withHandlers } from 'recompose'; -import { Dispatch } from 'redux'; -import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; -import { State, CanvasWorkpadBoundingBox } from '../../../../types'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../../../state/actions/elements'; -// @ts-expect-error untyped local -import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; -import { - setWriteable, - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, -} from '../../../state/actions/workpad'; -import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; -import { - getWorkpadBoundingBox, - getWorkpadWidth, - getWorkpadHeight, - isWriteable, - getRefreshInterval, - getAutoplay, -} from '../../../state/selectors/workpad'; -import { ViewMenu as Component, Props as ComponentProps } from './view_menu'; -import { getFitZoomScale } from './lib/get_fit_zoom_scale'; - -interface StateProps { - zoomScale: number; - boundingBox: CanvasWorkpadBoundingBox; - workpadWidth: number; - workpadHeight: number; - isWriteable: boolean; -} - -interface DispatchProps { - setWriteable: (isWorkpadWriteable: boolean) => void; - setZoomScale: (scale: number) => void; - setFullscreen: (showFullscreen: boolean) => void; -} - -const mapStateToProps = (state: State) => { - const { enabled, interval } = getAutoplay(state); - - return { - zoomScale: getZoomScale(state), - boundingBox: getWorkpadBoundingBox(state), - workpadWidth: getWorkpadWidth(state), - workpadHeight: getWorkpadHeight(state), - isWriteable: isWriteable(state) && canUserWrite(state), - refreshInterval: getRefreshInterval(state), - autoplayEnabled: enabled, - autoplayInterval: interval, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), - setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), - setFullscreen: (value: boolean) => { - dispatch(setFullscreen(value)); - - if (value) { - dispatch(selectToplevelNodes([])); - } - }, - doRefresh: () => dispatch(fetchAllRenderables()), - setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), - enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), - setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: ComponentProps -): ComponentProps => { - const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; - - return { - ...remainingStateProps, - ...dispatchProps, - ...ownProps, - toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), - enterFullscreen: () => dispatchProps.setFullscreen(true), - fitToWindow: () => - dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), - }; -}; - -export const ViewMenu = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withHandlers(zoomHandlerCreators) -)(Component); +export { ViewMenu } from './view_menu'; +export { ViewMenu as ViewMenuComponent } from './view_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts new file mode 100644 index 0000000000000..c9650a35ea2a6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers } from 'recompose'; +import { Dispatch } from 'redux'; +import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; +import { State, CanvasWorkpadBoundingBox } from '../../../../types'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +// @ts-expect-error untyped local +import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; +import { + setWriteable, + setRefreshInterval, + enableAutoplay, + setAutoplayInterval, +} from '../../../state/actions/workpad'; +import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; +import { + getWorkpadBoundingBox, + getWorkpadWidth, + getWorkpadHeight, + isWriteable, + getRefreshInterval, + getAutoplay, +} from '../../../state/selectors/workpad'; +import { ViewMenu as Component, Props as ComponentProps } from './view_menu.component'; +import { getFitZoomScale } from './lib/get_fit_zoom_scale'; + +interface StateProps { + zoomScale: number; + boundingBox: CanvasWorkpadBoundingBox; + workpadWidth: number; + workpadHeight: number; + isWriteable: boolean; +} + +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; + setZoomScale: (scale: number) => void; + setFullscreen: (showFullscreen: boolean) => void; +} + +const mapStateToProps = (state: State) => { + const { enabled, interval } = getAutoplay(state); + + return { + zoomScale: getZoomScale(state), + boundingBox: getWorkpadBoundingBox(state), + workpadWidth: getWorkpadWidth(state), + workpadHeight: getWorkpadHeight(state), + isWriteable: isWriteable(state) && canUserWrite(state), + refreshInterval: getRefreshInterval(state), + autoplayEnabled: enabled, + autoplayInterval: interval, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), + setFullscreen: (value: boolean) => { + dispatch(setFullscreen(value)); + + if (value) { + dispatch(selectToplevelNodes([])); + } + }, + doRefresh: () => dispatch(fetchAllRenderables()), + setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), + enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), + setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; + + return { + ...remainingStateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), + enterFullscreen: () => dispatchProps.setFullscreen(true), + fitToWindow: () => + dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), + }; +}; + +export const ViewMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withHandlers(zoomHandlerCreators) +)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx new file mode 100644 index 0000000000000..eb4b451896b46 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +// @ts-expect-error no @types definition +import { Shortcuts } from 'react-shortcuts'; +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { ComponentStrings } from '../../../i18n'; +import { ToolTipShortcut } from '../tool_tip_shortcut/'; +import { RefreshControl } from './refresh_control'; +// @ts-expect-error untyped local +import { FullscreenControl } from './fullscreen_control'; +import { EditMenu } from './edit_menu'; +import { ElementMenu } from './element_menu'; +import { ShareMenu } from './share_menu'; +import { ViewMenu } from './view_menu'; + +const { WorkpadHeader: strings } = ComponentStrings; + +export interface Props { + isWriteable: boolean; + toggleWriteable: () => void; + canUserWrite: boolean; + commit: (type: string, payload: any) => any; +} + +export const WorkpadHeader: FunctionComponent = ({ + isWriteable, + canUserWrite, + toggleWriteable, + commit, +}) => { + const keyHandler = (action: string) => { + if (action === 'EDITING') { + toggleWriteable(); + } + }; + + const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( + + {strings.getFullScreenTooltip()}{' '} + + + } + > + + + ); + + const getEditToggleToolTipText = () => { + if (!canUserWrite) { + return strings.getNoWritePermissionTooltipText(); + } + + const content = isWriteable + ? strings.getHideEditControlTooltip() + : strings.getShowEditControlTooltip(); + + return content; + }; + + const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { + const content = getEditToggleToolTipText(); + + if (textOnly) { + return content; + } + + return ( + + {content} + + ); + }; + + return ( + + + + {isWriteable && ( + + + + )} + + + + + + + + + + + + + + + {canUserWrite && ( + + )} + + + + + + + + + {fullscreenButton} + + + + + ); +}; + +WorkpadHeader.propTypes = { + isWriteable: PropTypes.bool, + toggleWriteable: PropTypes.func, + canUserWrite: PropTypes.bool, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx index eb4b451896b46..1f630040b0c36 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -4,147 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -// @ts-expect-error no @types definition -import { Shortcuts } from 'react-shortcuts'; -import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n'; -import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { RefreshControl } from './refresh_control'; -// @ts-expect-error untyped local -import { FullscreenControl } from './fullscreen_control'; -import { EditMenu } from './edit_menu'; -import { ElementMenu } from './element_menu'; -import { ShareMenu } from './share_menu'; -import { ViewMenu } from './view_menu'; - -const { WorkpadHeader: strings } = ComponentStrings; - -export interface Props { +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; +import { setWriteable } from '../../state/actions/workpad'; +import { State } from '../../../types'; +import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header.component'; + +interface StateProps { isWriteable: boolean; - toggleWriteable: () => void; canUserWrite: boolean; - commit: (type: string, payload: any) => any; + selectedPage: string; } -export const WorkpadHeader: FunctionComponent = ({ - isWriteable, - canUserWrite, - toggleWriteable, - commit, -}) => { - const keyHandler = (action: string) => { - if (action === 'EDITING') { - toggleWriteable(); - } - }; - - const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( - - {strings.getFullScreenTooltip()}{' '} - - - } - > - - - ); - - const getEditToggleToolTipText = () => { - if (!canUserWrite) { - return strings.getNoWritePermissionTooltipText(); - } - - const content = isWriteable - ? strings.getHideEditControlTooltip() - : strings.getShowEditControlTooltip(); - - return content; - }; - - const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { - const content = getEditToggleToolTipText(); - - if (textOnly) { - return content; - } - - return ( - - {content} - - ); - }; - - return ( - - - - {isWriteable && ( - - - - )} - - - - - - - - - - - - - - - {canUserWrite && ( - - )} - - - - - - - - - {fullscreenButton} - - - - - ); -}; +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; +} -WorkpadHeader.propTypes = { - isWriteable: PropTypes.bool, - toggleWriteable: PropTypes.func, - canUserWrite: PropTypes.bool, -}; +const mapStateToProps = (state: State): StateProps => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + canUserWrite: canUserWrite(state), + selectedPage: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => ({ + ...stateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), +}); + +export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.stories.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.stories.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.stories.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.stories.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__stories__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/index.ts b/x-pack/plugins/canvas/public/index.ts index c587623f2a0bc..d9a11204ad046 100644 --- a/x-pack/plugins/canvas/public/index.ts +++ b/x-pack/plugins/canvas/public/index.ts @@ -17,8 +17,4 @@ export interface WithKibanaProps { }; } -export interface UseKibanaProps { - canvas: CanvasServices; -} - -export const plugin = (initializerContext: PluginInitializerContext) => new CanvasPlugin(); +export const plugin = (_initializerContext: PluginInitializerContext) => new CanvasPlugin(); diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 9bd86ef98f1e3..9f79e81369b6b 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -12,7 +12,7 @@ import React, { FC, ReactElement, } from 'react'; -import { CanvasServices, CanvasServiceProviders } from '.'; +import { CanvasServices, CanvasServiceProviders, services } from '.'; export interface WithServicesProps { services: CanvasServices; @@ -36,23 +36,22 @@ export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; export const withServices = (type: ComponentType) => { - const EnhancedType: FC = (props) => { - const services = useServices(); - return createElement(type, { ...props, services }); - }; + const EnhancedType: FC = (props) => + createElement(type, { ...props, services: useServices() }); return EnhancedType; }; export const ServicesProvider: FC<{ - providers: CanvasServiceProviders; + providers?: Partial; children: ReactElement; -}> = ({ providers, children }) => { +}> = ({ providers = {}, children }) => { + const specifiedProviders: CanvasServiceProviders = { ...services, ...providers }; const value = { - embeddables: providers.embeddables.getService(), - expressions: providers.expressions.getService(), - notify: providers.notify.getService(), - platform: providers.platform.getService(), - navLink: providers.navLink.getService(), + embeddables: specifiedProviders.embeddables.getService(), + expressions: specifiedProviders.expressions.getService(), + notify: specifiedProviders.notify.getService(), + platform: specifiedProviders.platform.getService(), + navLink: specifiedProviders.navLink.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 3937d7fc05544..41d12db3a1853 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -31,7 +31,6 @@ @import '../components/function_form/function_form'; @import '../components/layout_annotations/layout_annotations'; @import '../components/loading/loading'; -@import '../components/navbar/navbar'; @import '../components/page_manager/page_manager'; @import '../components/positionable/positionable'; @import '../components/shape_preview/shape_preview'; diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index beea1814b54d2..671de53d74407 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -24,7 +24,7 @@ const storybookOptions = { run( ({ log, flags }) => { - const { dll, clean, stats, site } = flags; + const { addon, dll, clean, stats, site } = flags; // Delete the existing DLL if we're cleaning or building. if (clean || dll) { @@ -81,13 +81,20 @@ run( return; } + // Build the addon + execa.sync('node', ['scripts/build'], { + cwd: path.resolve(__dirname, '../storybook/addon'), + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }); + // Build site and exit if (site) { log.success('storybook: Generating Storybook site'); storybook({ ...storybookOptions, mode: 'static', - outputDir: path.resolve(__dirname, './../storybook'), + outputDir: path.resolve(__dirname, './../storybook/build'), }); return; } @@ -100,6 +107,14 @@ run( ...options, }); + if (addon) { + execa('node', ['scripts/build', '--watch'], { + cwd: path.resolve(__dirname, '../storybook/addon'), + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }); + } + storybook({ ...storybookOptions, port: 9001, @@ -110,8 +125,9 @@ run( Storybook runner for Canvas. `, flags: { - boolean: ['dll', 'clean', 'stats', 'site'], + boolean: ['addon', 'dll', 'clean', 'stats', 'site'], help: ` + --addon Watch the addon source code for changes. --clean Forces a clean of the Storybook DLL and exits. --dll Cleans and builds the Storybook dependency DLL and exits. --stats Produces a Webpack stats file. diff --git a/x-pack/plugins/canvas/server/lib/build_bool_array.js b/x-pack/plugins/canvas/server/lib/build_bool_array.ts similarity index 66% rename from x-pack/plugins/canvas/server/lib/build_bool_array.js rename to x-pack/plugins/canvas/server/lib/build_bool_array.ts index f1cab93ceebbb..bd418394cf375 100644 --- a/x-pack/plugins/canvas/server/lib/build_bool_array.js +++ b/x-pack/plugins/canvas/server/lib/build_bool_array.ts @@ -5,10 +5,11 @@ */ import { getESFilter } from './get_es_filter'; +import { ExpressionValueFilter } from '../../types'; -const compact = (arr) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); +const compact = (arr: T[]) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); -export function buildBoolArray(canvasQueryFilterArray) { +export function buildBoolArray(canvasQueryFilterArray: ExpressionValueFilter[]) { return compact( canvasQueryFilterArray.map((clause) => { try { diff --git a/x-pack/plugins/canvas/server/lib/filters.js b/x-pack/plugins/canvas/server/lib/filters.js deleted file mode 100644 index afa58c7ee30c2..0000000000000 --- a/x-pack/plugins/canvas/server/lib/filters.js +++ /dev/null @@ -1,38 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - TODO: This could be pluggable -*/ - -export function time(filter) { - if (!filter.column) { - throw new Error('column is required for Elasticsearch range filters'); - } - return { - range: { - [filter.column]: { gte: filter.from, lte: filter.to }, - }, - }; -} - -export function luceneQueryString(filter) { - return { - query_string: { - query: filter.query || '*', - }, - }; -} - -export function exactly(filter) { - return { - term: { - [filter.column]: { - value: filter.value, - }, - }, - }; -} diff --git a/x-pack/plugins/canvas/server/lib/filters.ts b/x-pack/plugins/canvas/server/lib/filters.ts new file mode 100644 index 0000000000000..9997640154e2c --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/filters.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FilterType, + ExpressionValueFilter, + CanvasTimeFilter, + CanvasLuceneFilter, + CanvasExactlyFilter, +} from '../../types'; + +/* + TODO: This could be pluggable +*/ + +const isTimeFilter = ( + maybeTimeFilter: ExpressionValueFilter +): maybeTimeFilter is CanvasTimeFilter => { + return maybeTimeFilter.filterType === FilterType.time; +}; +const isLuceneFilter = ( + maybeLuceneFilter: ExpressionValueFilter +): maybeLuceneFilter is CanvasLuceneFilter => { + return maybeLuceneFilter.filterType === FilterType.luceneQueryString; +}; +const isExactlyFilter = ( + maybeExactlyFilter: ExpressionValueFilter +): maybeExactlyFilter is CanvasExactlyFilter => { + return maybeExactlyFilter.filterType === FilterType.exactly; +}; + +export function time(filter: ExpressionValueFilter) { + if (!isTimeFilter(filter) || !filter.column) { + throw new Error('column is required for Elasticsearch range filters'); + } + return { + range: { + [filter.column]: { gte: filter.from, lte: filter.to }, + }, + }; +} + +export function luceneQueryString(filter: ExpressionValueFilter) { + if (!isLuceneFilter(filter)) { + throw new Error('Filter is not a lucene filter'); + } + return { + query_string: { + query: filter.query || '*', + }, + }; +} + +export function exactly(filter: ExpressionValueFilter) { + if (!isExactlyFilter(filter)) { + throw new Error('Filter is not an exactly filter'); + } + return { + term: { + [filter.column]: { + value: filter.value, + }, + }, + }; +} + +export const filters: Record = { + exactly, + time, + luceneQueryString, +}; diff --git a/x-pack/plugins/canvas/server/lib/get_es_filter.js b/x-pack/plugins/canvas/server/lib/get_es_filter.ts similarity index 75% rename from x-pack/plugins/canvas/server/lib/get_es_filter.js rename to x-pack/plugins/canvas/server/lib/get_es_filter.ts index 7c025ed8dee9b..acc222ecc376f 100644 --- a/x-pack/plugins/canvas/server/lib/get_es_filter.js +++ b/x-pack/plugins/canvas/server/lib/get_es_filter.ts @@ -10,11 +10,11 @@ filter is the abstracted canvas filter. */ -/*eslint import/namespace: ['error', { allowComputed: true }]*/ -import * as filters from './filters'; +import { filters } from './filters'; +import { ExpressionValueFilter } from '../../types'; -export function getESFilter(filter) { - if (!filters[filter.filterType]) { +export function getESFilter(filter: ExpressionValueFilter) { + if (!filter.filterType || !filters[filter.filterType]) { throw new Error(`Unknown filter type: ${filter.filterType}`); } diff --git a/x-pack/plugins/canvas/server/lib/normalize_type.js b/x-pack/plugins/canvas/server/lib/normalize_type.ts similarity index 89% rename from x-pack/plugins/canvas/server/lib/normalize_type.js rename to x-pack/plugins/canvas/server/lib/normalize_type.ts index fda2fbe631646..b684325aacba9 100644 --- a/x-pack/plugins/canvas/server/lib/normalize_type.js +++ b/x-pack/plugins/canvas/server/lib/normalize_type.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export function normalizeType(type) { - const normalTypes = { +export function normalizeType(type: string) { + const normalTypes: Record = { string: ['string', 'text', 'keyword', '_type', '_id', '_index', 'geo_point'], number: [ 'float', diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.js b/x-pack/plugins/canvas/server/lib/query_es_sql.js deleted file mode 100644 index 442703b00ea3a..0000000000000 --- a/x-pack/plugins/canvas/server/lib/query_es_sql.js +++ /dev/null @@ -1,59 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { map, zipObject } from 'lodash'; -import { buildBoolArray } from './build_bool_array'; -import { sanitizeName } from './sanitize_name'; -import { normalizeType } from './normalize_type'; - -export const queryEsSQL = (elasticsearchClient, { count, query, filter, timezone }) => - elasticsearchClient('transport.request', { - path: '/_sql?format=json', - method: 'POST', - body: { - query, - time_zone: timezone, - fetch_size: count, - client_id: 'canvas', - filter: { - bool: { - must: [{ match_all: {} }, ...buildBoolArray(filter)], - }, - }, - }, - }) - .then((res) => { - const columns = res.columns.map(({ name, type }) => { - return { name: sanitizeName(name), type: normalizeType(type) }; - }); - const columnNames = map(columns, 'name'); - const rows = res.rows.map((row) => zipObject(columnNames, row)); - - if (!!res.cursor) { - elasticsearchClient('transport.request', { - path: '/_sql/close', - method: 'POST', - body: { - cursor: res.cursor, - }, - }).catch((e) => { - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); - } - - return { - type: 'datatable', - columns, - rows, - }; - }) - .catch((e) => { - if (e.message.indexOf('parsing_exception') > -1) { - throw new Error( - `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` - ); - } - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts new file mode 100644 index 0000000000000..c3c122d1e301a --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { zipObject } from 'lodash'; +import { queryEsSQL } from './query_es_sql'; +// @ts-expect-error +import { buildBoolArray } from './build_bool_array'; + +const response = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [ + ['foo', 'bar'], + ['buz', 'baz'], + ], + cursor: 'cursor-value', +}; + +const baseArgs = { + count: 1, + query: 'query', + filter: [], + timezone: 'timezone', +}; + +const getApi = (resp = response) => { + const api = jest.fn(); + api.mockResolvedValue(resp); + return api; +}; + +describe('query_es_sql', () => { + it('should call the api with the given args', async () => { + const api = getApi(); + + queryEsSQL(api, baseArgs); + + expect(api).toHaveBeenCalled(); + const givenArgs = api.mock.calls[0][1]; + + expect(givenArgs.body.fetch_size).toBe(baseArgs.count); + expect(givenArgs.body.query).toBe(baseArgs.query); + expect(givenArgs.body.time_zone).toBe(baseArgs.timezone); + }); + + it('formats the response', async () => { + const api = getApi(); + + const result = await queryEsSQL(api, baseArgs); + + const expectedColumns = response.columns.map((c) => ({ name: c.name, type: 'string' })); + const columnNames = expectedColumns.map((c) => c.name); + const expectedRows = response.rows.map((r) => zipObject(columnNames, r)); + + expect(result.type).toBe('datatable'); + expect(result.columns).toEqual(expectedColumns); + expect(result.rows).toEqual(expectedRows); + }); + + it('fetches pages until it has the requested count', async () => { + const pageOne = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [['foo', 'bar']], + cursor: 'cursor-value', + }; + + const pageTwo = { + rows: [['buz', 'baz']], + }; + + const api = getApi(pageOne); + api.mockReturnValueOnce(pageOne).mockReturnValueOnce(pageTwo); + + const result = await queryEsSQL(api, { ...baseArgs, count: 2 }); + expect(result.rows).toHaveLength(2); + }); + + it('closes any cursors that remain open', async () => { + const api = getApi(); + + await queryEsSQL(api, baseArgs); + expect(api.mock.calls[1][1].body.cursor).toBe(response.cursor); + }); + + it('throws on errors', async () => { + const api = getApi(); + api.mockRejectedValueOnce(new Error('parsing_exception')); + api.mockRejectedValueOnce(new Error('generic es error')); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: parsing_exception"` + ); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unexpected error from Elasticsearch: generic es error"` + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.ts new file mode 100644 index 0000000000000..8639cfa31dca8 --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { map, zipObject } from 'lodash'; +import { buildBoolArray } from './build_bool_array'; +import { sanitizeName } from './sanitize_name'; +import { normalizeType } from './normalize_type'; +import { LegacyAPICaller } from '../../../../../src/core/server'; +import { ExpressionValueFilter } from '../../types'; + +interface Args { + count: number; + query: string; + timezone?: string; + filter: ExpressionValueFilter[]; +} + +interface CursorResponse { + cursor?: string; + rows: string[][]; +} + +type QueryResponse = CursorResponse & { + columns: Array<{ + name: string; + type: string; + }>; + cursor?: string; + rows: string[][]; +}; + +export const queryEsSQL = async ( + elasticsearchClient: LegacyAPICaller, + { count, query, filter, timezone }: Args +) => { + try { + let response: QueryResponse = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + query, + time_zone: timezone, + fetch_size: count, + client_id: 'canvas', + filter: { + bool: { + must: [{ match_all: {} }, ...buildBoolArray(filter)], + }, + }, + }, + }); + + const columns = response.columns.map(({ name, type }) => { + return { name: sanitizeName(name), type: normalizeType(type) }; + }); + const columnNames = map(columns, 'name'); + let rows = response.rows.map((row) => zipObject(columnNames, row)); + + while (rows.length < count && response.cursor !== undefined) { + response = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + + rows = [...rows, ...response.rows.map((row) => zipObject(columnNames, row))]; + } + + if (response.cursor !== undefined) { + elasticsearchClient('transport.request', { + path: '/_sql/close', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + } + + return { + type: 'datatable', + columns, + rows, + }; + } catch (e) { + if (e.message.indexOf('parsing_exception') > -1) { + throw new Error( + `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` + ); + } + throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); + } +}; diff --git a/x-pack/plugins/canvas/server/lib/sanitize_name.js b/x-pack/plugins/canvas/server/lib/sanitize_name.ts similarity index 85% rename from x-pack/plugins/canvas/server/lib/sanitize_name.js rename to x-pack/plugins/canvas/server/lib/sanitize_name.ts index 4c787c816a331..781ab20509b36 100644 --- a/x-pack/plugins/canvas/server/lib/sanitize_name.js +++ b/x-pack/plugins/canvas/server/lib/sanitize_name.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function sanitizeName(name) { +export function sanitizeName(name: string) { // invalid characters const invalid = ['(', ')']; const pattern = invalid.map((v) => escapeRegExp(v)).join('|'); @@ -12,6 +12,6 @@ export function sanitizeName(name) { return name.replace(regex, '_'); } -function escapeRegExp(string) { +function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts index 7a9830124e305..000b7f6029952 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts @@ -8,7 +8,6 @@ import { mapValues, keys } from 'lodash'; import { schema } from '@kbn/config-schema'; import { API_ROUTE } from '../../../common/lib'; import { catchErrorHandler } from '../catch_error_handler'; -// @ts-expect-error unconverted lib import { normalizeType } from '../../lib/normalize_type'; import { RouteInitializerDeps } from '..'; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/page.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/page.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/rendered_element.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/rendered_element.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/canvas.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/canvas.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/page.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/page.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/rendered_element.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__stories__/rendered_element.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/page_controls.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/scrubber.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/scrubber.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/title.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/footer.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/footer.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/page_controls.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/page_controls.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/scrubber.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/title.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/title.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.stories.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/autoplay_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/autoplay_settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.stories.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/toolbar_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.stories.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/toolbar_settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js index 10baaddfc9b05..e1db6e4a64f71 100644 --- a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line const autoprefixer = require('autoprefixer'); const prefixer = require('postcss-prefix-selector'); diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 93dc3dbccd549..43e422a161569 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -111,7 +111,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/x-pack/plugins/canvas/storybook/addon/babel.config.js b/x-pack/plugins/canvas/storybook/addon/babel.config.js new file mode 100644 index 0000000000000..5081cf455906f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/babel.config.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + presets: ['@kbn/babel-preset/webpack_preset'], + plugins: ['@babel/plugin-proposal-class-properties'], +}; diff --git a/x-pack/plugins/canvas/storybook/addon/scripts/build.js b/x-pack/plugins/canvas/storybook/addon/scripts/build.js new file mode 100644 index 0000000000000..b3525244fad25 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/scripts/build.js @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const { resolve } = require('path'); + +const del = require('del'); +const supportsColor = require('supports-color'); +const { run, withProcRunner } = require('@kbn/dev-utils'); + +const ROOT_DIR = resolve(__dirname, '..'); +const BUILD_DIR = resolve(ROOT_DIR, 'target'); + +const padRight = (width, str) => + str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; + +run( + async ({ log, flags }) => { + await withProcRunner(log, async (proc) => { + if (!flags.watch) { + log.info('Deleting old output'); + await del(BUILD_DIR); + } + + const cwd = ROOT_DIR; + const env = { process }; + + if (supportsColor.stdout) { + env.FORCE_COLOR = 'true'; + } + + log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); + await proc.run(padRight(10, `babel`), { + cmd: 'babel', + args: [ + 'src', + '--config-file', + require.resolve('../babel.config.js'), + '--out-dir', + BUILD_DIR, + '--extensions', + '.ts,.js,.tsx', + '--copy-files', + ...(flags.watch ? ['--watch'] : ['--quiet']), + ], + wait: true, + env, + cwd, + }); + + log.success('Complete'); + }); + }, + { + description: 'Simple build tool for Canvas Storybook addon', + flags: { + boolean: ['watch'], + help: ` + --watch Run in watch mode + `, + }, + } +); diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx new file mode 100644 index 0000000000000..9c29a44a67318 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import { EuiSelectable, EuiSelectableOption } from '@elastic/eui'; +import addons from '@storybook/addons'; +import uuid from 'uuid/v4'; + +import { EVENTS } from '../constants'; +import { RecordedAction, RecordedPayload } from '../types'; + +export const ActionList: FC<{ + onSelect: (action: RecordedAction | null) => void; +}> = ({ onSelect }) => { + const [recordedActions, setRecordedActions] = useState>({}); + const [selectedAction, setSelectedAction] = useState(null); + + useEffect(() => { + onSelect(selectedAction); + }, [onSelect, selectedAction]); + + useEffect(() => { + const actionListener = (newAction: RecordedPayload) => { + const id = uuid(); + setRecordedActions({ ...recordedActions, [id]: { ...newAction, id } }); + }; + + const resetListener = () => { + setSelectedAction(null); + setRecordedActions({}); + }; + + const channel = addons.getChannel(); + channel.addListener(EVENTS.ACTION, actionListener); + channel.addListener(EVENTS.RESET, resetListener); + + return () => { + channel.removeListener(EVENTS.ACTION, actionListener); + channel.removeListener(EVENTS.RESET, resetListener); + }; + }); + + useEffect(() => { + const values = Object.values(recordedActions); + if (values.length > 0) { + setSelectedAction(values[values.length - 1]); + } + }, [recordedActions]); + + const options: EuiSelectableOption[] = Object.values(recordedActions).map((recordedAction) => ({ + id: recordedAction.id, + key: recordedAction.id, + label: recordedAction.action.type, + checked: recordedAction.id === selectedAction?.id ? 'on' : undefined, + })); + + const onChange: (selectedOptions: EuiSelectableOption[]) => void = (selectedOptions) => { + selectedOptions.forEach((option) => { + if (option && option.checked && option.id) { + const selected = recordedActions[option.id]; + + if (selected) { + setSelectedAction(selected); + } + } + }); + }; + + return ( + + {(list) => list} + + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx new file mode 100644 index 0000000000000..351b94edb351f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { isObject, isDate } from 'lodash'; +import uuid from 'uuid/v4'; +import { EuiTreeView } from '@elastic/eui'; + +import { Node } from '@elastic/eui/src/components/tree_view/tree_view'; + +import { RecordedAction } from '../types'; + +const actionToTree = (recordedAction: RecordedAction) => { + const { action, newState, previousState } = recordedAction; + + return [ + { + label: 'Action', + id: uuid(), + children: jsonToTree(action), + }, + { + label: 'Previous State', + id: uuid(), + children: jsonToTree(previousState), + }, + { + label: 'Current State', + id: uuid(), + children: jsonToTree(newState), + }, + ]; +}; + +const jsonToTree: (obj: Record) => Node[] = (obj) => { + const keys = Object.keys(obj); + + const values = keys.map((label) => { + const value = obj[label]; + + if (!value) { + return null; + } + + const id = uuid(); + + if (isDate(value)) { + return { label: `${label}: ${(value as Date).toDateString()}` }; + } + + if (isObject(value)) { + const children = jsonToTree(value); + + if (children !== null && Object.keys(children).length > 0) { + return { label, id, children }; + } else { + return { label, id }; + } + } + + return { label: `${label}: ${value.toString().slice(0, 100)}`, id }; + }); + + return values.filter((value) => value !== null) as Node[]; +}; + +export const ActionTree: FC<{ action: RecordedAction | null }> = ({ action }) => { + const items = action ? actionToTree(action) : null; + let tree = <>; + + if (action && items) { + tree = ( + + ); + } else if (action) { + tree =
No change
; + } + + return tree; +}; diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts similarity index 66% rename from x-pack/plugins/lists/common/siem_common_deps.ts rename to x-pack/plugins/canvas/storybook/addon/src/components/index.ts index 2b37e2b7bf106..5acb1acf3b459 100644 --- a/x-pack/plugins/lists/common/siem_common_deps.ts +++ b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -// DEPRECATED: Do not add exports to this file; please import from shared_imports instead - -export * from './shared_imports'; +export { ActionList } from './action_list'; +export { ActionTree } from './action_tree'; diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx new file mode 100644 index 0000000000000..4db3c23c93843 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiAccordion } from '@elastic/eui'; +import { formatters } from 'jsondiffpatch'; + +import { RecordedAction } from '../types'; + +interface Props { + action: RecordedAction | null; +} + +export const StateChange: FC = ({ action }) => { + if (!action) { + return null; + } + + const { change, previousState } = action; + const html = formatters.html.format(change, previousState); + formatters.html.hideUnchanged(); + + return ( + + {/* eslint-disable-next-line react/no-danger */} +
+ + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/constants.ts b/x-pack/plugins/canvas/storybook/addon/src/constants.ts new file mode 100644 index 0000000000000..fb2646ef3ba8f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ADDON_ID = 'kbn-canvas/redux-actions'; +export const ACTIONS_PANEL_ID = `${ADDON_ID}/panel`; + +const RESULT = `${ADDON_ID}/result`; +const REQUEST = `${ADDON_ID}/request`; +const ACTION = `${ADDON_ID}/action`; +const RESET = `${ADDON_ID}/reset`; + +export const EVENTS = { ACTION, RESULT, REQUEST, RESET }; diff --git a/x-pack/plugins/canvas/storybook/addon/src/panel.css b/x-pack/plugins/canvas/storybook/addon/src/panel.css new file mode 100644 index 0000000000000..b2b6591343b5f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/panel.css @@ -0,0 +1,171 @@ +.panel__tree { + font-family: monospace; + font-size: 85%; +} + +.panel__tree .euiTreeView { + padding-left: 12px; + font-size: 85%; +} + +.panel__resizeableContainer { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; +} + +.panel__stateChange .euiAccordion__button { + font-size: 12px; + font-family: monospace; +} + +.panel__stateChange .euiAccordion__iconWrapper { + transform: scale(.80); + transform-origin: top left; + margin: 8px 0px 4px 7px; +} + +.jsondiffpatch-delta { + font-family: monospace; + font-size: 12px; + line-height: 20px; + margin: 0; + padding: 0 0 0 12px; + display: inline-block; +} +.jsondiffpatch-delta pre { + font-size: 12px; + margin: 0; + padding: 0; + display: inline-block; +} +ul.jsondiffpatch-delta { + list-style-type: none; + padding: 0 0 0 20px; + margin: 0; +} +.jsondiffpatch-delta ul { + list-style-type: none; + padding: 0 0 0 20px; + margin: 0; +} + +.jsondiffpatch-added .jsondiffpatch-property-name, +.jsondiffpatch-added .jsondiffpatch-value pre, +.jsondiffpatch-modified .jsondiffpatch-right-value pre, +.jsondiffpatch-textdiff-added { + background: #bbffbb; +} + +.jsondiffpatch-deleted .jsondiffpatch-property-name, +.jsondiffpatch-deleted pre, +.jsondiffpatch-modified .jsondiffpatch-left-value pre, +.jsondiffpatch-textdiff-deleted { + background: #ffbbbb; + text-decoration: line-through; +} + +.jsondiffpatch-unchanged { display: none; } + +.jsondiffpatch-value { + display: inline-block; +} + +.jsondiffpatch-property-name { + display: inline-block; + padding-right: 5px; + vertical-align: top; +} + +.jsondiffpatch-property-name:after { + content: ': '; +} + +.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after { + content: ': ['; +} + +.jsondiffpatch-child-node-type-array:after { + content: '],'; +} + +div.jsondiffpatch-child-node-type-array:before { + content: '['; +} + +div.jsondiffpatch-child-node-type-array:after { + content: ']'; +} + +.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after { + content: ': {'; +} + +.jsondiffpatch-child-node-type-object:after { + content: '},'; +} + +div.jsondiffpatch-child-node-type-object:before { + content: '{'; +} + +div.jsondiffpatch-child-node-type-object:after { + content: '}'; +} + +.jsondiffpatch-value pre:after { + content: ','; +} + +li:last-child > .jsondiffpatch-value pre:after, +.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after { + content: ''; +} + +.jsondiffpatch-modified .jsondiffpatch-value { + display: inline-block; +} + +.jsondiffpatch-modified .jsondiffpatch-right-value { + margin-left: 5px; +} + +.jsondiffpatch-moved .jsondiffpatch-value { + display: none; +} + +.jsondiffpatch-moved .jsondiffpatch-moved-destination { + display: inline-block; + background: #ffffbb; + color: #888; +} + +.jsondiffpatch-moved .jsondiffpatch-moved-destination:before { + content: ' => '; +} + +ul.jsondiffpatch-textdiff { + padding: 0; +} + +.jsondiffpatch-textdiff-location { + color: #bbb; + display: inline-block; + min-width: 60px; +} + +.jsondiffpatch-textdiff-line { + display: inline-block; +} + +.jsondiffpatch-textdiff-line-number:after { + content: ','; +} + +.jsondiffpatch-error { + background: red; + color: white; + font-weight: bold; +} diff --git a/x-pack/plugins/canvas/storybook/addon/src/panel.tsx b/x-pack/plugins/canvas/storybook/addon/src/panel.tsx new file mode 100644 index 0000000000000..adf6e8555c00a --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/panel.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiResizableContainer } from '@elastic/eui'; +import { StateChange } from './components/state_change'; + +import '@elastic/eui/dist/eui_theme_light.css'; +import './panel.css'; + +import { RecordedAction } from './types'; +import { ActionList, ActionTree } from './components'; + +export const Panel = () => { + const [selectedAction, setSelectedAction] = useState(null); + + return ( + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/register.tsx b/x-pack/plugins/canvas/storybook/addon/src/register.tsx new file mode 100644 index 0000000000000..3a5c4a6818ac1 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/register.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import React from 'react'; +import { addons, types } from '@storybook/addons'; +import { AddonPanel } from '@storybook/components'; +import { STORY_CHANGED } from '@storybook/core-events'; + +import { ADDON_ID, EVENTS, ACTIONS_PANEL_ID } from './constants'; +import { Panel } from './panel'; + +addons.register(ADDON_ID, (api) => { + const channel = addons.getChannel(); + + api.on(STORY_CHANGED, (storyId) => { + channel.emit(EVENTS.RESET, storyId); + }); + + addons.add(ACTIONS_PANEL_ID, { + title: 'Redux Actions', + type: types.PANEL, + render: ({ active, key }) => { + return ( + + + + ); + }, + }); +}); diff --git a/x-pack/plugins/canvas/storybook/addon/src/state.ts b/x-pack/plugins/canvas/storybook/addon/src/state.ts new file mode 100644 index 0000000000000..6d601fff7184a --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/state.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* es-lint-disable import/no-extraneous-dependencies */ +import { applyMiddleware, Dispatch, Store } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import addons from '@storybook/addons'; +import { diff } from 'jsondiffpatch'; +import { isFunction } from 'lodash'; + +import { EVENTS } from './constants'; + +// @ts-expect-error untyped local +import { appReady } from '../../../public/state/middleware/app_ready'; +// @ts-expect-error untyped local +import { resolvedArgs } from '../../../public/state/middleware/resolved_args'; + +// @ts-expect-error untyped local +import { getRootReducer } from '../../../public/state/reducers'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../../public/state/defaults'; +// @ts-expect-error Untyped local +import { getInitialState as getState } from '../../../public/state/initial_state'; +import { State } from '../../../types'; + +export const getInitialState: () => State = () => getState(); +export const getMiddleware = () => applyMiddleware(thunkMiddleware); +export const getReducer = () => getRootReducer(getInitialState()); + +export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( + action +) => { + const channel = addons.getChannel(); + + const previousState = store.getState(); + const returnValue = dispatch(action); + const newState = store.getState(); + const change = diff(previousState, newState) || {}; + + channel.emit(EVENTS.ACTION, { + previousState, + newState, + change, + action: isFunction(action) ? { type: '(thunk)' } : action, + }); + + return returnValue; +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/types.ts b/x-pack/plugins/canvas/storybook/addon/src/types.ts new file mode 100644 index 0000000000000..e8a2cb70c89ff --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { State } from '../../../types'; + +export interface RecordedPayload { + previousState: State; + newState: State; + change: Partial; + action: Action; +} + +export interface RecordedAction extends RecordedPayload { + id: string; +} diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json new file mode 100644 index 0000000000000..9cab0af235f2e --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.json", + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "exclude": [ + "target" + ], + "compilerOptions": { + "declaration": false, + } +} diff --git a/x-pack/plugins/canvas/storybook/config.js b/x-pack/plugins/canvas/storybook/config.js deleted file mode 100644 index dc16d6c46084d..0000000000000 --- a/x-pack/plugins/canvas/storybook/config.js +++ /dev/null @@ -1,73 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { configure, addDecorator, addParameters } from '@storybook/react'; -import { withInfo } from '@storybook/addon-info'; -import { create } from '@storybook/theming'; - -import { startServices } from '../public/services/stubs'; -import { addDecorators } from './decorators'; - -// If we're running Storyshots, be sure to register the require context hook. -// Otherwise, add the other decorators. -if (process.env.NODE_ENV === 'test') { - require('babel-plugin-require-context-hook/register')(); -} else { - // Customize the info for each story. - addDecorator( - withInfo({ - inline: true, - styles: { - infoBody: { - margin: 20, - }, - infoStory: { - margin: '40px 60px', - }, - }, - }) - ); -} - -addDecorators(); -startServices(); - -function loadStories() { - require('./dll_contexts'); - - // Only gather and require CSS files related to Canvas. The other CSS files - // are built into the DLL. - const css = require.context( - '../../../../built_assets/css', - true, - /plugins\/(?=canvas).*light\.css/ - ); - css.keys().forEach((filename) => css(filename)); - - // Find all files ending in *.stories.tsx - const req = require.context('./..', true, /.(stories).tsx$/); - req.keys().forEach((filename) => req(filename)); - - // Import Canvas CSS - require('../public/style/index.scss'); -} - -// Set up the Storybook environment with custom settings. -addParameters({ - options: { - theme: create({ - base: 'light', - brandTitle: 'Canvas Storybook', - brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', - }), - showPanel: true, - isFullscreen: false, - panelPosition: 'bottom', - isToolshown: true, - }, -}); - -configure(loadStories, module); diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts index aa1e958a410f5..8cd716cf7e3f1 100644 --- a/x-pack/plugins/canvas/storybook/decorators/index.ts +++ b/x-pack/plugins/canvas/storybook/decorators/index.ts @@ -5,15 +5,43 @@ */ import { addDecorator } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; // @ts-expect-error import { withInfo } from '@storybook/addon-info'; +import { Provider as ReduxProvider } from 'react-redux'; + +import { ServicesProvider } from '../../public/services'; +import { RouterContext } from '../../public/components/router'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { routerContextDecorator } from './router_decorator'; import { kibanaContextDecorator } from './kibana_decorator'; +import { servicesContextDecorator } from './services_decorator'; + +export { reduxDecorator } from './redux_decorator'; export const addDecorators = () => { - addDecorator(withKnobs); + if (process.env.NODE_ENV === 'test') { + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('babel-plugin-require-context-hook/register')(); + } else { + // Customize the info for each story. + addDecorator( + withInfo({ + inline: true, + styles: { + infoBody: { + margin: 20, + }, + infoStory: { + margin: '40px 60px', + }, + }, + propTablesExclude: [ReduxProvider, ServicesProvider, RouterContext, KibanaContextProvider], + }) + ); + } + addDecorator(kibanaContextDecorator); addDecorator(routerContextDecorator); + addDecorator(servicesContextDecorator); }; diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx new file mode 100644 index 0000000000000..e35b065a61764 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* es-lint-disable import/no-extraneous-dependencies */ + +import React from 'react'; +import { createStore } from 'redux'; +import { Provider as ReduxProvider } from 'react-redux'; +import { cloneDeep } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../public/state/defaults'; +import { CanvasWorkpad, CanvasElement, CanvasAsset } from '../../types'; + +// @ts-expect-error untyped local +import { elementsRegistry } from '../../public/lib/elements_registry'; +import { image } from '../../canvas_plugin_src/elements/image'; +elementsRegistry.register(image); + +import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state'; +export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants'; + +interface Params { + workpad?: CanvasWorkpad; + elements?: CanvasElement[]; + assets?: CanvasAsset[]; +} + +export const reduxDecorator = (params: Params = {}) => { + const state = cloneDeep(getInitialState()); + const { workpad, elements, assets } = params; + + if (workpad) { + set(state, 'persistent.workpad', workpad); + } + + if (elements) { + set(state, 'persistent.workpad.pages.0.elements', elements); + } + + if (assets) { + set( + state, + 'assets', + assets.reduce((obj: Record, item) => { + obj[item.id] = item; + return obj; + }, {}) + ); + } + + return (story: Function) => { + const store = createStore(getReducer(), state, getMiddleware()); + store.dispatch = patchDispatch(store, store.dispatch); + return {story()}; + }; +}; diff --git a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx index 43b0da6473f23..464577b1f7c1e 100644 --- a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx @@ -5,26 +5,9 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -class RouterContext extends React.Component { - static childContextTypes = { - router: PropTypes.object.isRequired, - }; +import { RouterContext } from '../../public/components/router'; - getChildContext() { - return { - router: { - getFullPath: () => 'path', - create: () => '', - }, - }; - } - render() { - return <>{this.props.children}; - } -} - -export function routerContextDecorator(story: Function) { - return {story()}; -} +export const routerContextDecorator = (story: Function) => ( + {} }}>{story()} +); diff --git a/x-pack/plugins/canvas/public/components/navbar/navbar.js b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx similarity index 58% rename from x-pack/plugins/canvas/public/components/navbar/navbar.js rename to x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx index dcf6389acd4a3..918eaffb47d77 100644 --- a/x-pack/plugins/canvas/public/components/navbar/navbar.js +++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx @@ -5,12 +5,9 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -export const Navbar = ({ children }) => { - return
{children}
; -}; +import { ServicesProvider } from '../../public/services'; -Navbar.propTypes = { - children: PropTypes.node, -}; +export const servicesContextDecorator = (story: Function) => ( + {story()} +); diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts new file mode 100644 index 0000000000000..5cad89eb614e5 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ACTIONS_PANEL_ID } from './addon/src/constants'; + +export * from './decorators'; +export { ACTIONS_PANEL_ID } from './addon/src/constants'; +export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } }); diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts new file mode 100644 index 0000000000000..ad6d10f9bc75f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/main.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + stories: ['../**/*.stories.tsx'], + addons: [ + '@storybook/addon-actions', + '@storybook/addon-knobs', + './storybook/addon/target/register', + ], +}; diff --git a/x-pack/plugins/canvas/storybook/manager.ts b/x-pack/plugins/canvas/storybook/manager.ts new file mode 100644 index 0000000000000..6727040c9b27f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/manager.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addons } from '@storybook/addons'; +import { create } from '@storybook/theming'; +import { PANEL_ID } from '@storybook/addon-actions'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Canvas Storybook', + brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', + }), + showPanel: true, + isFullscreen: false, + panelPosition: 'bottom', + isToolshown: true, + selectedPanel: PANEL_ID, +}); diff --git a/x-pack/plugins/canvas/storybook/middleware.js b/x-pack/plugins/canvas/storybook/middleware.ts similarity index 74% rename from x-pack/plugins/canvas/storybook/middleware.js rename to x-pack/plugins/canvas/storybook/middleware.ts index baa524aefa709..d319a6918a02a 100644 --- a/x-pack/plugins/canvas/storybook/middleware.js +++ b/x-pack/plugins/canvas/storybook/middleware.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -const path = require('path'); -const serve = require('serve-static'); +import path from 'path'; +// @ts-expect-error +import serve from 'serve-static'; // Extend the Storybook Middleware to include a route to access Legacy UI assets -module.exports = function (router) { +module.exports = function (router: { get: (...args: any[]) => void }) { router.get( '/ui', serve(path.resolve(__dirname, '../../../../../src/core/server/core_app/assets')) diff --git a/x-pack/plugins/canvas/storybook/preview.ts b/x-pack/plugins/canvas/storybook/preview.ts new file mode 100644 index 0000000000000..fc194664c84b8 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/preview.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; + +import { startServices } from '../public/services/stubs'; +import { addDecorators } from './decorators'; + +// Import the modules from the DLL. +import './dll_contexts'; + +// Import Canvas CSS +import '../public/style/index.scss'; + +startServices({ + notify: { + success: (message) => action(`success: ${message}`)(), + error: (message) => action(`error: ${message}`)(), + info: (message) => action(`info: ${message}`)(), + warning: (message) => action(`warning: ${message}`)(), + }, +}); + +addDecorators(); + +// Only gather and require CSS files related to Canvas. The other CSS files +// are built into the DLL. +const css = require.context( + '../../../../built_assets/css', + true, + /plugins\/(?=canvas).*light\.css/ +); +css.keys().forEach((filename) => css(filename)); diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.js b/x-pack/plugins/canvas/storybook/storyshots.test.tsx similarity index 83% rename from x-pack/plugins/canvas/storybook/storyshots.test.js rename to x-pack/plugins/canvas/storybook/storyshots.test.tsx index e3a9654bb49fa..b51a85edaa67b 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReactChildren } from 'react'; import path from 'path'; import moment from 'moment'; import 'moment-timezone'; import ReactDOM from 'react-dom'; import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; +// @ts-expect-error untyped library import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; import { addSerializer } from 'jest-specific-snapshot'; // Several of the renderers, used by the runtime, use jQuery. import jquery from 'jquery'; +// @ts-expect-error jQuery global global.$ = jquery; +// @ts-expect-error jQuery global global.jQuery = jquery; // Set our default timezone to UTC for tests so we can generate predictable snapshots @@ -23,7 +27,7 @@ moment.tz.setDefault('UTC'); // Freeze time for the tests for predictable snapshots const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 -Date.now = jest.fn(() => testTime); +Date.now = jest.fn(() => testTime.getTime()); // Mock telemetry service jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => {} })); @@ -53,10 +57,10 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => { }); // Mock React Portal for components that use modals, tooltips, etc -ReactDOM.createPortal = jest.fn((element) => { - return element; -}); +// @ts-expect-error Portal mocks are notoriously difficult to type +ReactDOM.createPortal = jest.fn((element) => element); +// Mock the EUI HTML ID Generator so elements have a predictable ID in snapshots jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { htmlIdGenerator: () => () => `generated-id`, @@ -67,18 +71,19 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { // https://github.com/elastic/eui/issues/3712 jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { return { - EuiOverlayMask: ({ children }) => children, + EuiOverlayMask: ({ children }: { children: ReactChildren }) => children, }; }); // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( - '../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories', + '../public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories', () => { return 'Disabled Panel'; } ); +// @ts-expect-error untyped library import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; jest.mock('@elastic/eui/test-env/components/observer/observer'); EuiObserver.mockImplementation(() => 'EuiObserver'); @@ -86,6 +91,7 @@ EuiObserver.mockImplementation(() => 'EuiObserver'); // This element uses a `ref` and cannot be rendered by Jest snapshots. import { RenderedElement } from '../shareable_runtime/components/rendered_element'; jest.mock('../shareable_runtime/components/rendered_element'); +// @ts-expect-error RenderedElement.mockImplementation(() => 'RenderedElement'); addSerializer(styleSheetSerializer); @@ -94,5 +100,6 @@ addSerializer(styleSheetSerializer); initStoryshots({ configPath: path.resolve(__dirname, './../storybook'), test: multiSnapshotWithOptions({}), + // Don't snapshot tests that start with 'redux' storyNameRegex: /^((?!.*?redux).)*$/, }); diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index 927f71b832ba0..1321ade30bbde 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -6,232 +6,198 @@ const path = require('path'); const webpack = require('webpack'); +const webpackMerge = require('webpack-merge'); const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); // Extend the Storybook Webpack config with some customizations -module.exports = async ({ config }) => { - // Find and alter the CSS rule to replace the Kibana public path string with a path - // to the route we've added in middleware.js - const cssRule = config.module.rules.find((rule) => rule.test.source.includes('.css$')); - cssRule.use.push({ - loader: 'string-replace-loader', - options: { - search: '__REPLACE_WITH_PUBLIC_PATH__', - replace: '/', - flags: 'g', - }, - }); - - // Include the React preset from Kibana for Storybook JS files. - config.module.rules.push({ - test: /\.js$/, - exclude: /node_modules/, - loaders: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }); - - // Handle Typescript files - config.module.rules.push({ - test: /\.tsx?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }, - ], - }); - - config.module.rules.push({ - test: /\.mjs$/, - include: /node_modules/, - type: 'javascript/auto', - }); - - // Parse props data for .tsx files - // This is notoriously slow, and is making Storybook unusable. Disabling for now. - // See: https://github.com/storybookjs/storybook/issues/7998 - // - // config.module.rules.push({ - // test: /\.tsx$/, - // // Exclude example files, as we don't display props info for them - // exclude: /\.examples.tsx$/, - // use: [ - // // Parse TS comments to create Props tables in the UI - // require.resolve('react-docgen-typescript-loader'), - // ], - // }); - - // Enable SASS, but exclude CSS Modules in Storybook - config.module.rules.push({ - test: /\.scss$/, - exclude: /\.module.(s(a|c)ss)$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader', options: { importLoaders: 2 } }, - { - loader: 'postcss-loader', - options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), - }, - }, - { - loader: 'sass-loader', - options: { - prependData(loaderContext) { - return `@import ${stringifyRequest( - loaderContext, - path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') - )};\n`; - }, - sassOptions: { - includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], +module.exports = async ({ config: storybookConfig }) => { + const config = { + module: { + rules: [ + // Include the React preset from Kibana for JS(X) and TS(X) + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + loaders: 'babel-loader', + options: { + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], }, }, - }, - ], - }); - - // Enable CSS Modules in Storybook - config.module.rules.push({ - test: /\.module\.s(a|c)ss$/, - loader: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - modules: { - localIdentName: '[name]__[local]___[hash:base64:5]', - }, + // Parse props data for .tsx files + // This is notoriously slow, and is making Storybook unusable. Disabling for now. + // See: https://github.com/storybookjs/storybook/issues/7998 + // + // { + // test: /\.tsx$/, + // // Exclude example files, as we don't display props info for them + // exclude: /\.examples.tsx$/, + // use: [ + // // Parse TS comments to create Props tables in the UI + // require.resolve('react-docgen-typescript-loader'), + // ], + // }, + // Enable SASS, but exclude CSS Modules in Storybook + { + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') + )};\n`; + }, + sassOptions: { + includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], + }, + }, + }, + ], }, - }, - { - loader: 'postcss-loader', - options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + // Enable CSS Modules in Storybook (Shareable Runtime) + { + test: /\.module\.s(a|c)ss$/, + loader: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2, + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + }, + ], }, - }, - { - loader: 'sass-loader', - }, - ], - }); - - // Exclude large-dependency modules that need not be included in Storybook. - config.module.rules.push({ - test: [ - path.resolve(__dirname, '../public/components/embeddable_flyout'), - path.resolve(__dirname, '../../reporting/public'), - ], - use: 'null-loader', - }); - - // Ensure jQuery is global for Storybook, specifically for the runtime. - config.plugins.push( - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery', - }) - ); - - // Reference the built DLL file of static(ish) dependencies, which are removed - // during kbn:bootstrap and rebuilt if missing. - config.plugins.push( - new webpack.DllReferencePlugin({ - manifest: path.resolve(DLL_OUTPUT, 'manifest.json'), - context: KIBANA_ROOT, - }) - ); - - // Copy the DLL files to the Webpack build for use in the Storybook UI - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ { - from: path.resolve(DLL_OUTPUT, 'dll.js'), - to: 'dll.js', + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', }, + // Exclude large-dependency, troublesome or irrelevant modules. { - from: path.resolve(DLL_OUTPUT, 'dll.css'), - to: 'dll.css', + test: [ + path.resolve(__dirname, '../public/components/embeddable_flyout'), + path.resolve(__dirname, '../../reporting/public'), + path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/angular'), + path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/paginate'), + ], + use: 'null-loader', }, ], - }) - ); - - config.plugins.push( - // replace imports for `uiExports/*` modules with a synthetic module - // created by create_ui_exports_module.js - new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { - // uiExports used by Canvas - const extensions = { - hacks: [], - chromeNavControls: [], - }; - - // everything following the first / in the request is - // treated as a type of appExtension - const type = resource.request.slice(resource.request.indexOf('/') + 1); - - resource.request = [ - // the "val-loader" is used to execute create_ui_exports_module - // and use its return value as the source for the module in the - // bundle. This allows us to bypass writing to the file system - require.resolve('val-loader'), - '!', - require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), - '?', - // this JSON is parsed by create_ui_exports_module and determines - // what require() calls it will execute within the bundle - JSON.stringify({ type, modules: extensions[type] || [] }), - ].join(''); - }), - - // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/notify/, - path.resolve(__dirname, '../tasks/mocks/uiNotify') - ), - new webpack.NormalModuleReplacementPlugin( - /lib\/download_workpad/, - path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/custom_element_service/, - path.resolve(__dirname, '../tasks/mocks/customElementService') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/ui_metric/, - path.resolve(__dirname, '../tasks/mocks/uiMetric') - ) - ); - - // Tell Webpack about relevant extensions - config.resolve.extensions.push('.ts', '.tsx', '.scss'); - - // Alias imports to either a mock or the proper module or directory. - // NOTE: order is important here - `ui/notify` will override `ui/notify/foo` if it - // is added first. - config.resolve.alias['ui/notify/lib/format_msg'] = path.resolve( - __dirname, - '../tasks/mocks/uiNotifyFormatMsg' - ); - config.resolve.alias['ui/notify'] = path.resolve(__dirname, '../tasks/mocks/uiNotify'); - config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve( - __dirname, - '../tasks/mocks/uiAbsoluteToParsedUrl' - ); - config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome'); - config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'); - config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'); + }, + plugins: [ + // Reference the built DLL file of static(ish) dependencies, which are removed + // during kbn:bootstrap and rebuilt if missing. + new webpack.DllReferencePlugin({ + manifest: path.resolve(DLL_OUTPUT, 'manifest.json'), + context: KIBANA_ROOT, + }), + // Ensure jQuery is global for Storybook, specifically for the runtime. + new webpack.ProvidePlugin({ + $: 'jquery', + jQuery: 'jquery', + }), + // Copy the DLL files to the Webpack build for use in the Storybook UI + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(DLL_OUTPUT, 'dll.js'), + to: 'dll.js', + }, + { + from: path.resolve(DLL_OUTPUT, 'dll.css'), + to: 'dll.css', + }, + ], + }), + // replace imports for `uiExports/*` modules with a synthetic module + // created by create_ui_exports_module.js + new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { + // uiExports used by Canvas + const extensions = { + hacks: [], + chromeNavControls: [], + }; + + // everything following the first / in the request is + // treated as a type of appExtension + const type = resource.request.slice(resource.request.indexOf('/') + 1); + + resource.request = [ + // the "val-loader" is used to execute create_ui_exports_module + // and use its return value as the source for the module in the + // bundle. This allows us to bypass writing to the file system + require.resolve('val-loader'), + '!', + require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), + '?', + // this JSON is parsed by create_ui_exports_module and determines + // what require() calls it will execute within the bundle + JSON.stringify({ type, modules: extensions[type] || [] }), + ].join(''); + }), + + new webpack.NormalModuleReplacementPlugin( + /lib\/download_workpad/, + path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/custom_element_service/, + path.resolve(__dirname, '../tasks/mocks/customElementService') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/ui_metric/, + path.resolve(__dirname, '../tasks/mocks/uiMetric') + ), + ], + resolve: { + extensions: ['.ts', '.tsx', '.scss', '.mjs', '.html'], + alias: { + 'ui/url/absolute_to_parsed_url': path.resolve( + __dirname, + '../tasks/mocks/uiAbsoluteToParsedUrl' + ), + ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'), + ng_mock$: path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'), + }, + }, + }; - config.resolve.extensions.push('.mjs'); + // Find and alter the CSS rule to replace the Kibana public path string with a path + // to the route we've added in middleware.js + const cssRule = storybookConfig.module.rules.find((rule) => rule.test.source.includes('.css$')); + cssRule.use.push({ + loader: 'string-replace-loader', + options: { + search: '__REPLACE_WITH_PUBLIC_PATH__', + replace: '/', + flags: 'g', + }, + }); - return config; + return webpackMerge(storybookConfig, config); }; diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 0e9371e4cb5e4..4e54750f08eea 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -25,9 +25,6 @@ module.exports = { '@elastic/eui/dist/eui_theme_light.css', '@kbn/ui-framework/dist/kui_light.css', '@storybook/addon-actions/register', - '@storybook/addon-knobs', - '@storybook/addon-knobs/react', - '@storybook/addon-knobs/register', '@storybook/core', '@storybook/core/dist/server/common/polyfills.js', '@storybook/react', @@ -38,6 +35,7 @@ module.exports = { 'chroma-js', 'highlight.js', 'html-entities', + 'jsondiffpatch', 'jquery', 'lodash', 'markdown-it', @@ -114,7 +112,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/x-pack/plugins/canvas/types/filters.ts b/x-pack/plugins/canvas/types/filters.ts new file mode 100644 index 0000000000000..356ebbbb76ac0 --- /dev/null +++ b/x-pack/plugins/canvas/types/filters.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExpressionValueFilter } from '.'; + +export enum FilterType { + luceneQueryString = 'luceneQueryString', + time = 'time', + exactly = 'exactly', +} + +export type CanvasTimeFilter = ExpressionValueFilter & { + filterType: typeof FilterType.time; + to: string; + from: string; +}; + +export type CanvasLuceneFilter = ExpressionValueFilter & { + filterType: typeof FilterType.luceneQueryString; + query: string; +}; + +export type CanvasExactlyFilter = ExpressionValueFilter & { + filterType: typeof FilterType.exactly; + value: string; + column: string; +}; + +export type CanvasFilter = CanvasTimeFilter | CanvasExactlyFilter | CanvasLuceneFilter; diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 0799627ce9b5a..f39c2d4367f9e 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -8,6 +8,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './filters'; export * from './functions'; export * from './renderers'; export * from './shortcuts'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 434d38c76d428..4ddcb3386f314 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; -import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public'; @@ -18,7 +17,6 @@ export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; export interface PluginDeps { discover: Pick; - embeddable: Pick; kibanaLegacy?: { dashboardConfig: { getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls']; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index 14cd48ae1f509..b6bdafc26b445 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -8,19 +8,14 @@ import { ExploreDataChartAction } from './explore_data_chart_action'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; -import { - EmbeddableStart, - RangeSelectContext, - ValueClickContext, - ChartActionContext, -} from '../../../../../../src/plugins/embeddable/public'; +import { ExploreDataChartActionContext } from './explore_data_chart_action'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, VISUALIZE_EMBEDDABLE_TYPE, } from '../../../../../../src/plugins/visualizations/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -import { Filter, TimeRange } from '../../../../../../src/plugins/data/public'; +import { Filter, RangeFilter } from '../../../../../../src/plugins/data/public'; const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; @@ -34,10 +29,19 @@ afterEach(() => { i18nTranslateSpy.mockClear(); }); -const setup = ({ - useRangeEvent = false, - dashboardOnlyMode = false, -}: { useRangeEvent?: boolean; dashboardOnlyMode?: boolean } = {}) => { +const setup = ( + { + useRangeEvent = false, + timeFieldName, + filters = [], + dashboardOnlyMode = false, + }: { + useRangeEvent?: boolean; + filters?: Filter[]; + timeFieldName?: string; + dashboardOnlyMode?: boolean; + } = { filters: [] } +) => { type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; const core = coreMock.createStart(); @@ -46,17 +50,10 @@ const setup = ({ createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; - const filtersAndTimeRangeFromContext = jest.fn((async () => ({ - filters: [], - })) as EmbeddableStart['filtersAndTimeRangeFromContext']); - const plugins: PluginDeps = { discover: { urlGenerator, }, - embeddable: { - filtersAndTimeRangeFromContext, - }, kibanaLegacy: { dashboardConfig: { getHideWriteControls: () => dashboardOnlyMode, @@ -91,19 +88,13 @@ const setup = ({ getOutput: () => output, } as unknown) as VisualizeEmbeddableContract; - const data: ChartActionContext['data'] = { - ...(useRangeEvent - ? ({ range: {} } as RangeSelectContext['data']) - : ({ data: [] } as ValueClickContext['data'])), - timeFieldName: 'order_date', - }; - const context = { + filters, + timeFieldName, embeddable, - data, - } as ChartActionContext; + } as ExploreDataChartActionContext; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context }; + return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -236,32 +227,41 @@ describe('"Explore underlying data" panel action', () => { }); test('applies chart event filters', async () => { - const { action, context, urlGenerator, plugins } = setup(); - - ((plugins.embeddable - .filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => { - const filters: Filter[] = [ - { - meta: { - alias: 'alias', - disabled: false, - negate: false, + const timeFieldName = 'timeField'; + const from = '2020-07-13T13:40:43.583Z'; + const to = '2020-07-13T13:44:43.583Z'; + const filters: Array = [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + field: timeFieldName, + params: { + gte: from, + lte: to, }, }, - ]; - const timeRange: TimeRange = { - from: 'from', - to: 'to', - }; - return { filters, timeRange }; - }); + range: { + [timeFieldName]: { + gte: from, + lte: to, + }, + }, + }, + ]; - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0); + const { action, context, urlGenerator } = setup({ filters, timeFieldName }); await action.getHref(context); - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1); - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context); expect(urlGenerator.createUrl).toHaveBeenCalledWith({ filters: [ { @@ -274,8 +274,8 @@ describe('"Explore underlying data" panel action', () => { ], indexPatternId: 'index-ptr-foo', timeRange: { - from: 'from', - to: 'to', + from, + to, }, }); }); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 658a6bcb3cf4d..a89fe3cd12a19 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -5,17 +5,19 @@ */ import { Action } from '../../../../../../src/plugins/ui_actions/public'; -import { - ValueClickContext, - RangeSelectContext, -} from '../../../../../../src/plugins/embeddable/public'; import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; -import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { + isTimeRange, + isQuery, + isFilters, + ApplyGlobalFilterActionContext, + esFilters, +} from '../../../../../../src/plugins/data/public'; import { KibanaURL } from './kibana_url'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; -export type ExploreDataChartActionContext = ValueClickContext | RangeSelectContext; +export type ExploreDataChartActionContext = ApplyGlobalFilterActionContext; export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; @@ -31,6 +33,11 @@ export class ExploreDataChartAction extends AbstractExploreDataAction { + if (context.embeddable?.type === 'map') return false; // TODO: https://github.com/elastic/kibana/issues/73043 + return super.isCompatible(context); + } + protected readonly getUrl = async ( context: ExploreDataChartActionContext ): Promise => { @@ -42,7 +49,11 @@ export class ExploreDataChartAction extends AbstractExploreDataAction Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; - const filtersAndTimeRangeFromContext = jest.fn((async () => ({ - filters: [], - })) as EmbeddableStart['filtersAndTimeRangeFromContext']); - const plugins: PluginDeps = { discover: { urlGenerator, }, - embeddable: { - filtersAndTimeRangeFromContext, - }, kibanaLegacy: { dashboardConfig: { getHideWriteControls: () => dashboardOnlyMode, diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index 4b018354aa092..9e66925132a7d 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -9,8 +9,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { UiActionsSetup, UiActionsStart, - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, + APPLY_FILTER_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; @@ -77,8 +76,7 @@ export class DiscoverEnhancedPlugin if (this.config.actions.exploreDataInChart.enabled) { const exploreDataChartAction = new ExploreDataChartAction(params); - uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction); - uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction); + uiActions.addTriggerAction(APPLY_FILTER_TRIGGER, exploreDataChartAction); } } } diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index f3136ca155c78..d5b50fce38718 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -194,7 +194,7 @@ export const AlertPreview: React.FC = (props) => { plural: previewResult.resultTotals.noData !== 1 ? 's' : '', }} /> - ) : null} + ) : null}{' '} {previewResult.resultTotals.error ? ( { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); const { inventoryPrefill } = useAlertPrefillContext(); const { nodeType, metric, filterQuery } = inventoryPrefill; @@ -27,26 +26,12 @@ export const InventoryAlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); - const menuItems = useMemo(() => { - return [ - setFlyoutVisible(true)}> - - , - - - , - ]; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [kibana.services]); + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; return ( <> diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 583cbe18ee9db..b69078beec670 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -368,6 +368,7 @@ export const Expressions: React.FC = (props) => { validate={validateMetricThreshold} fetch={alertsContext.http.fetch} groupByDisplayName={alertParams.nodeType} + showNoDataResults={alertParams.alertOnNoData} /> diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx new file mode 100644 index 0000000000000..fc565aee37ff4 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +export const ManageAlertsContextMenuItem = () => { + const manageAlertsLinkProps = useLinkProps({ + app: 'management', + pathname: '/insightsAndAlerting/triggersActions/alerts', + }); + return ( + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index 384a93e796dbe..dd61be0eee362 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useAlertPrefillContext } from '../../use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; +import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item'; export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); const { metricThresholdPrefill } = useAlertPrefillContext(); const { groupBy, filterQuery, metrics } = metricThresholdPrefill; @@ -27,26 +26,12 @@ export const MetricsAlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); - const menuItems = useMemo(() => { - return [ - setFlyoutVisible(true)}> - - , - - - , - ]; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [kibana.services]); + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; return ( <> diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index cd1e93a2a0c96..8bb8b3934b5fd 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -185,7 +185,7 @@ export const Expressions: React.FC = (props) => { const preFillAlertCriteria = useCallback(() => { const md = alertsContext.metadata; - if (md && md.currentOptions?.metrics) { + if (md?.currentOptions?.metrics?.length) { setAlertParams( 'criteria', md.currentOptions.metrics.map((metric) => ({ @@ -249,13 +249,18 @@ export const Expressions: React.FC = (props) => { if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } - }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + }, [alertsContext.metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps const handleFieldSearchChange = useCallback( (e: ChangeEvent) => onFilterChange(e.target.value), [onFilterChange] ); + const groupByPreviewDisplayName = useMemo(() => { + if (Array.isArray(alertParams.groupBy)) return alertParams.groupBy.join(', '); + return alertParams.groupBy; + }, [alertParams.groupBy]); + return ( <> @@ -400,7 +405,7 @@ export const Expressions: React.FC = (props) => { showNoDataResults={alertParams.alertOnNoData} validate={validateMetricThreshold} fetch={alertsContext.http.fetch} - groupByDisplayName={alertParams.groupBy} + groupByDisplayName={groupByPreviewDisplayName} /> diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index cdb6b341c7299..c90c534193fdc 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -45,7 +45,7 @@ interface Props { derivedIndexPattern: IIndexPattern; source: InfraSource | null; filterQuery?: string; - groupBy?: string; + groupBy?: string | string[]; } const tooltipProps = { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index 185895062cfe2..a3d09742e9a57 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -19,7 +19,7 @@ export const useMetricsExplorerChartData = ( derivedIndexPattern: IIndexPattern, source: InfraSource | null, filterQuery?: string, - groupBy?: string + groupBy?: string | string[] ) => { const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' }; const options: MetricsExplorerOptions = useMemo( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index 58586c1dd8b98..b2317c558be44 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -53,7 +53,7 @@ export interface ExpressionChartData { export interface AlertParams { criteria: MetricExpression[]; - groupBy?: string; + groupBy?: string[]; filterQuery?: string; sourceId?: string; filterQueryText?: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts index 04aeba41fa00d..ca4fc0abc37a4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts @@ -157,6 +157,25 @@ describe('createTSVBLink()', () => { }); }); + it('should use the workaround index pattern when there are multiple listed in the source', () => { + const customSource = { + ...source, + metricAlias: 'my-beats-*,metrics-*', + fields: { ...source.fields, timestamp: 'time' }, + }; + const link = createTSVBLink(customSource, options, series, timeRange, chartOptions); + expect(link).toStrictEqual({ + app: 'visualize', + hash: '/create', + search: { + _a: + "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metric*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metric*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', + type: 'metrics', + }, + }); + }); + test('createFilterFromOptions()', () => { const customOptions = { ...options, groupBy: 'host.name' }; const customSeries = { ...series, id: 'test"foo' }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 3afc0d050e736..afddaf6621f10 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -23,6 +23,14 @@ import { SourceQuery } from '../../../../../graphql/types'; import { createMetricLabel } from './create_metric_label'; import { LinkDescriptor } from '../../../../../hooks/use_link_props'; +/* + We've recently changed the default index pattern in Metrics UI from `metricbeat-*` to + `metrics-*,metricbeat-*`. There is a bug in TSVB when there is an empty index in the pattern + the field dropdowns are not populated correctly. This index pattern is a temporary fix. + See: https://github.com/elastic/kibana/issues/73987 +*/ +const TSVB_WORKAROUND_INDEX_PATTERN = 'metric*'; + export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptionsMetric) => { if (metric.aggregation === 'rate') { const metricId = uuid.v1(); @@ -128,6 +136,13 @@ export const createFilterFromOptions = ( return { language: 'kuery', query: filters.join(' and ') }; }; +const createTSVBIndexPattern = (alias: string) => { + if (alias.split(',').length > 1) { + return TSVB_WORKAROUND_INDEX_PATTERN; + } + return alias; +}; + export const createTSVBLink = ( source: SourceQuery.Query['source']['configuration'] | undefined, options: MetricsExplorerOptions, @@ -135,6 +150,9 @@ export const createTSVBLink = ( timeRange: MetricsExplorerTimeOptions, chartOptions: MetricsExplorerChartOptions ): LinkDescriptor => { + const tsvbIndexPattern = createTSVBIndexPattern( + (source && source.metricAlias) || TSVB_WORKAROUND_INDEX_PATTERN + ); const appState = { filters: [], linked: false, @@ -147,8 +165,8 @@ export const createTSVBLink = ( axis_position: 'left', axis_scale: 'normal', id: uuid.v1(), - default_index_pattern: (source && source.metricAlias) || 'metricbeat-*', - index_pattern: (source && source.metricAlias) || 'metricbeat-*', + default_index_pattern: tsvbIndexPattern, + index_pattern: tsvbIndexPattern, interval: 'auto', series: options.metrics.map(mapMetricToSeries(chartOptions)), show_grid: 1, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 5c31c78b10fa9..3b795810b39f0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -23,9 +23,9 @@ import { InfraSourceConfiguration } from '../../sources'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; type ConditionResult = InventoryMetricConditions & { - shouldFire: boolean | boolean[]; + shouldFire: boolean[]; currentValue: number; - isNoData: boolean; + isNoData: boolean[]; isError: boolean; }; @@ -71,8 +71,8 @@ export const evaluateCondition = async ( value !== null && (Array.isArray(value) ? value.map((v) => comparisonFunction(Number(v), threshold)) - : comparisonFunction(value as number, threshold)), - isNoData: value === null, + : [comparisonFunction(value as number, threshold)]), + isNoData: Array.isArray(value) ? value.map((v) => v === null) : [value === null], isError: value === undefined, currentValue: getCurrentValue(value), }; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 60eee49a5010d..7b816f2f225b5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { first, get } from 'lodash'; +import { first, get, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; @@ -56,11 +56,14 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = for (const item of inventoryItems) { const alertInstance = services.alertInstanceFactory(`${item}`); // AND logic; all criteria must be across the threshold - const shouldAlertFire = results.every((result) => result[item].shouldFire); + const shouldAlertFire = results.every((result) => + // Grab the result of the most recent bucket + last(result[item].shouldFire) + ); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = results.some((result) => result[item].isNoData); + const isNoData = results.some((result) => last(result[item].isNoData)); const isError = results.some((result) => result[item].isError); const nextState = isError diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 5c654e2f47e78..562f344dbd060 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -59,28 +59,29 @@ export const previewInventoryMetricThresholdAlert = async ({ const inventoryItems = Object.keys(first(results) as any); const previewResults = inventoryItems.map((item) => { - const isNoData = results.some((result) => result[item].isNoData); - if (isNoData) { - return null; - } - const isError = results.some((result) => result[item].isError); - if (isError) { - return undefined; - } - const numberOfResultBuckets = lookbackSize; const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution); - return [...Array(numberOfExecutionBuckets)].reduce( - (totalFired, _, i) => - totalFired + - (results.every((result) => { - const shouldFire = result[item].shouldFire as boolean[]; - return shouldFire[Math.floor(i * alertResultsPerExecution)]; - }) - ? 1 - : 0), - 0 - ); + let numberOfTimesFired = 0; + let numberOfNoDataResults = 0; + let numberOfErrors = 0; + for (let i = 0; i < numberOfExecutionBuckets; i++) { + const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); + const allConditionsFiredInMappedBucket = results.every((result) => { + const shouldFire = result[item].shouldFire as boolean[]; + return shouldFire[mappedBucketIndex]; + }); + const someConditionsNoDataInMappedBucket = results.some((result) => { + const hasNoData = result[item].isNoData as boolean[]; + return hasNoData[mappedBucketIndex]; + }); + const someConditionsErrorInMappedBucket = results.some((result) => { + return result[item].isError; + }); + if (allConditionsFiredInMappedBucket) numberOfTimesFired++; + if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; + if (someConditionsErrorInMappedBucket) numberOfErrors++; + } + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; }); return previewResults; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index ca46f6cc16547..49f82c7ccec0b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -72,7 +72,9 @@ export const evaluateAlert = ( typeof point.value === 'number' && comparisonFunction(point.value, threshold) ) : [false], - isNoData: (Array.isArray(points) ? last(points)?.value : points) === null, + isNoData: Array.isArray(points) + ? points.map((point) => point?.value === null || point === null) + : [points === null], isError: isNaN(Array.isArray(points) ? last(points)?.value : points), }; }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index b4754a8624fd5..b2a8f0281b9e2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -45,7 +45,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => ); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = alertResults.some((result) => result[group].isNoData); + const isNoData = alertResults.some((result) => last(result[group].isNoData)); const isError = alertResults.some((result) => result[group].isError); const nextState = isError diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 0ecfa27d0f0a8..5aca7f0890940 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -36,7 +36,7 @@ export const previewMetricThresholdAlert: ( params: PreviewMetricThresholdAlertParams, iterations?: number, precalculatedNumberOfGroups?: number -) => Promise> = async ( +) => Promise = async ( { callCluster, params, @@ -77,15 +77,6 @@ export const previewMetricThresholdAlert: ( const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; const previewResults = await Promise.all( groups.map(async (group) => { - const isNoData = alertResults.some((alertResult) => alertResult[group].isNoData); - if (isNoData) { - return null; - } - const isError = alertResults.some((alertResult) => alertResult[group].isError); - if (isError) { - return NaN; - } - // Interpolate the buckets returned by evaluateAlert and return a count of how many of these // buckets would have fired the alert. If the alert interval and bucket interval are the same, // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation @@ -95,14 +86,25 @@ export const previewMetricThresholdAlert: ( numberOfResultBuckets / alertResultsPerExecution ); let numberOfTimesFired = 0; + let numberOfNoDataResults = 0; + let numberOfErrors = 0; for (let i = 0; i < numberOfExecutionBuckets; i++) { const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); const allConditionsFiredInMappedBucket = alertResults.every( (alertResult) => alertResult[group].shouldFire[mappedBucketIndex] ); + const someConditionsNoDataInMappedBucket = alertResults.some((alertResult) => { + const hasNoData = alertResult[group].isNoData as boolean[]; + return hasNoData[mappedBucketIndex]; + }); + const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => { + return alertResult[group].isError; + }); if (allConditionsFiredInMappedBucket) numberOfTimesFired++; + if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; + if (someConditionsErrorInMappedBucket) numberOfErrors++; } - return numberOfTimesFired; + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; }) ); return previewResults; @@ -152,9 +154,9 @@ export const previewMetricThresholdAlert: ( // so filter these results out entirely and only regard the resultA portion .filter((value) => typeof value !== 'undefined') .reduce((a, b) => { - if (typeof a !== 'number') return a; - if (typeof b !== 'number') return b; - return a + b; + if (!a) return b; + if (!b) return a; + return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }) ); return zippedResult as any; diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 8a3e9e4d0bedc..5594323d706de 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -55,10 +55,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, groupResult) => { - if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; - if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; - return { ...totals, fired: totals.fired + groupResult }; + (totals, [firedResult, noDataResult, errorResult]) => { + return { + ...totals, + fired: totals.fired + firedResult, + noData: totals.noData + noDataResult, + error: totals.error + errorResult, + }; }, { fired: 0, @@ -66,7 +69,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) error: 0, } ); - return response.ok({ body: alertPreviewSuccessResponsePayloadRT.encode({ numberOfGroups, @@ -86,10 +88,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, groupResult) => { - if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; - if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; - return { ...totals, fired: totals.fired + groupResult }; + (totals, [firedResult, noDataResult, errorResult]) => { + return { + ...totals, + fired: totals.fired + firedResult, + noData: totals.noData + noDataResult, + error: totals.error + errorResult, + }; }, { fired: 0, diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index 3d3c91a4310f8..73cd8463bb6aa 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -6,5 +6,4 @@ export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; -export const DEFAULT_REGISTRY_URL = 'https://epr-snapshot.ea-web.elastic.dev'; export const INDEX_PATTERN_PLACEHOLDER_SUFFIX = '-index_pattern_placeholder'; diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 7acef263f973a..69bcc498c18be 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -22,7 +22,8 @@ export interface IngestManagerConfigType { host?: string; ca_sha256?: string; }; - agentConfigRolloutConcurrency: number; + agentConfigRolloutRateLimitIntervalMs: number; + agentConfigRolloutRateLimitRequestPerInterval: number; }; } diff --git a/x-pack/plugins/ingest_manager/dev_docs/definitions.md b/x-pack/plugins/ingest_manager/dev_docs/definitions.md index a33d95f3afa38..bd20780611055 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/definitions.md +++ b/x-pack/plugins/ingest_manager/dev_docs/definitions.md @@ -10,7 +10,7 @@ This section is to define terms used across ingest management. A package config is a definition on how to collect data from a service, for example `nginx`. A package config contains definitions for one or multiple inputs and each input can contain one or multiple streams. -With the example of the nginx Package Config, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected +With the example of the nginx Package Config, it contains two inputs: `logs` and `nginx/metrics`. Logs and metrics are collected differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. ## Data Stream diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx index e98ebb7cadc7c..5343d86244f1e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx @@ -64,6 +64,14 @@ export const EnrollmentStepAgentConfig: React.FC = (props) => { useEffect( function useDefaultConfigEffect() { if (agentConfigs && agentConfigs.length && !selectedState.agentConfigId) { + if (agentConfigs.length === 1) { + setSelectedState({ + ...selectedState, + agentConfigId: agentConfigs[0].id, + }); + return; + } + const defaultConfig = agentConfigs.find((config) => config.is_default); if (defaultConfig) { setSelectedState({ diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index ce81736f2e84f..1ec13bd80f0fb 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -43,5 +43,4 @@ export { // Defaults DEFAULT_AGENT_CONFIG, DEFAULT_OUTPUT, - DEFAULT_REGISTRY_URL, } from '../../common'; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 40e0153a26581..e2f659f54d625 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -6,7 +6,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; -export { AgentService, ESIndexPatternService } from './services'; +export { AgentService, ESIndexPatternService, getRegistryUrl } from './services'; export { IngestManagerSetupContract, IngestManagerSetupDeps, @@ -35,7 +35,8 @@ export const config = { host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), }), - agentConfigRolloutConcurrency: schema.number({ defaultValue: 10 }), + agentConfigRolloutRateLimitIntervalMs: schema.number({ defaultValue: 5000 }), + agentConfigRolloutRateLimitRequestPerInterval: schema.number({ defaultValue: 5 }), }), }), }; diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index e7495df254a09..e5e1194d59ecb 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -83,9 +83,9 @@ export interface IngestManagerAppContext { security?: SecurityPluginSetup; config$?: Observable; savedObjects: SavedObjectsServiceStart; - isProductionMode: boolean; - kibanaVersion: string; - kibanaBranch: string; + isProductionMode: PluginInitializerContext['env']['mode']['prod']; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + kibanaBranch: PluginInitializerContext['env']['packageInfo']['branch']; cloud?: CloudSetup; logger?: Logger; httpSetup?: HttpServiceSetup; @@ -144,9 +144,9 @@ export class IngestManagerPlugin private cloud: CloudSetup | undefined; private logger: Logger | undefined; - private isProductionMode: boolean; - private kibanaVersion: string; - private kibanaBranch: string; + private isProductionMode: IngestManagerAppContext['isProductionMode']; + private kibanaVersion: IngestManagerAppContext['kibanaVersion']; + private kibanaBranch: IngestManagerAppContext['kibanaBranch']; private httpSetup: HttpServiceSetup | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts index f84f417ce402d..e5b5a83743287 100644 --- a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts @@ -39,7 +39,7 @@ describe('registerLimitedConcurrencyRoutes', () => { }); // assertions for calls to .decrease are commented out because it's called on the -// "req.events.aborted$ observable (which) will never emit from a mocked request in a jest unit test environment" +// "req.events.completed$ observable (which) will never emit from a mocked request in a jest unit test environment" // https://github.com/elastic/kibana/pull/72338#issuecomment-661908791 describe('preAuthHandler', () => { test(`ignores routes when !isMatch`, async () => { diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts index 11fdc944e031d..7ba8e151b726c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts @@ -66,9 +66,7 @@ export function createLimitedPreAuthHandler({ maxCounter.increase(); - // requests.events.aborted$ has a bug (but has test which explicitly verifies) where it's fired even when the request completes - // https://github.com/elastic/kibana/pull/70495#issuecomment-656288766 - request.events.aborted$.toPromise().then(() => { + request.events.completed$.toPromise().then(() => { maxCounter.decrease(); }); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts deleted file mode 100644 index 70207dcf325c4..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts +++ /dev/null @@ -1,45 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Rx from 'rxjs'; -import { share } from 'rxjs/operators'; -import { createSubscriberConcurrencyLimiter } from './rxjs_utils'; - -function createSpyObserver(o: Rx.Observable): [Rx.Subscription, jest.Mock] { - const spy = jest.fn(); - const observer = o.subscribe(spy); - return [observer, spy]; -} - -describe('createSubscriberConcurrencyLimiter', () => { - it('should not publish to more than n concurrent subscriber', async () => { - const subject = new Rx.Subject(); - const sharedObservable = subject.pipe(share()); - - const limiter = createSubscriberConcurrencyLimiter(2); - - const [observer1, spy1] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer2, spy2] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer3, spy3] = createSpyObserver(sharedObservable.pipe(limiter())); - const [observer4, spy4] = createSpyObserver(sharedObservable.pipe(limiter())); - subject.next('test1'); - - expect(spy1).toBeCalled(); - expect(spy2).toBeCalled(); - expect(spy3).not.toBeCalled(); - expect(spy4).not.toBeCalled(); - - observer1.unsubscribe(); - expect(spy3).toBeCalled(); - expect(spy4).not.toBeCalled(); - - observer2.unsubscribe(); - expect(spy4).toBeCalled(); - - observer3.unsubscribe(); - observer4.unsubscribe(); - }); -}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts index dc0ed35207e46..dddade6841460 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts @@ -43,23 +43,37 @@ export const toPromiseAbortable = ( } }); -export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { - let observers: Array<[Rx.Subscriber, any]> = []; - let activeObservers: Array> = []; +export function createRateLimiter( + ratelimitIntervalMs: number, + ratelimitRequestPerInterval: number +) { + function createCurrentInterval() { + return { + startedAt: Rx.asyncScheduler.now(), + numRequests: 0, + }; + } - function processNext() { - if (activeObservers.length >= maxConcurrency) { - return; - } - const observerValuePair = observers.shift(); + let currentInterval: { startedAt: number; numRequests: number } = createCurrentInterval(); + let observers: Array<[Rx.Subscriber, any]> = []; + let timerSubscription: Rx.Subscription | undefined; - if (!observerValuePair) { + function createTimeout() { + if (timerSubscription) { return; } - - const [observer, value] = observerValuePair; - activeObservers.push(observer); - observer.next(value); + timerSubscription = Rx.asyncScheduler.schedule(() => { + timerSubscription = undefined; + currentInterval = createCurrentInterval(); + for (const [waitingObserver, value] of observers) { + if (currentInterval.numRequests >= ratelimitRequestPerInterval) { + createTimeout(); + continue; + } + currentInterval.numRequests++; + waitingObserver.next(value); + } + }, ratelimitIntervalMs); } return function limit(): Rx.MonoTypeOperatorFunction { @@ -67,8 +81,14 @@ export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { new Rx.Observable((observer) => { const subscription = observable.subscribe({ next(value) { + if (currentInterval.numRequests < ratelimitRequestPerInterval) { + currentInterval.numRequests++; + observer.next(value); + return; + } + observers = [...observers, [observer, value]]; - processNext(); + createTimeout(); }, error(err) { observer.error(err); @@ -79,10 +99,8 @@ export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { }); return () => { - activeObservers = activeObservers.filter((o) => o !== observer); observers = observers.filter((o) => o[0] !== observer); subscription.unsubscribe(); - processNext(); }; }); }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index 53270afe453c4..1547b6b5ea053 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -28,7 +28,7 @@ import * as APIKeysService from '../../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_UPDATE_ACTIONS_INTERVAL_MS } from '../../../constants'; import { createAgentAction, getNewActionsSince } from '../actions'; import { appContextService } from '../../app_context'; -import { toPromiseAbortable, AbortError, createSubscriberConcurrencyLimiter } from './rxjs_utils'; +import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -134,8 +134,9 @@ export function agentCheckinStateNewActionsFactory() { const agentConfigs$ = new Map>(); const newActions$ = createNewActionsSharedObservable(); // Rx operators - const concurrencyLimiter = createSubscriberConcurrencyLimiter( - appContextService.getConfig()?.fleet.agentConfigRolloutConcurrency ?? 10 + const rateLimiter = createRateLimiter( + appContextService.getConfig()?.fleet.agentConfigRolloutRateLimitIntervalMs ?? 5000, + appContextService.getConfig()?.fleet.agentConfigRolloutRateLimitRequestPerInterval ?? 50 ); async function subscribeToNewActions( @@ -158,7 +159,7 @@ export function agentCheckinStateNewActionsFactory() { const stream$ = agentConfig$.pipe( timeout(appContextService.getConfig()?.fleet.pollingRequestTimeout || 0), filter((config) => shouldCreateAgentConfigAction(agent, config)), - concurrencyLimiter(), + rateLimiter(), mergeMap((config) => createAgentActionFromConfig(soClient, agent, config)), merge(newActions$), mergeMap(async (data) => { diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index bdc7a443ba6dd..7f82670a4d02c 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -10,6 +10,7 @@ import { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, } from '../../../encrypted_saved_objects/server'; +import packageJSON from '../../../../../package.json'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin'; @@ -22,9 +23,9 @@ class AppContextService { private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; - private isProductionMode: boolean = false; - private kibanaVersion: string | undefined; - private kibanaBranch: string | undefined; + private isProductionMode: IngestManagerAppContext['isProductionMode'] = false; + private kibanaVersion: IngestManagerAppContext['kibanaVersion'] = packageJSON.version; + private kibanaBranch: IngestManagerAppContext['kibanaBranch'] = packageJSON.branch; private cloud?: CloudSetup; private logger: Logger | undefined; private httpSetup?: HttpServiceSetup; @@ -121,16 +122,10 @@ class AppContextService { } public getKibanaVersion() { - if (!this.kibanaVersion) { - throw new Error('Kibana version is not set.'); - } return this.kibanaVersion; } public getKibanaBranch() { - if (!this.kibanaBranch) { - throw new Error('Kibana branch is not set.'); - } return this.kibanaBranch; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index 47c9121808988..b788d1bcbb4a9 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -3,20 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_REGISTRY_URL } from '../../../constants'; import { appContextService, licenseService } from '../../'; +// from https://github.com/elastic/package-registry#docker (maybe from OpenAPI one day) +// the unused variables cause a TS warning about unused values +// chose to comment them out vs @ts-ignore or @ts-expect-error on each line + +const PRODUCTION_REGISTRY_URL_CDN = 'https://epr.elastic.co'; +// const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co'; +// const EXPERIMENTAL_REGISTRY_URL_CDN = 'https://epr-experimental.elastic.co/'; +const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co'; + +// const PRODUCTION_REGISTRY_URL_NO_CDN = 'https://epr.ea-web.elastic.dev'; +// const STAGING_REGISTRY_URL_NO_CDN = 'https://epr-staging.ea-web.elastic.dev'; +// const EXPERIMENTAL_REGISTRY_URL_NO_CDN = 'https://epr-experimental.ea-web.elastic.dev/'; +// const SNAPSHOT_REGISTRY_URL_NO_CDN = 'https://epr-snapshot.ea-web.elastic.dev'; + +const getDefaultRegistryUrl = (): string => { + const branch = appContextService.getKibanaBranch(); + if (branch === 'master') { + return SNAPSHOT_REGISTRY_URL_CDN; + } else { + return PRODUCTION_REGISTRY_URL_CDN; + } +}; + export const getRegistryUrl = (): string => { const license = licenseService.getLicenseInformation(); const customUrl = appContextService.getConfig()?.registryUrl; + const isGoldPlus = license?.isAvailable && license?.isActive && license?.hasAtLeast('gold'); - if ( - customUrl && - license && - license.isAvailable && - license.hasAtLeast('gold') && - license.isActive - ) { + if (customUrl && isGoldPlus) { return customUrl; } @@ -24,5 +41,5 @@ export const getRegistryUrl = (): string => { appContextService.getLogger().warn('Gold license is required to use a custom registry url.'); } - return DEFAULT_REGISTRY_URL; + return getDefaultRegistryUrl(); }; diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 74adab09d12eb..f6ca9e7bbbe71 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -9,6 +9,8 @@ import { AgentStatus, Agent } from '../types'; import * as settingsService from './settings'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; +export { getRegistryUrl } from './epm/registry/registry_url'; + /** * Service to return the index pattern of EPM packages */ diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index a72f4f429a1be..b30a586487009 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -249,6 +249,27 @@ describe('Lens App', () => { expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); }); + it('passes global filters to frame', async () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); + args.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { + return [pinnedFilter]; + }); + const component = mount(); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + dateRange: { fromDate: 'now-7d', toDate: 'now' }, + query: { query: '', language: 'kuery' }, + filters: [pinnedFilter], + }) + ); + }); + it('sets breadcrumbs when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); instance = mount(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 2a7eaff32fa08..ab4c4820315ac 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -94,7 +94,7 @@ export function App({ toDate: currentRange.to, }, originatingApp, - filters: [], + filters: data.query.filterManager.getFilters(), indicateNoData: false, }; }); diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index d450debd56293..fad8ecc86277b 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { EsDataTypeGeoPoint, diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 26511f89c32b8..76aa896a741f6 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { DefaultNamespace } from '../types/default_namespace'; -import { DefaultStringArray, NonEmptyString } from '../../siem_common_deps'; +import { DefaultStringArray, NonEmptyString } from '../../shared_imports'; export const name = t.string; export type Name = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts index 7ac75b077acb5..d8e3793ac9bd6 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { SearchEsListItemSchema, searchEsListItemSchema } from './search_es_list_item_schema'; import { getSearchEsListItemMock } from './search_es_list_item_schema.mock'; @@ -22,7 +22,7 @@ describe('search_es_list_item_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate with a madeup value', () => { + test('it should FAIL validation when a madeup value', () => { const payload: SearchEsListItemSchema & { madeupValue: string } = { ...getSearchEsListItemMock(), madeupValue: 'madeupvalue', diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts index 739f102e6a872..27a6c5ef52460 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { SearchEsListSchema, searchEsListSchema } from './search_es_list_schema'; import { getSearchEsListMock } from './search_es_list_schema.mock'; @@ -22,7 +22,7 @@ describe('search_es_list_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate with a madeup value', () => { + test('it should FAIL validation when a madeup value', () => { const payload: SearchEsListSchema & { madeupValue: string } = { ...getSearchEsListMock(), madeupValue: 'madeupvalue', diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 75e0410be610a..e40a80a0d589d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index ab30e8e35548d..626b9e3e624ef 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -22,7 +22,7 @@ import { import { RequiredKeepUndefined } from '../../types'; import { CreateCommentsArray, DefaultCreateCommentsArray, nonEmptyEntriesArray } from '../types'; import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../siem_common_deps'; +import { DefaultUuid } from '../../shared_imports'; export const createEndpointListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index cf4c1fea0306f..d2ad69d1ee7b6 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index c3f41cac90c64..039a38594a367 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -29,7 +29,7 @@ import { nonEmptyEntriesArray, } from '../types'; import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../siem_common_deps'; +import { DefaultUuid } from '../../shared_imports'; export const createExceptionListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts index 21270f526900b..c9e2aa37a132b 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { CreateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 94a4e1588f5ab..7009fbd709e54 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -25,7 +25,7 @@ import { DefaultUuid, DefaultVersionNumber, DefaultVersionNumberDecoded, -} from '../../siem_common_deps'; +} from '../../shared_imports'; import { NamespaceType } from '../types'; export const createExceptionListSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts index 8178d49690e39..813d5e349e7e6 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateListItemSchemaMock } from './create_list_item_schema.mock'; import { CreateListItemSchema, createListItemSchema } from './create_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts index 9b496a01045de..82340453a98f1 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { CreateListSchema, createListSchema } from './create_list_schema'; import { getCreateListSchemaMock } from './create_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 18ed0f42ccd6f..bfe3ecdcb623b 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { description, deserializer, id, meta, name, serializer, type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../siem_common_deps'; +import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../shared_imports'; export const createListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts index fa75be8bc541e..fa3c1ef3b02f5 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts index 042f62a8d129b..d249cd779e862 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts index 2bb0a23173bd6..ec781d59af120 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts index 9bc2825d774ed..7b2263863e1f6 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteListItemSchema, deleteListItemSchema } from './delete_list_item_schema'; import { getDeleteListItemSchemaMock } from './delete_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts index 278508305c6f0..65ca2f3f457e9 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteListSchema, deleteListSchema } from './delete_list_schema'; import { getDeleteListSchemaMock } from './delete_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts index 1ffe2e2fc4ecc..cd6f4c1b147db 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ExportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts index 8249b1e2d49c2..79449b136d066 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindEndpointListItemSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts index f402f22b093ad..1e971a4eebc33 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts index ef96346c732b8..6f5d34d6be73e 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindExceptionListSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts index 59d4b4485b578..8c119aeb14e24 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts index 63f29a64b4bf9..086e457e8f6b8 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindListSchemaDecodedMock, getFindListSchemaMock } from './find_list_schema.mock'; import { FindListSchemaEncoded, findListSchema } from './find_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts index 9d03229b4d1d9..9945dc03c2e14 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ImportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts index 7f7c6368a1c5e..4de77b66610d3 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ImportListItemSchema, importListItemSchema } from './import_list_item_schema'; import { getImportListItemSchemaMock } from './import_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts index 58c19e8f9cb4f..b148f19da8a86 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getPathListItemSchemaMock } from './patch_list_item_schema.mock'; import { PatchListItemSchema, patchListItemSchema } from './patch_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts index 3ab658014bbfa..dea48df3f1702 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getPathListSchemaMock } from './patch_list_schema.mock'; import { PatchListSchema, patchListSchema } from './patch_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts index 70a1d783c87d6..adec476ea5ad7 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadEndpointListItemSchemaMock } from './read_endpoint_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts index 86c80a527be0d..b7c2715f14e1c 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadExceptionListItemSchemaMock } from './read_exception_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts index 86cebc3cd3f8e..3bc61e3a5e90a 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadExceptionListSchemaMock } from './read_exception_list_schema.mock'; import { ReadExceptionListSchema, readExceptionListSchema } from './read_exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts index 5c71c9820cc1e..1d140719ad939 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadListItemSchemaMock } from './read_list_item_schema.mock'; import { ReadListItemSchema, readListItemSchema } from './read_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts index a1ba2655dd723..0b7e92c23f77a 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadListSchemaMock } from './read_list_schema.mock'; import { ReadListSchema, readListSchema } from './read_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts index db5bc45ad028b..ecbbb250a88f6 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts index ce589fb097a60..a49a5552603fd 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts index 892f277045a69..650cbd439ad2b 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts index 6127e20343834..cb6cd76dd3f03 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateListItemSchema, updateListItemSchema } from './update_list_item_schema'; import { getUpdateListItemSchemaMock } from './update_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts index 6e7fb158767b5..a59a93b06e34d 100644 --- a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getAcknowledgeSchemaResponseMock } from './acknowledge_schema.mock'; import { AcknowledgeSchema, acknowledgeSchema } from './acknowledge_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts index 5fccaaac22e3a..8c1392109979e 100644 --- a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { CreateEndpointListSchema, createEndpointListSchema } from './create_endpoint_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts index c8bf73cf842e1..32b55104e4fdf 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { ExceptionListItemSchema, exceptionListItemSchema } from './exception_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts index b773dd498ed01..1b5ef08b02d5f 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { ExceptionListSchema, exceptionListSchema } from './exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts index 70fcf9a86122c..5da3accccd9c2 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { getFoundExceptionListItemSchemaMock } from './found_exception_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts index a96ee07c4613b..d4fa8ee0e3481 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { getFoundExceptionListSchemaMock } from './found_exception_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts index 9cb130ec0e8ad..2b072d8f95cd8 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListItemIndexExistSchemaResponseMock } from './list_item_index_exist_schema.mock'; import { ListItemIndexExistSchema, listItemIndexExistSchema } from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts index 8b73506d13750..ec4c8d2c2d1ea 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListItemResponseMock } from './list_item_schema.mock'; import { ListItemSchema, listItemSchema } from './list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts index e7ae9b45a5e15..87e56e5dd95ac 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListResponseMock } from './list_schema.mock'; import { ListSchema, listSchema } from './list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/types/comment.test.ts b/x-pack/plugins/lists/common/schemas/types/comment.test.ts index c7c945277f756..081bb9b4bae54 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DATE_NOW } from '../../constants.mock'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getCommentsArrayMock, getCommentsMock } from './comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/comment.ts b/x-pack/plugins/lists/common/schemas/types/comment.ts index 6b0b0166b9ee1..4d7aba3b3ad98 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { created_at, created_by, id, updated_at, updated_by } from '../common/schemas'; export const comment = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts index 366bf84d48bbf..8bca8df437871 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.ts index fd33313430ce6..4ccc28b2c4a6d 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; export const createComment = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts index 541b8ab1c799c..ee2dc0cf2a478 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultCommentsArray } from './default_comments_array'; import { CommentsArray } from './comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts index eb960b5411904..4aac3cc84a3a2 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultCreateCommentsArray } from './default_create_comments_array'; import { CreateCommentsArray } from './create_comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts index 152f85233aa1a..8e7ffdbdaea7b 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultNamespace } from './default_namespace'; @@ -48,7 +48,7 @@ describe('default_namespace', () => { expect(message.schema).toEqual('single'); }); - test('it should NOT validate if not "single" or "agnostic"', () => { + test('it should FAIL validation if not "single" or "agnostic"', () => { const payload = 'something else'; const decoded = DefaultNamespace.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts index 255c89959b610..e377faae87947 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultNamespaceArray, DefaultNamespaceArrayType } from './default_namespace_array'; @@ -21,7 +21,7 @@ describe('default_namespace_array', () => { expect(message.schema).toEqual(['single']); }); - test('it should NOT validate a numeric value', () => { + test('it should FAIL validation of numeric value', () => { const payload = 5; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -86,7 +86,7 @@ describe('default_namespace_array', () => { expect(message.schema).toEqual(['single', 'agnostic', 'single']); }); - test('it should not validate 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { + test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts index 612148dc4ccab..25c84af8c9ee3 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultUpdateCommentsArray } from './default_update_comments_array'; import { UpdateCommentsArray } from './update_comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts index b14afab327fb0..3ddeeebfceda7 100644 --- a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { EmptyStringArray, EmptyStringArrayEncoded } from './empty_string_array'; @@ -57,7 +57,7 @@ describe('empty_string_array', () => { expect(message.schema).toEqual(['a', 'b', 'c']); }); - test('it should NOT validate a number', () => { + test('it should FAIL validation of number', () => { const payload: number = 5; const decoded = EmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index 16794415138b2..c0093ed750b62 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -12,21 +12,18 @@ import { getEntryExistsMock } from './entry_exists.mock'; import { getEntryNestedMock } from './entry_nested.mock'; export const getListAndNonListEntriesArrayMock = (): EntriesArray => [ - { ...getEntryMatchMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryListMock() }, - { ...getEntryExistsMock() }, - { ...getEntryNestedMock() }, + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryListMock(), + getEntryExistsMock(), + getEntryNestedMock(), ]; -export const getListEntriesArrayMock = (): EntriesArray => [ - { ...getEntryListMock() }, - { ...getEntryListMock() }, -]; +export const getListEntriesArrayMock = (): EntriesArray => [getEntryListMock(), getEntryListMock()]; export const getEntriesArrayMock = (): EntriesArray => [ - { ...getEntryMatchMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryExistsMock() }, - { ...getEntryNestedMock() }, + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryExistsMock(), + getEntryNestedMock(), ]; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/entries.test.ts index cad94220a232c..f5c022c7a394f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -20,7 +20,7 @@ import { entriesArray, entriesArrayOrUndefined, entry } from './entries'; describe('Entries', () => { describe('entry', () => { test('it should validate a match entry', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -29,7 +29,7 @@ describe('Entries', () => { }); test('it should validate a match_any entry', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -38,7 +38,7 @@ describe('Entries', () => { }); test('it should validate a exists entry', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -47,7 +47,7 @@ describe('Entries', () => { }); test('it should validate a list entry', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -55,8 +55,8 @@ describe('Entries', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate a nested entry', () => { - const payload = { ...getEntryNestedMock() }; + test('it should FAIL validation of nested entry', () => { + const payload = getEntryNestedMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -79,7 +79,7 @@ describe('Entries', () => { describe('entriesArray', () => { test('it should validate an array with match entry', () => { - const payload = [{ ...getEntryMatchMock() }]; + const payload = [getEntryMatchMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -88,7 +88,7 @@ describe('Entries', () => { }); test('it should validate an array with match_any entry', () => { - const payload = [{ ...getEntryMatchAnyMock() }]; + const payload = [getEntryMatchAnyMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -97,7 +97,7 @@ describe('Entries', () => { }); test('it should validate an array with exists entry', () => { - const payload = [{ ...getEntryExistsMock() }]; + const payload = [getEntryExistsMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -106,7 +106,7 @@ describe('Entries', () => { }); test('it should validate an array with list entry', () => { - const payload = [{ ...getEntryListMock() }]; + const payload = [getEntryListMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -115,7 +115,7 @@ describe('Entries', () => { }); test('it should validate an array with nested entry', () => { - const payload = [{ ...getEntryNestedMock() }]; + const payload = [getEntryNestedMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -144,7 +144,7 @@ describe('Entries', () => { }); test('it should validate an array with nested entry', () => { - const payload = [{ ...getEntryNestedMock() }]; + const payload = [getEntryNestedMock()]; const decoded = entriesArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts index 9d5b669333db8..0eb35b0768cf4 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryExistsMock } from './entry_exists.mock'; import { EntryExists, entriesExists } from './entry_exists'; describe('entriesExists', () => { test('it should validate an entry', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesExists', () => { }); test('it should validate when "operator" is "included"', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesExists', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); payload.operator = 'excluded'; const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesExists', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string } = { ...getEntryExistsMock(), field: '', @@ -56,16 +56,16 @@ describe('entriesExists', () => { test('it should strip out extra keys', () => { const payload: EntryExists & { extraKey?: string; - } = { ...getEntryExistsMock() }; + } = getEntryExistsMock(); payload.extraKey = 'some extra key'; const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryExistsMock() }); + expect(message.schema).toEqual(getEntryExistsMock()); }); - test('it should not validate when "type" is not "exists"', () => { + test('it should FAIL validation when "type" is not "exists"', () => { const payload: Omit & { type: string } = { ...getEntryExistsMock(), type: 'match', diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts index 05c82d2532218..4d9c09cc93574 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; export const entriesExists = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts index 14857edad5e3b..834fed3550e3f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryListMock } from './entry_list.mock'; import { EntryList, entriesList } from './entry_list'; describe('entriesList', () => { test('it should validate an entry', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesList', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesList', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); payload.operator = 'excluded'; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesList', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "list" is not expected value', () => { + test('it should FAIL validation when "list" is not expected value', () => { const payload: Omit & { list: string } = { ...getEntryListMock(), list: 'someListId', @@ -55,7 +55,7 @@ describe('entriesList', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "list.id" is empty string', () => { + test('it should FAIL validation when "list.id" is empty string', () => { const payload: Omit & { list: { id: string; type: 'ip' } } = { ...getEntryListMock(), list: { id: '', type: 'ip' }, @@ -67,7 +67,7 @@ describe('entriesList', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "lists"', () => { + test('it should FAIL validation when "type" is not "lists"', () => { const payload: Omit & { type: 'match_any' } = { ...getEntryListMock(), type: 'match_any', @@ -84,12 +84,12 @@ describe('entriesList', () => { test('it should strip out extra keys', () => { const payload: EntryList & { extraKey?: string; - } = { ...getEntryListMock() }; + } = getEntryListMock(); payload.extraKey = 'some extra key'; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryListMock() }); + expect(message.schema).toEqual(getEntryListMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.ts index ae9de967db027..fcfec5e0cccdf 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator, type } from '../common/schemas'; export const entriesList = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts index 2c64592518eb7..7b49c418b547f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { EntryMatch, entriesMatch } from './entry_match'; describe('entriesMatch', () => { test('it should validate an entry', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesMatch', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesMatch', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); payload.operator = 'excluded'; const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string } = { ...getEntryMatchMock(), field: '', @@ -53,7 +53,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "value" is not string', () => { + test('it should FAIL validation when "value" is not string', () => { const payload: Omit & { value: string[] } = { ...getEntryMatchMock(), value: ['some value'], @@ -67,7 +67,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "value" is empty string', () => { + test('it should FAIL validation when "value" is empty string', () => { const payload: Omit & { value: string } = { ...getEntryMatchMock(), value: '', @@ -79,7 +79,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "match"', () => { + test('it should FAIL validation when "type" is not "match"', () => { const payload: Omit & { type: string } = { ...getEntryMatchMock(), type: 'match_any', @@ -96,12 +96,12 @@ describe('entriesMatch', () => { test('it should strip out extra keys', () => { const payload: EntryMatch & { extraKey?: string; - } = { ...getEntryMatchMock() }; + } = getEntryMatchMock(); payload.extraKey = 'some value'; const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryMatchMock() }); + expect(message.schema).toEqual(getEntryMatchMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.ts index a21f83f317e35..247d64674e27d 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; export const entriesMatch = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts index 4dab2f45711f0..628ccfd74b606 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; import { EntryMatchAny, entriesMatchAny } from './entry_match_any'; describe('entriesMatchAny', () => { test('it should validate an entry', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesMatchAny', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesMatchAny', () => { }); test('it should validate when operator is "excluded"', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); payload.operator = 'excluded'; const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when field is empty string', () => { + test('it should FAIL validation when field is empty string', () => { const payload: Omit & { field: string } = { ...getEntryMatchAnyMock(), field: '', @@ -53,7 +53,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when value is empty array', () => { + test('it should FAIL validation when value is empty array', () => { const payload: Omit & { value: string[] } = { ...getEntryMatchAnyMock(), value: [], @@ -65,7 +65,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when value is not string array', () => { + test('it should FAIL validation when value is not string array', () => { const payload: Omit & { value: string } = { ...getEntryMatchAnyMock(), value: 'some string', @@ -79,7 +79,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "match_any"', () => { + test('it should FAIL validation when "type" is not "match_any"', () => { const payload: Omit & { type: string } = { ...getEntryMatchAnyMock(), type: 'match', @@ -94,12 +94,12 @@ describe('entriesMatchAny', () => { test('it should strip out extra keys', () => { const payload: EntryMatchAny & { extraKey?: string; - } = { ...getEntryMatchAnyMock() }; + } = getEntryMatchAnyMock(); payload.extraKey = 'some extra key'; const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryMatchAnyMock() }); + expect(message.schema).toEqual(getEntryMatchAnyMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts index e93ad4aa131d1..b6c4ef509c477 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts index f645bc9e40d78..d0e7712301ee1 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts @@ -11,7 +11,7 @@ import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; export const getEntryNestedMock = (): EntryNested => ({ - entries: [{ ...getEntryMatchMock() }, { ...getEntryMatchAnyMock() }], + entries: [getEntryMatchMock(), getEntryMatchAnyMock()], field: FIELD, type: NESTED, }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts index d9b58855413b1..d77440b207d03 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryNestedMock } from './entry_nested.mock'; import { EntryNested, entriesNested } from './entry_nested'; @@ -16,7 +16,7 @@ import { getEntryExistsMock } from './entry_exists.mock'; describe('entriesNested', () => { test('it should validate a nested entry', () => { - const payload = { ...getEntryNestedMock() }; + const payload = getEntryNestedMock(); const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -24,7 +24,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate when "type" is not "nested"', () => { + test('it should FAIL validation when "type" is not "nested"', () => { const payload: Omit & { type: 'match' } = { ...getEntryNestedMock(), type: 'match', @@ -36,7 +36,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string; } = { ...getEntryNestedMock(), field: '' }; @@ -47,7 +47,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "field" is not a string', () => { + test('it should FAIL validation when "field" is not a string', () => { const payload: Omit & { field: number; } = { ...getEntryNestedMock(), field: 1 }; @@ -58,7 +58,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "entries" is not a an array', () => { + test('it should FAIL validation when "entries" is not a an array', () => { const payload: Omit & { entries: string; } = { ...getEntryNestedMock(), entries: 'im a string' }; @@ -72,7 +72,7 @@ describe('entriesNested', () => { }); test('it should validate when "entries" contains an entry item that is type "match"', () => { - const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] }; + const payload = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -92,7 +92,7 @@ describe('entriesNested', () => { }); test('it should validate when "entries" contains an entry item that is type "exists"', () => { - const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryExistsMock() }] }; + const payload = { ...getEntryNestedMock(), entries: [getEntryExistsMock()] }; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -113,12 +113,12 @@ describe('entriesNested', () => { test('it should strip out extra keys', () => { const payload: EntryNested & { extraKey?: string; - } = { ...getEntryNestedMock() }; + } = getEntryNestedMock(); payload.extraKey = 'some extra key'; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryNestedMock() }); + expect(message.schema).toEqual(getEntryNestedMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts index 9989f501d4338..f9e8e4356b811 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts index a2697286aa038..42d476a9fefb2 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -22,7 +22,7 @@ import { nonEmptyEntriesArray } from './non_empty_entries_array'; import { EntriesArray } from './entries'; describe('non_empty_entries_array', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: EntriesArray = []; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -33,7 +33,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -44,7 +44,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -56,7 +56,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }]; + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -65,7 +65,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }]; + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -74,7 +74,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }]; + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -92,7 +92,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "nested" entries', () => { - const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }]; + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -109,7 +109,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of entries of value list and non-value list entries', () => { + test('it should FAIL validation when given an array of entries of value list and non-value list entries', () => { const payload: EntriesArray = [...getListAndNonListEntriesArrayMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -118,7 +118,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of non entries', () => { + test('it should FAIL validation when given an array of non entries', () => { const payload = [1]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts index 1154f2b6098da..7dbc3465610c0 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -17,7 +17,7 @@ import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; import { EntriesArray } from './entries'; describe('non_empty_nested_entries_array', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: EntriesArray = []; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -28,7 +28,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -39,7 +39,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -51,7 +51,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }]; + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -60,7 +60,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }]; + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -69,7 +69,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }]; + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -77,8 +77,8 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of "nested" entries', () => { - const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }]; + test('it should FAIL validation when given an array of "nested" entries', () => { + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -105,9 +105,9 @@ describe('non_empty_nested_entries_array', () => { test('it should validate an array of entries', () => { const payload: EntriesArray = [ - { ...getEntryExistsMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryMatchMock() }, + getEntryExistsMock(), + getEntryMatchAnyMock(), + getEntryMatchMock(), ]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -116,7 +116,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of non entries', () => { + test('it should FAIL validation when given an array of non entries', () => { const payload = [1]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts index e3cc9104853e5..4b31b649556b2 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts @@ -7,12 +7,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; describe('nonEmptyOrNullableStringArray', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: string[] = []; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -34,7 +34,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -45,7 +45,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of with an empty string', () => { + test('it should FAIL validation when given an array of with an empty string', () => { const payload: string[] = ['im good', '']; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -56,7 +56,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of non strings', () => { + test('it should FAIL validation when given an array of non strings', () => { const payload = [1]; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts index fac088568f85e..db81b0d469859 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts @@ -7,12 +7,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { NonEmptyStringArray } from './non_empty_string_array'; describe('non_empty_string_array', () => { - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload: NonEmptyStringArray | null = null; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload: NonEmptyStringArray | undefined = undefined; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -34,7 +34,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate a single value of an empty string ""', () => { + test('it should FAIL validation of single value of an empty string ""', () => { const payload: NonEmptyStringArray = ''; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -72,7 +72,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual(['a', 'b', 'c']); }); - test('it should NOT validate a number', () => { + test('it should FAIL validation of number', () => { const payload: number = 5; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts index ac7716af40966..ac4d0304cbb8e 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './update_comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.ts index b95812cb35bf9..dc14bf480857f 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { id } from '../common/schemas'; export const updateComment = t.intersection([ diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index d661cb103fad8..203c84b2943fd 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -29,7 +29,7 @@ import { updateExceptionListItemSchema, updateExceptionListSchema, } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { AddEndpointExceptionListProps, diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts index 606109f1910c4..211b2445a0429 100644 --- a/x-pack/plugins/lists/public/lists/api.ts +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -29,7 +29,7 @@ import { listSchema, } from '../../common/schemas'; import { LIST_INDEX, LIST_ITEM_URL, LIST_PRIVILEGES_URL, LIST_URL } from '../../common/constants'; -import { validateEither } from '../../common/siem_common_deps'; +import { validateEither } from '../../common/shared_imports'; import { toError, toPromise } from '../common/fp_utils'; import { diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index 22aa1fb59858b..7fd07ed5fb8cd 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateEndpointListItemSchemaDecoded, createEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index b1e589be67cd1..91b6a328c8649 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_URL } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { createEndpointListSchema } from '../../common/schemas'; import { getExceptionListClient } from './utils/get_exception_list_client'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index ed58621dae973..fc0473b2b3704 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateExceptionListItemSchemaDecoded, createExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index fbe9c6ec9d83b..08db0825e07bd 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateExceptionListSchemaDecoded, createExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index 1bffdd6bd5b5f..be08093dc7055 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -7,7 +7,7 @@ import { IRouter } from 'kibana/server'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { LIST_INDEX } from '../../common/constants'; import { acknowledgeSchema } from '../../common/schemas'; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 656d6af2c6c9a..0a4a1c739ae7c 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -9,7 +9,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { createListItemSchema, listItemSchema } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index 297dcfc49db34..90f5bf9b2c650 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateListSchemaDecoded, createListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 2d5028bd9525a..380fdcf862060 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteEndpointListItemSchemaDecoded, deleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index 06ff051925407..07e0fad20c900 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteExceptionListItemSchemaDecoded, deleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index f2bf517f55ae3..769ce732240b7 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteExceptionListSchemaDecoded, deleteExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index be58d8aeed17d..aa587273036ae 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { acknowledgeSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index 50313cd1294ae..2284068552485 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 4eeb6d8f126ad..f87645b79fc75 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { deleteListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index 9f83761cc501a..d6a459b3ac961 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindEndpointListItemSchemaDecoded, findEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 270aad85796b2..88643e53ff0a7 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindExceptionListItemSchemaDecoded, findExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index c5cae7a1e0bb8..41342261ef681 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindExceptionListSchemaDecoded, findExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 533dc74aa3694..454ea891857c3 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindListItemSchemaDecoded, findListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index 268eb36a5e26e..d751214006dcc 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { findListSchema, foundListSchema } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index e162e7829e456..ce5fdaccae251 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { importListItemQuerySchema, listSchema } from '../../common/schemas'; import { ConfigType } from '../config'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index d975e80079ab7..58cca0313006d 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemSchema, patchListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index 421f1279f2619..e33d8d7c9c598 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, patchListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index fd932746ce990..e80347d97bb7a 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index fe8256fbda5cd..0cfac6467f089 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index 0512876d298d4..d9359881616f4 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 87a4d85e0d254..5524c1beeaa52 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemIndexExistSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index b7cf2b9f7123b..99d34d0fd84a6 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -9,7 +9,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index 4bce09ecd3bde..da3cf73b56819 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, readListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index f717dc0fb3392..e0d6a0ffffa6b 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index f5e0e7ae75700..7e15f694aee13 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 6fcee81ed573f..bead10802df4f 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index d479bc63b64bd..3490027b12747 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemSchema, updateListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index 6206c0943a8f3..816ad13d3770e 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, updateListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index bbd4b0eaf0e33..a7f5c96e13d7b 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -8,7 +8,7 @@ import { ExceptionListClient } from '../services/exception_lists/exception_list_ import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; import { foundExceptionListItemSchema } from '../../common/schemas'; import { NamespaceType } from '../../common/schemas/types'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; export const validateExceptionListSize = async ( exceptionLists: ExceptionListClient, diff --git a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts index 205d61f204ba6..5c7243a1d15a3 100644 --- a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts +++ b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts @@ -9,7 +9,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { CursorOrUndefined, SortFieldOrUndefined } from '../../../common/schemas'; -import { exactCheck } from '../../../common/siem_common_deps'; +import { exactCheck } from '../../../common/shared_imports'; /** * Used only internally for this ad-hoc opaque cursor structure to keep track of the diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index f91e272d625f6..4c829f8e75c20 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -6,8 +6,8 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; -// @ts-ignore -import turf from 'turf'; +import bbox from '@turf/bbox'; +import { multiPoint } from '@turf/helpers'; import { FeatureCollection } from 'geojson'; import { MapStoreState } from '../reducers/store'; import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../common/constants'; @@ -368,7 +368,7 @@ export function fitToDataBounds() { return; } - const dataBounds = turfBboxToBounds(turf.bbox(turf.multiPoint(corners))); + const dataBounds = turfBboxToBounds(bbox(multiPoint(corners))); dispatch(setGotoWithBounds(scaleBounds(dataBounds, FIT_TO_BOUNDS_SCALE_FACTOR))); }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index ef0cfdf0b4742..4914432f02de0 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -6,17 +6,19 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; -// @ts-ignore -import turf from 'turf'; -import uuid from 'uuid/v4'; +import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; +import uuid from 'uuid/v4'; + import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { getDataFilters, + getFilters, getMapSettings, getWaitingForMapReadyLayerListRaw, getQuery, + getTimeFilters, } from '../selectors/map_selectors'; import { CLEAR_GOTO, @@ -124,13 +126,13 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt if (extent) { let doesBufferContainExtent = false; if (buffer) { - const bufferGeometry = turf.bboxPolygon([ + const bufferGeometry = turfBboxPolygon([ buffer.minLon, buffer.minLat, buffer.maxLon, buffer.maxLat, ]); - const extentGeometry = turf.bboxPolygon([ + const extentGeometry = turfBboxPolygon([ extent.minLon, extent.minLat, extent.maxLon, @@ -217,13 +219,13 @@ export function setQuery({ dispatch({ type: SET_QUERY, - timeFilters, + timeFilters: timeFilters ? timeFilters : getTimeFilters(getState()), query: { - ...query, + ...(query ? query : getQuery(getState())), // ensure query changes to trigger re-fetch when "Refresh" clicked queryLastTriggeredAt: refresh ? generateQueryTimestamp() : prevTriggeredAt, }, - filters, + filters: filters ? filters : getFilters(getState()), }); if (getMapSettings(getState()).autoFitToDataBounds) { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 23889bdca2dd7..f5f5071bab158 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -158,7 +158,7 @@ export class VectorLayer extends AbstractLayer { async getBounds({ startLoading, stopLoading, registerCancelCallback, dataFilters }) { const isStaticLayer = !this.getSource().isBoundsAware(); - if (isStaticLayer) { + if (isStaticLayer || this.hasJoins()) { return getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 33d5deef2e39f..79eccf09b2888 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -6,7 +6,8 @@ import React from 'react'; import uuid from 'uuid/v4'; -import turf from 'turf'; +import turfBbox from '@turf/bbox'; +import { multiPoint } from '@turf/helpers'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; @@ -216,7 +217,7 @@ export class ESPewPewSource extends AbstractESAggSource { return null; } - return turfBboxToBounds(turf.bbox(turf.multiPoint(corners))); + return turfBboxToBounds(turfBbox(multiPoint(corners))); } canFormatFeatureProperties() { diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 8398bd7af39ad..147870dbef371 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; -// @ts-ignore -import turf from 'turf'; +import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; import { isRefreshOnlyQuery } from './is_refresh_only_query'; import { ISource } from '../sources/source'; @@ -27,13 +26,13 @@ export function updateDueToExtent(prevMeta: DataMeta = {}, nextMeta: DataMeta = return NO_SOURCE_UPDATE_REQUIRED; } - const previousBufferGeometry = turf.bboxPolygon([ + const previousBufferGeometry = turfBboxPolygon([ previousBuffer.minLon, previousBuffer.minLat, previousBuffer.maxLon, previousBuffer.maxLat, ]); - const newBufferGeometry = turf.bboxPolygon([ + const newBufferGeometry = turfBboxPolygon([ newBuffer.minLon, newBuffer.minLat, newBuffer.maxLon, diff --git a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts index aa78d7064fb0a..76d305f0162d2 100644 --- a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts +++ b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import turf from 'turf'; +import turfBbox from '@turf/bbox'; import { FeatureCollection } from 'geojson'; import { MapExtent } from '../../../common/descriptor_types'; import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; @@ -28,7 +27,7 @@ export function getFeatureCollectionBounds( return null; } - const bbox = turf.bbox({ + const bbox = turfBbox({ type: 'FeatureCollection', features: visibleFeatures, }); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts index f2ceb8685d43e..3e89d67e11504 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts @@ -6,9 +6,9 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -// @ts-ignore -import turf from 'turf'; -// @ts-ignore +// @ts-expect-error +import turfDistance from '@turf/distance'; +// @ts-expect-error import turfCircle from '@turf/circle'; type DrawCircleState = { @@ -75,7 +75,7 @@ export const DrawCircle = { // second click, finish draw // @ts-ignore this.updateUIClasses({ mouse: 'pointer' }); - state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [ + state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, [ e.lngLat.lng, e.lngLat.lat, ]); @@ -90,7 +90,7 @@ export const DrawCircle = { } const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; - state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation); + state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation); const newCircleFeature = turfCircle( state.circle.properties.center, state.circle.properties.radiusKm diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index 8fb0ecb50b28b..5b73cd9e77201 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -48,7 +48,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { const { addLayerWithoutDataSync, createMapStore, - getIndexPatternService, + getIndexPatternsFromIds, getQueryableUniqueIndexPatternIds, } = await lazyLoadMapModules(); const store = createMapStore(); @@ -66,17 +66,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { ); } - const promises = queryableIndexPatternIds.map(async (indexPatternId) => { - try { - // @ts-ignore - return await getIndexPatternService().get(indexPatternId); - } catch (error) { - // Unable to load index pattern, better to not throw error so map embeddable can render - // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern - return null; - } - }); - const indexPatterns = await Promise.all(promises); + const indexPatterns = await getIndexPatternsFromIds(queryableIndexPatternIds); return _.compact(indexPatterns) as IIndexPattern[]; } diff --git a/x-pack/plugins/maps/public/index_pattern_util.ts b/x-pack/plugins/maps/public/index_pattern_util.ts index e65e37ef19809..4b4bfb41990b9 100644 --- a/x-pack/plugins/maps/public/index_pattern_util.ts +++ b/x-pack/plugins/maps/public/index_pattern_util.ts @@ -30,11 +30,17 @@ export function getGeoTileAggNotSupportedReason(field: IFieldType): string | nul export async function getIndexPatternsFromIds( indexPatternIds: string[] = [] ): Promise { - const promises: Array> = []; - indexPatternIds.forEach((id) => { - promises.push(getIndexPatternService().get(id)); + const promises: IndexPattern[] = []; + indexPatternIds.forEach(async (indexPatternId) => { + try { + // @ts-ignore + promises.push(getIndexPatternService().get(indexPatternId)); + } catch (error) { + // Unable to load index pattern, better to not throw error so map can render + // Error will be surfaced by layer since it too will be unable to locate the index pattern + return null; + } }); - return await Promise.all(promises); } diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 12d6d75ac57ba..b77bf208c5865 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -8,6 +8,7 @@ import { AnyAction } from 'redux'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IndexPatternsService } from 'src/plugins/data/public/index_patterns'; import { ReactElement } from 'react'; +import { IndexPattern } from 'src/plugins/data/public'; import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapStore, MapStoreState } from '../reducers/store'; @@ -44,8 +45,9 @@ interface LazyLoadedMapModules { indexPatternId: string, indexPatternTitle: string ) => LayerDescriptor[]; - registerLayerWizard(layerWizard: LayerWizard): void; + registerLayerWizard: (layerWizard: LayerWizard) => void; registerSource(entry: SourceRegistryEntry): void; + getIndexPatternsFromIds: (indexPatternIds: string[]) => Promise; } export async function lazyLoadMapModules(): Promise { @@ -71,6 +73,7 @@ export async function lazyLoadMapModules(): Promise { createSecurityLayerDescriptors, registerLayerWizard, registerSource, + getIndexPatternsFromIds, } = await import('./lazy'); resolve({ @@ -88,6 +91,7 @@ export async function lazyLoadMapModules(): Promise { createSecurityLayerDescriptors, registerLayerWizard, registerSource, + getIndexPatternsFromIds, }); }); return loadModulesPromise; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index c839122ab90b1..e55160383a8f3 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -21,3 +21,4 @@ export * from '../../routing/maps_router'; export * from '../../classes/layers/solution_layers/security'; export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry'; export { registerSource } from '../../classes/sources/source_registry'; +export { getIndexPatternsFromIds } from '../../index_pattern_util'; diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js index cf6f51c8aeacf..5d02160fc3eb5 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js @@ -3,7 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import _ from 'lodash'; +import rison from 'rison-node'; +import { i18n } from '@kbn/i18n'; // Import each layer type, even those not used, to init in registry import '../../classes/sources/wms_source'; import '../../classes/sources/ems_file_source'; @@ -16,7 +19,7 @@ import { KibanaTilemapSource } from '../../classes/sources/kibana_tilemap_source import { TileLayer } from '../../classes/layers/tile_layer/tile_layer'; import { EMSTMSSource } from '../../classes/sources/ems_tms_source'; import { VectorTileLayer } from '../../classes/layers/vector_tile_layer/vector_tile_layer'; -import { getIsEmsEnabled } from '../../kibana_services'; +import { getIsEmsEnabled, getToasts } from '../../kibana_services'; import { getKibanaTileMap } from '../../meta'; export function getInitialLayers(layerListJSON, initialLayers = []) { @@ -41,3 +44,33 @@ export function getInitialLayers(layerListJSON, initialLayers = []) { return initialLayers; } + +export function getInitialLayersFromUrlParam() { + const locationSplit = window.location.href.split('?'); + if (locationSplit.length <= 1) { + return []; + } + const mapAppParams = new URLSearchParams(locationSplit[1]); + if (!mapAppParams.has('initialLayers')) { + return []; + } + + try { + let mapInitLayers = mapAppParams.get('initialLayers'); + if (mapInitLayers[mapInitLayers.length - 1] === '#') { + mapInitLayers = mapInitLayers.substr(0, mapInitLayers.length - 1); + } + return rison.decode_array(mapInitLayers); + } catch (e) { + getToasts().addWarning({ + title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { + defaultMessage: `Initial layers not added to map`, + }), + text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { + defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, + values: { errorMsg: e.message }, + }), + }); + return []; + } +} diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js deleted file mode 100644 index 2575bbb9df920..0000000000000 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { MapsTopNavMenu } from './top_nav_menu'; -import { - enableFullScreen, - openMapSettings, - removePreviewLayers, - setRefreshConfig, - setSelectedLayer, - updateFlyout, -} from '../../../actions'; -import { FLYOUT_STATE } from '../../../reducers/ui'; -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; -import { getFlyoutDisplay } from '../../../selectors/ui_selectors'; -import { hasDirtyState } from '../../../selectors/map_selectors'; - -function mapStateToProps(state = {}) { - return { - isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, - inspectorAdapters: getInspectorAdapters(state), - isSaveDisabled: hasDirtyState(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - closeFlyout: () => { - dispatch(setSelectedLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(removePreviewLayers()); - }, - setRefreshStoreConfig: (refreshConfig) => dispatch(setRefreshConfig(refreshConfig)), - enableFullScreen: () => dispatch(enableFullScreen()), - openMapSettings: () => dispatch(openMapSettings()), - }; -} - -const connectedMapsTopNavMenu = connect(mapStateToProps, mapDispatchToProps)(MapsTopNavMenu); -export { connectedMapsTopNavMenu as MapsTopNavMenu }; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js index 9b0d52e4fe297..c5f959c54fb66 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js @@ -9,8 +9,11 @@ import { MapsAppView } from './maps_app_view'; import { getFlyoutDisplay, getIsFullScreen } from '../../../selectors/ui_selectors'; import { getFilters, + getQuery, getQueryableUniqueIndexPatternIds, getRefreshConfig, + getTimeFilters, + hasDirtyState, hasUnsavedChanges, } from '../../../selectors/map_selectors'; import { @@ -24,13 +27,20 @@ import { setRefreshConfig, setSelectedLayer, updateFlyout, + enableFullScreen, + openMapSettings, + removePreviewLayers, } from '../../../actions'; import { FLYOUT_STATE } from '../../../reducers/ui'; import { getMapsCapabilities } from '../../../kibana_services'; +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; function mapStateToProps(state = {}) { return { isFullScreen: getIsFullScreen(state), + isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + isSaveDisabled: hasDirtyState(state), + inspectorAdapters: getInspectorAdapters(state), nextIndexPatternIds: getQueryableUniqueIndexPatternIds(state), flyoutDisplay: getFlyoutDisplay(state), refreshConfig: getRefreshConfig(state), @@ -38,17 +48,19 @@ function mapStateToProps(state = {}) { hasUnsavedChanges: (savedMap, initialLayerListConfig) => { return hasUnsavedChanges(state, savedMap, initialLayerListConfig); }, + query: getQuery(state), + timeFilters: getTimeFilters(state), }; } function mapDispatchToProps(dispatch) { return { - dispatchSetQuery: (refresh, filters, query, time) => { + dispatchSetQuery: ({ refresh, filters, query, timeFilters }) => { dispatch( setQuery({ filters, query, - timeFilters: time, + timeFilters, refresh, }) ); @@ -64,6 +76,13 @@ function mapDispatchToProps(dispatch) { dispatch(updateFlyout(FLYOUT_STATE.NONE)); dispatch(setReadOnly(!getMapsCapabilities().save)); }, + closeFlyout: () => { + dispatch(setSelectedLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removePreviewLayers()); + }, + enableFullScreen: () => dispatch(enableFullScreen()), + openMapSettings: () => dispatch(openMapSettings()), }; } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index b26c44df25104..97a08f11a6757 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -10,28 +10,28 @@ import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; import { - getIndexPatternService, - getToasts, getData, getCoreChrome, + getMapsCapabilities, + getNavigation, } from '../../../kibana_services'; import { copyPersistentState } from '../../../reducers/util'; -import { getInitialLayers } from '../../bootstrap/get_initial_layers'; -import rison from 'rison-node'; +import { getInitialLayers, getInitialLayersFromUrlParam } from '../../bootstrap/get_initial_layers'; import { getInitialTimeFilters } from '../../bootstrap/get_initial_time_filters'; import { getInitialRefreshConfig } from '../../bootstrap/get_initial_refresh_config'; import { getInitialQuery } from '../../bootstrap/get_initial_query'; -import { MapsTopNavMenu } from '../../page_elements/top_nav_menu'; import { getGlobalState, updateGlobalState, - useGlobalStateSyncing, + startGlobalStateSyncing, } from '../../state_syncing/global_sync'; import { AppStateManager } from '../../state_syncing/app_state_manager'; -import { useAppStateSyncing } from '../../state_syncing/app_sync'; +import { startAppStateSyncing } from '../../state_syncing/app_sync'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { MapContainer } from '../../../connected_components/map_container'; import { goToSpecifiedPath } from '../../maps_router'; +import { getIndexPatternsFromIds } from '../../../index_pattern_util'; +import { getTopNavConfig } from './top_nav_config'; const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', @@ -42,12 +42,12 @@ export class MapsAppView extends React.Component { _globalSyncChangeMonitorSubscription = null; _appSyncUnsubscribe = null; _appStateManager = new AppStateManager(); + _prevIndexPatternIds = null; constructor(props) { super(props); this.state = { indexPatterns: [], - prevIndexPatternIds: [], initialized: false, savedQuery: '', initialLayerListConfig: null, @@ -55,20 +55,17 @@ export class MapsAppView extends React.Component { } componentDidMount() { - // Init sync utils - // eslint-disable-next-line react-hooks/rules-of-hooks - this._globalSyncUnsubscribe = useGlobalStateSyncing(); - // eslint-disable-next-line react-hooks/rules-of-hooks - this._appSyncUnsubscribe = useAppStateSyncing(this._appStateManager); + this._isMounted = true; + + this._globalSyncUnsubscribe = startGlobalStateSyncing(); + this._appSyncUnsubscribe = startAppStateSyncing(this._appStateManager); this._globalSyncChangeMonitorSubscription = getData().query.state$.subscribe( this._updateFromGlobalState ); - // Check app state in case of refresh - const initAppState = this._appStateManager.getAppState(); - this._onQueryChange(initAppState); - if (initAppState.savedQuery) { - this._updateStateFromSavedQuery(initAppState.savedQuery); + const initialSavedQuery = this._appStateManager.getAppState().savedQuery; + if (initialSavedQuery) { + this._updateStateFromSavedQuery(initialSavedQuery); } this._initMap(); @@ -86,11 +83,12 @@ export class MapsAppView extends React.Component { } componentDidUpdate() { - // TODO: Handle null when converting to TS - this._handleStoreChanges(); + this._updateIndexPatterns(); } componentWillUnmount() { + this._isMounted = false; + if (this._globalSyncUnsubscribe) { this._globalSyncUnsubscribe(); } @@ -101,14 +99,6 @@ export class MapsAppView extends React.Component { this._globalSyncChangeMonitorSubscription.unsubscribe(); } - // Clean up app state filters - const { filterManager } = getData().query; - filterManager.filters.forEach((filter) => { - if (filter.$state.store === esFilters.FilterStateStore.APP_STATE) { - filterManager.removeFilter(filter); - } - }); - getCoreChrome().setBreadcrumbs([]); } @@ -138,183 +128,96 @@ export class MapsAppView extends React.Component { }; _updateFromGlobalState = ({ changes, state: globalState }) => { - if (!changes || !globalState) { + if (!this.state.initialized || !changes || !globalState) { return; } - const newState = {}; - Object.keys(changes).forEach((key) => { - if (changes[key]) { - newState[key] = globalState[key]; - } - }); - this.setState(newState, () => { - this._appStateManager.setQueryAndFilters({ - filters: getData().query.filterManager.getAppFilters(), - }); - const { time, filters, refreshInterval } = globalState; - this.props.dispatchSetQuery(refreshInterval, filters, this.state.query, time); - }); + this._onQueryChange({ time: globalState.time, refresh: true }); }; - _getInitialLayersFromUrlParam() { - const locationSplit = window.location.href.split('?'); - if (locationSplit.length <= 1) { - return []; - } - const mapAppParams = new URLSearchParams(locationSplit[1]); - if (!mapAppParams.has('initialLayers')) { - return []; - } + async _updateIndexPatterns() { + const { nextIndexPatternIds } = this.props; - try { - let mapInitLayers = mapAppParams.get('initialLayers'); - if (mapInitLayers[mapInitLayers.length - 1] === '#') { - mapInitLayers = mapInitLayers.substr(0, mapInitLayers.length - 1); - } - return rison.decode_array(mapInitLayers); - } catch (e) { - getToasts().addWarning({ - title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { - defaultMessage: `Initial layers not added to map`, - }), - text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { - defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, - values: { errorMsg: e.message }, - }), - }); - return []; + if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { + return; } - } - async _updateIndexPatterns(nextIndexPatternIds) { - const indexPatterns = []; - const getIndexPatternPromises = nextIndexPatternIds.map(async (indexPatternId) => { - try { - const indexPattern = await getIndexPatternService().get(indexPatternId); - indexPatterns.push(indexPattern); - } catch (err) { - // unable to fetch index pattern - } - }); + this._prevIndexPatternIds = nextIndexPatternIds; - await Promise.all(getIndexPatternPromises); - this.setState({ - indexPatterns, - }); + const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); + if (this._isMounted) { + this.setState({ indexPatterns }); + } } - _handleStoreChanges = () => { - const { prevIndexPatternIds } = this.state; - const { nextIndexPatternIds } = this.props; + _onQueryChange = ({ filters, query, time, refresh = false }) => { + const { filterManager } = getData().query; - if (nextIndexPatternIds !== prevIndexPatternIds) { - this.setState({ prevIndexPatternIds: nextIndexPatternIds }); - this._updateIndexPatterns(nextIndexPatternIds); + if (filters) { + filterManager.setFilters(filters); } - }; - - _getAppStateFilters = () => { - return this._appStateManager.getFilters() || []; - }; - _syncAppAndGlobalState = () => { - const { query, time, initialized } = this.state; - const { refreshConfig } = this.props; - const { filterManager } = getData().query; + this.props.dispatchSetQuery({ + refresh, + filters: filterManager.getFilters(), + query, + timeFilters: time, + }); - // appState + // sync appState this._appStateManager.setQueryAndFilters({ - query: query, filters: filterManager.getAppFilters(), + query, }); - // globalState - const refreshInterval = { - pause: refreshConfig.isPaused, - value: refreshConfig.interval, - }; - updateGlobalState( - { - time: time, - refreshInterval, - filters: filterManager.getGlobalFilters(), - }, - !initialized - ); - this.setState({ refreshInterval }); + // sync globalState + const updatedGlobalState = { filters: filterManager.getGlobalFilters() }; + if (time) { + updatedGlobalState.time = time; + } + updateGlobalState(updatedGlobalState, !this.state.initialized); }; - _onQueryChange = async ({ filters, query, time, refresh }) => { - const { filterManager } = getData().query; - const { dispatchSetQuery } = this.props; - const newState = {}; - let newFilters; - if (filters) { - filterManager.setFilters(filters); // Maps and merges filters - newFilters = filterManager.getFilters(); + _initMapAndLayerSettings() { + const globalState = getGlobalState(); + const mapStateJSON = this.props.savedMap.mapStateJSON; + + let savedObjectFilters = []; + if (mapStateJSON) { + const mapState = JSON.parse(mapStateJSON); + if (mapState.filters) { + savedObjectFilters = mapState.filters; + } } + const appFilters = this._appStateManager.getFilters() || []; + + const query = getInitialQuery({ + mapStateJSON, + appState: this._appStateManager.getAppState(), + }); if (query) { - newState.query = query; - } - if (time) { - newState.time = time; + getData().query.queryString.setQuery(query); } - this.setState(newState, () => { - this._syncAppAndGlobalState(); - dispatchSetQuery( - refresh, - newFilters || this.props.filters, - query || this.state.query, - time || this.state.time - ); - }); - }; - _initQueryTimeRefresh() { - const { setRefreshConfig, savedMap } = this.props; - const { queryString } = getData().query; - // TODO: Handle null when converting to TS - const globalState = getGlobalState(); - const mapStateJSON = savedMap ? savedMap.mapStateJSON : undefined; - const newState = { - query: getInitialQuery({ - mapStateJSON, - appState: this._appStateManager.getAppState(), - }), + this._onQueryChange({ + filters: [..._.get(globalState, 'filters', []), ...appFilters, ...savedObjectFilters], + query, time: getInitialTimeFilters({ mapStateJSON, globalState, }), - refreshConfig: getInitialRefreshConfig({ + }); + + this._onRefreshConfigChange( + getInitialRefreshConfig({ mapStateJSON, globalState, - }), - }; - - if (newState.query) queryString.setQuery(newState.query); - this.setState({ query: newState.query, time: newState.time }); - updateGlobalState( - { - time: newState.time, - refreshInterval: { - value: newState.refreshConfig.interval, - pause: newState.refreshConfig.isPaused, - }, - }, - !this.state.initialized + }) ); - setRefreshConfig(newState.refreshConfig); - } - - _initMapAndLayerSettings() { - const { savedMap } = this.props; - // Get saved map & layer settings - this._initQueryTimeRefresh(); const layerList = getInitialLayers( - savedMap.layerListJSON, - this._getInitialLayersFromUrlParam() + this.props.savedMap.layerListJSON, + getInitialLayersFromUrlParam() ); this.props.replaceLayerList(layerList); this.setState({ @@ -322,138 +225,135 @@ export class MapsAppView extends React.Component { }); } - _updateFiltersAndDispatch = (filters) => { + _onFiltersChange = (filters) => { this._onQueryChange({ filters, }); }; - _onRefreshChange = ({ isPaused, refreshInterval }) => { - const { refreshConfig } = this.props; - const newRefreshConfig = { - isPaused, - interval: isNaN(refreshInterval) ? refreshConfig.interval : refreshInterval, - }; - this.setState({ refreshConfig: newRefreshConfig }, this._syncAppAndGlobalState); - this.props.setRefreshConfig(newRefreshConfig); - }; + // mapRefreshConfig: MapRefreshConfig + _onRefreshConfigChange(mapRefreshConfig) { + this.props.setRefreshConfig(mapRefreshConfig); + updateGlobalState( + { + refreshInterval: { + pause: mapRefreshConfig.isPaused, + value: mapRefreshConfig.interval, + }, + }, + !this.state.initialized + ); + } + + _updateStateFromSavedQuery = (savedQuery) => { + this.setState({ savedQuery: { ...savedQuery } }); + this._appStateManager.setQueryAndFilters({ savedQuery }); - _updateStateFromSavedQuery(savedQuery) { - if (!savedQuery) { - this.setState({ savedQuery: '' }); - return; - } const { filterManager } = getData().query; const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); const allFilters = [...savedQueryFilters, ...globalFilters]; - if (savedQuery.attributes.timefilter) { - if (savedQuery.attributes.timefilter.refreshInterval) { - this._onRefreshChange({ - isPaused: savedQuery.attributes.timefilter.refreshInterval.pause, - refreshInterval: savedQuery.attributes.timefilter.refreshInterval.value, - }); - } - this._onQueryChange({ - filters: allFilters, - query: savedQuery.attributes.query, - time: savedQuery.attributes.timefilter, - }); - } else { - this._onQueryChange({ - filters: allFilters, - query: savedQuery.attributes.query, + const refreshInterval = _.get(savedQuery, 'attributes.timefilter.refreshInterval'); + if (refreshInterval) { + this._onRefreshConfigChange({ + isPaused: refreshInterval.pause, + interval: refreshInterval.value, }); } - } + this._onQueryChange({ + filters: allFilters, + query: savedQuery.attributes.query, + time: savedQuery.attributes.timefilter, + }); + }; - _syncStoreAndGetFilters() { - const { - savedMap, - setGotoWithCenter, - setMapSettings, - setIsLayerTOCOpen, - setOpenTOCDetails, - } = this.props; - let savedObjectFilters = []; - if (savedMap.mapStateJSON) { - const mapState = JSON.parse(savedMap.mapStateJSON); - setGotoWithCenter({ + _initMap() { + this._initMapAndLayerSettings(); + + this.props.clearUi(); + + if (this.props.savedMap.mapStateJSON) { + const mapState = JSON.parse(this.props.savedMap.mapStateJSON); + this.props.setGotoWithCenter({ lat: mapState.center.lat, lon: mapState.center.lon, zoom: mapState.zoom, }); - if (mapState.filters) { - savedObjectFilters = mapState.filters; - } if (mapState.settings) { - setMapSettings(mapState.settings); + this.props.setMapSettings(mapState.settings); } } - if (savedMap.uiStateJSON) { - const uiState = JSON.parse(savedMap.uiStateJSON); - setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN)); - setOpenTOCDetails(_.get(uiState, 'openTOCDetails', [])); + if (this.props.savedMap.uiStateJSON) { + const uiState = JSON.parse(this.props.savedMap.uiStateJSON); + this.props.setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN)); + this.props.setOpenTOCDetails(_.get(uiState, 'openTOCDetails', [])); } - return savedObjectFilters; - } - async _initMap() { - const { clearUi, savedMap } = this.props; - // TODO: Handle null when converting to TS - const globalState = getGlobalState(); - this._initMapAndLayerSettings(); - clearUi(); - - const savedObjectFilters = this._syncStoreAndGetFilters(savedMap); - await this._onQueryChange({ - filters: [ - ..._.get(globalState, 'filters', []), - ...this._getAppStateFilters(), - ...savedObjectFilters, - ], - }); this.setState({ initialized: true }); } _renderTopNav() { - const { query, time, savedQuery, indexPatterns } = this.state; - const { savedMap, refreshConfig, isFullScreen } = this.props; - - return !isFullScreen ? ( - { - this.setState( - { - refreshConfig: newConfig, - }, - callback - ); + if (this.props.isFullScreen) { + return null; + } + + const topNavConfig = getTopNavConfig({ + savedMap: this.props.savedMap, + isOpenSettingsDisabled: this.props.isOpenSettingsDisabled, + isSaveDisabled: this.props.isSaveDisabled, + closeFlyout: this.props.closeFlyout, + enableFullScreen: this.props.enableFullScreen, + openMapSettings: this.props.openMapSettings, + inspectorAdapters: this.props.inspectorAdapters, + setBreadcrumbs: this._setBreadcrumbs, + }); + + const { TopNavMenu } = getNavigation().ui; + return ( + { + this._onQueryChange({ + query, + time: dateRange, + refresh: true, + }); }} - indexPatterns={indexPatterns} - updateFiltersAndDispatch={this._updateFiltersAndDispatch} - onQuerySaved={(query) => { - this.setState({ savedQuery: query }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + onFiltersUpdated={this._onFiltersChange} + dateRangeFrom={this.props.timeFilters.from} + dateRangeTo={this.props.timeFilters.to} + isRefreshPaused={this.props.refreshConfig.isPaused} + refreshInterval={this.props.refreshConfig.interval} + onRefreshChange={({ isPaused, refreshInterval }) => { + this._onRefreshConfigChange({ + isPaused, + interval: refreshInterval, + }); }} - onSavedQueryUpdated={(query) => { - this.setState({ savedQuery: { ...query } }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + showSearchBar={true} + showFilterBar={true} + showDatePicker={true} + showSaveQuery={getMapsCapabilities().saveQuery} + savedQuery={this.state.savedQuery} + onSaved={this._updateStateFromSavedQuery} + onSavedQueryUpdated={this._updateStateFromSavedQuery} + onClearSavedQuery={() => { + const { filterManager, queryString } = getData().query; + this.setState({ savedQuery: '' }); + this._appStateManager.setQueryAndFilters({ savedQuery: '' }); + this._onQueryChange({ + filters: filterManager.getGlobalFilters(), + query: queryString.getDefaultQuery(), + }); }} - syncAppAndGlobalState={this._syncAppAndGlobalState} - setBreadcrumbs={this._setBreadcrumbs} /> - ) : null; + ); } render() { @@ -469,7 +369,7 @@ export class MapsAppView extends React.Component { newFilters.forEach((filter) => { filter.$state = { store: esFilters.FilterStateStore.APP_STATE }; }); - this._updateFiltersAndDispatch([...filters, ...newFilters]); + this._onFiltersChange([...filters, ...newFilters]); }} />
diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx similarity index 68% rename from x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js rename to x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx index 2340e3716547b..46d662b28a82f 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx @@ -6,122 +6,44 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { Adapters } from 'src/plugins/inspector/public'; +import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { - getNavigation, getCoreChrome, getMapsCapabilities, getInspector, getToasts, getCoreI18n, - getData, } from '../../../kibana_services'; import { SavedObjectSaveModal, + OnSaveProps, showSaveModal, } from '../../../../../../../src/plugins/saved_objects/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +// @ts-expect-error import { goToSpecifiedPath } from '../../maps_router'; +import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; -export function MapsTopNavMenu({ +export function getTopNavConfig({ savedMap, - query, - onQueryChange, - onQuerySaved, - onSavedQueryUpdated, - savedQuery, - time, - refreshConfig, - setRefreshConfig, - setRefreshStoreConfig, - indexPatterns, - updateFiltersAndDispatch, + isOpenSettingsDisabled, isSaveDisabled, closeFlyout, enableFullScreen, openMapSettings, inspectorAdapters, - syncAppAndGlobalState, setBreadcrumbs, - isOpenSettingsDisabled, +}: { + savedMap: ISavedGisMap; + isOpenSettingsDisabled: boolean; + isSaveDisabled: boolean; + closeFlyout: () => void; + enableFullScreen: () => void; + openMapSettings: () => void; + inspectorAdapters: Adapters; + setBreadcrumbs: () => void; }) { - const { TopNavMenu } = getNavigation().ui; - const { filterManager, queryString } = getData().query; - const showSaveQuery = getMapsCapabilities().saveQuery; - const onClearSavedQuery = () => { - onQuerySaved(undefined); - onQueryChange({ - filters: filterManager.getGlobalFilters(), - query: queryString.getDefaultQuery(), - }); - }; - - // Nav settings - const config = getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs - ); - - const submitQuery = function ({ dateRange, query }) { - onQueryChange({ - query, - time: dateRange, - refresh: true, - }); - }; - - const onRefreshChange = function ({ isPaused, refreshInterval }) { - const newRefreshConfig = { - isPaused, - interval: isNaN(refreshInterval) ? refreshConfig.interval : refreshInterval, - }; - setRefreshConfig(newRefreshConfig, () => { - setRefreshStoreConfig(newRefreshConfig); - syncAppAndGlobalState(); - }); - }; - - return ( - - ); -} - -function getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs -) { return [ { id: 'full-screen', @@ -193,11 +115,11 @@ function getTopNavConfig( newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate, - }) => { + }: OnSaveProps) => { const currentTitle = savedMap.title; savedMap.title = newTitle; savedMap.copyOnSave = newCopyOnSave; - const saveOptions = { + const saveOptions: SavedObjectSaveOpts = { confirmOverwrite: false, isTitleDuplicateConfirmed, onTitleDuplicate, @@ -231,7 +153,12 @@ function getTopNavConfig( ]; } -async function doSave(savedMap, saveOptions, closeFlyout, setBreadcrumbs) { +async function doSave( + savedMap: ISavedGisMap, + saveOptions: SavedObjectSaveOpts, + closeFlyout: () => void, + setBreadcrumbs: () => void +) { closeFlyout(); savedMap.syncWithStore(); let id; diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js index 19118c6130805..4cdba13bd85d2 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js @@ -14,13 +14,13 @@ export class AppStateManager { _updated$ = new Subject(); setQueryAndFilters({ query, savedQuery, filters }) { - if (this._query !== query) { + if (query && this._query !== query) { this._query = query; } - if (this._savedQuery !== savedQuery) { + if (savedQuery && this._savedQuery !== savedQuery) { this._savedQuery = savedQuery; } - if (this._filters !== filters) { + if (filters && this._filters !== filters) { this._filters = filters; } this._updated$.next(); diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js index 69d6dbbe0c4d3..60e8dc9cd574c 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js @@ -10,11 +10,15 @@ import { map } from 'rxjs/operators'; import { getData } from '../../kibana_services'; import { kbnUrlStateStorage } from '../maps_router'; -export function useAppStateSyncing(appStateManager) { +export function startAppStateSyncing(appStateManager) { // get appStateContainer // sync app filters with app state container from data.query to state container const { query } = getData(); + // Filter manager state persists across applications + // clear app state filters to prevent application filters from other applications being transfered to maps + query.filterManager.setAppFilters([]); + const stateContainer = { get: () => ({ query: appStateManager.getQuery(), diff --git a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts index b466f254e4d08..4e17241752f53 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts +++ b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts @@ -9,7 +9,7 @@ import { getData } from '../../kibana_services'; // @ts-ignore import { kbnUrlStateStorage } from '../maps_router'; -export function useGlobalStateSyncing() { +export function startGlobalStateSyncing() { const { stop } = syncQueryStateWithUrl(getData().query, kbnUrlStateStorage); return stop; } diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index cf645404860f5..cc3af9d7f4980 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -25,6 +25,7 @@ export type MlDependencies = Omit & MlStartDepende interface AppProps { coreStart: CoreStart; deps: MlDependencies; + appMountParams: AppMountParameters; } const localStorage = new Storage(window.localStorage); @@ -46,8 +47,9 @@ export interface MlServicesContext { export type MlGlobalServices = ReturnType; -const App: FC = ({ coreStart, deps }) => { +const App: FC = ({ coreStart, deps, appMountParams }) => { const pageDeps = { + history: appMountParams.history, indexPatterns: deps.data.indexPatterns, config: coreStart.uiSettings!, setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, @@ -104,7 +106,11 @@ export const renderApp = ( appMountParams.onAppLeave((actions) => actions.default()); const mlLicense = setLicenseCache(deps.licensing, [ - () => ReactDOM.render(, appMountParams.element), + () => + ReactDOM.render( + , + appMountParams.element + ), ]); return () => { diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 4850d583a626c..f603264896cd3 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -62,6 +62,8 @@ class LinksMenuUI extends Component { const timestamp = record.timestamp; const configuredUrlValue = customUrl.url_value; const timeRangeInterval = parseInterval(customUrl.time_range); + const basePath = this.props.kibana.services.http.basePath.get(); + if (configuredUrlValue.includes('$earliest$')) { let earliestMoment = moment(timestamp); if (timeRangeInterval !== null) { @@ -117,7 +119,7 @@ class LinksMenuUI extends Component { // Replace any tokens in the configured url_value with values from the source record, // and then open link in a new tab/window. const urlPath = replaceStringTokens(customUrl.url_value, record, true); - openCustomUrlWindow(urlPath, customUrl); + openCustomUrlWindow(urlPath, customUrl, basePath); }) .catch((resp) => { console.log('openCustomUrl(): error loading categoryDefinition:', resp); @@ -136,7 +138,7 @@ class LinksMenuUI extends Component { // Replace any tokens in the configured url_value with values from the source record, // and then open link in a new tab/window. const urlPath = getUrlForRecord(customUrl, record); - openCustomUrlWindow(urlPath, customUrl); + openCustomUrlWindow(urlPath, customUrl, basePath); } }; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index 0f071a42a5688..8a43ae12deb21 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -5,6 +5,7 @@ */ export { useMlKibana, StartServices, MlKibanaReactContextValue } from './kibana_context'; +export { useNavigateToPath, NavigateToPath } from './use_navigate_to_path'; export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts new file mode 100644 index 0000000000000..f2db970bf5057 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { PLUGIN_ID } from '../../../../common/constants/app'; + +import { useMlKibana } from './kibana_context'; + +export type NavigateToPath = ReturnType; + +export const useNavigateToPath = () => { + const { + services: { + application: { getUrlForApp, navigateToUrl }, + }, + } = useMlKibana(); + + const location = useLocation(); + + return useMemo( + () => (path: string | undefined, preserveSearch = false) => { + navigateToUrl( + getUrlForApp(PLUGIN_ID, { + path: `${path}${preserveSearch === true ? location.search : ''}`, + }) + ); + }, + [location] + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx index b6b335afa53f5..183cbe084f9b3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -7,17 +7,13 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiHorizontalRule, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; export const BackToListPanel: FC = () => { - const { - services: { - application: { navigateToUrl }, - }, - } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const redirectToAnalyticsManagementPage = async () => { - await navigateToUrl('#/data_frame_analytics?'); + await navigateToPath('/data_frame_analytics'); }; return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 0ac237bb33e76..1d6a603caa817 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -44,7 +44,7 @@ export const DetailsStepForm: FC = ({ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setFormState } = actions; - const { form, cloneJob, isJobCreated } = state; + const { form, cloneJob, hasSwitchedToEditor, isJobCreated } = state; const { createIndexPattern, description, @@ -61,7 +61,9 @@ export const DetailsStepForm: FC = ({ resultsField, } = form; - const [destIndexSameAsId, setDestIndexSameAsId] = useState(cloneJob === undefined); + const [destIndexSameAsId, setDestIndexSameAsId] = useState( + cloneJob === undefined && hasSwitchedToEditor === false + ); const forceInput = useRef(null); @@ -90,7 +92,11 @@ export const DetailsStepForm: FC = ({ useEffect(() => { if (destinationIndexNameValid === true) { debouncedIndexCheck(); - } else if (destinationIndex.trim() === '' && destinationIndexNameExists === true) { + } else if ( + typeof destinationIndex === 'string' && + destinationIndex.trim() === '' && + destinationIndexNameExists === true + ) { setFormState({ destinationIndexNameExists: false }); } @@ -102,7 +108,7 @@ export const DetailsStepForm: FC = ({ useEffect(() => { if (destIndexSameAsId === true && !jobIdEmpty && jobIdValid) { setFormState({ destinationIndex: jobId }); - } else if (destIndexSameAsId === false) { + } else if (destIndexSameAsId === false && hasSwitchedToEditor === false) { setFormState({ destinationIndex: '' }); } }, [destIndexSameAsId, jobId]); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index 04dd25896d443..2f0e2ed3428c0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -6,7 +6,6 @@ import React, { FC, useEffect, useState } from 'react'; import { - EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -16,7 +15,7 @@ import { EuiSpacer, EuiSteps, EuiStepStatus, - EuiText, + EuiSwitch, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -48,9 +47,15 @@ export const Page: FC = ({ jobId }) => { const { currentIndexPattern } = mlContext; const createAnalyticsForm = useCreateAnalyticsForm(); - const { isAdvancedEditorEnabled } = createAnalyticsForm.state; - const { jobType } = createAnalyticsForm.state.form; - const { initiateWizard, setJobClone, switchToAdvancedEditor } = createAnalyticsForm.actions; + const { state } = createAnalyticsForm; + const { isAdvancedEditorEnabled, disableSwitchToForm } = state; + const { jobType } = state.form; + const { + initiateWizard, + setJobClone, + switchToAdvancedEditor, + switchToForm, + } = createAnalyticsForm.actions; useEffect(() => { initiateWizard(); @@ -170,34 +175,40 @@ export const Page: FC = ({ jobId }) => { - {isAdvancedEditorEnabled === false && ( - - + + - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.create.switchToJsonEditorSwitch', - { - defaultMessage: 'Switch to json editor', - } - )} - - - - - )} + checked={isAdvancedEditorEnabled} + onChange={(e) => { + if (e.target.checked === true) { + switchToAdvancedEditor(); + } else { + switchToForm(); + } + }} + data-test-subj="mlAnalyticsCreateJobWizardAdvancedEditorSwitch" + /> + + {isAdvancedEditorEnabled === true && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx index 7a409e5238a57..010aa7b8513b5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx @@ -13,7 +13,7 @@ import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, @@ -350,11 +350,11 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { export const useNavigateToWizardWithClonedJob = () => { const { services: { - application: { navigateToUrl }, notifications: { toasts }, savedObjects, }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const savedObjectsClient = savedObjects.client; @@ -395,8 +395,8 @@ export const useNavigateToWizardWithClonedJob = () => { } if (sourceIndexId) { - await navigateToUrl( - `ml#/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ + await navigateToPath( + `/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ item.config.id }` ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx index e31670ea42ceb..e123af204b515 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx @@ -12,11 +12,9 @@ import { DataFrameAnalyticsListRow } from '../analytics_list/common'; import { ViewButton } from './view_button'; -export const getViewAction = ( - isManagementTable: boolean = false -): EuiTableActionsColumnType['actions'][number] => ({ +export const getViewAction = (): EuiTableActionsColumnType< + DataFrameAnalyticsListRow +>['actions'][number] => ({ isPrimary: true, - render: (item: DataFrameAnalyticsListRow) => ( - - ), + render: (item: DataFrameAnalyticsListRow) => , }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx index a0790cd802409..9472a3af852fc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { getAnalysisType } from '../../../../common/analytics'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; import { getResultsUrl, DataFrameAnalyticsListRow } from '../analytics_list/common'; @@ -17,23 +17,15 @@ import { getViewLinkStatus } from './get_view_link_status'; interface ViewButtonProps { item: DataFrameAnalyticsListRow; - isManagementTable: boolean; } -export const ViewButton: FC = ({ item, isManagementTable }) => { - const { - services: { - application: { navigateToUrl, navigateToApp }, - }, - } = useMlKibana(); +export const ViewButton: FC = ({ item }) => { + const navigateToPath = useNavigateToPath(); const { disabled, tooltipContent } = getViewLinkStatus(item); const analysisType = getAnalysisType(item.config.analysis); - const url = getResultsUrl(item.id, analysisType); - const navigator = isManagementTable - ? () => navigateToApp('ml', { path: url }) - : () => navigateToUrl(url); + const onClickHandler = () => navigateToPath(getResultsUrl(item.id, analysisType)); const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', { defaultMessage: 'View', @@ -47,7 +39,7 @@ export const ViewButton: FC = ({ item, isManagementTable }) => flush="left" iconType="visTable" isDisabled={disabled} - onClick={navigator} + onClick={onClickHandler} size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx index bc02c81bac0f0..373b9991d4d3c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -43,7 +43,7 @@ export const useActions = ( let modals: JSX.Element | null = null; const actions: EuiTableActionsColumnType['actions'] = [ - getViewAction(isManagementTable), + getViewAction(), ]; // isManagementTable will be the same for the lifecycle of the component 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 b03a58a02309d..29d495062e309 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 @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; const fixedPageSize: number = 8; @@ -27,16 +27,13 @@ interface Props { export const SourceSelection: FC = ({ onClose }) => { const { - services: { - application: { navigateToUrl }, - savedObjects, - uiSettings, - }, + services: { savedObjects, uiSettings }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const onSearchSelected = async (id: string, type: string) => { - await navigateToUrl( - `ml#/data_frame_analytics/new_job?${ + await navigateToPath( + `/data_frame_analytics/new_job?${ type === 'index-pattern' ? 'index' : 'savedSearchId' }=${encodeURIComponent(id)}` ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts index 4bfee9f308313..5f3045696f170 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -25,6 +25,7 @@ export enum ACTION { SET_JOB_CONFIG, SET_JOB_IDS, SWITCH_TO_ADVANCED_EDITOR, + SWITCH_TO_FORM, SET_ESTIMATED_MODEL_MEMORY_LIMIT, SET_JOB_CLONE, } @@ -38,7 +39,8 @@ export type Action = | ACTION.OPEN_MODAL | ACTION.RESET_ADVANCED_EDITOR_MESSAGES | ACTION.RESET_FORM - | ACTION.SWITCH_TO_ADVANCED_EDITOR; + | ACTION.SWITCH_TO_ADVANCED_EDITOR + | ACTION.SWITCH_TO_FORM; } // Actions with custom payloads: | { type: ACTION.ADD_REQUEST_MESSAGE; requestMessage: FormMessage } @@ -71,6 +73,7 @@ export interface ActionDispatchers { setJobConfig: (payload: State['jobConfig']) => void; startAnalyticsJob: () => void; switchToAdvancedEditor: () => void; + switchToForm: () => void; setEstimatedModelMemoryLimit: (value: State['estimatedModelMemoryLimit']) => void; setJobClone: (cloneJob: DeepReadonly) => Promise; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index acdaf15cdf4b7..8d8421a116b91 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -8,13 +8,17 @@ import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; // @ts-ignore import numeral from '@elastic/numeral'; -import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; import { Action, ACTION } from './actions'; -import { getInitialState, getJobConfigFromFormState, State } from './state'; +import { + getInitialState, + getFormStateFromJobConfig, + getJobConfigFromFormState, + State, +} from './state'; import { isJobIdValid, validateModelMemoryLimitUnits, @@ -41,6 +45,7 @@ import { TRAINING_PERCENT_MAX, } from '../../../../common/analytics'; import { indexPatterns } from '../../../../../../../../../../src/plugins/data/public'; +import { isAdvancedConfig } from '../../components/action_clone/clone_button'; const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( ', ' @@ -458,13 +463,16 @@ export function reducer(state: State, action: Action): State { case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: let resultJobConfig; + let disableSwitchToForm = false; try { resultJobConfig = JSON.parse(collapseLiteralStrings(action.advancedEditorRawString)); + disableSwitchToForm = isAdvancedConfig(resultJobConfig); } catch (e) { return { ...state, advancedEditorRawString: action.advancedEditorRawString, isAdvancedEditorValidJson: false, + disableSwitchToForm: true, advancedEditorMessages: [], }; } @@ -473,6 +481,7 @@ export function reducer(state: State, action: Action): State { ...validateAdvancedEditor({ ...state, jobConfig: resultJobConfig }), advancedEditorRawString: action.advancedEditorRawString, isAdvancedEditorValidJson: true, + disableSwitchToForm, }; case ACTION.SET_FORM_STATE: @@ -538,17 +547,53 @@ export function reducer(state: State, action: Action): State { case ACTION.SWITCH_TO_ADVANCED_EDITOR: let { jobConfig } = state; - const isJobConfigEmpty = isEmpty(state.jobConfig); - if (isJobConfigEmpty) { - jobConfig = getJobConfigFromFormState(state.form); - } + jobConfig = getJobConfigFromFormState(state.form); + const shouldDisableSwitchToForm = isAdvancedConfig(jobConfig); + return validateAdvancedEditor({ ...state, advancedEditorRawString: JSON.stringify(jobConfig, null, 2), isAdvancedEditorEnabled: true, + disableSwitchToForm: shouldDisableSwitchToForm, + hasSwitchedToEditor: true, jobConfig, }); + case ACTION.SWITCH_TO_FORM: + const { jobConfig: config, jobIds } = state; + const { jobId } = state.form; + // @ts-ignore + const formState = getFormStateFromJobConfig(config, false); + + if (typeof jobId === 'string' && jobId.trim() !== '') { + formState.jobId = jobId; + } + + formState.jobIdExists = jobIds.some((id) => formState.jobId === id); + formState.jobIdEmpty = jobId === ''; + formState.jobIdValid = isJobIdValid(jobId); + formState.jobIdInvalidMaxLength = !!maxLengthValidator(JOB_ID_MAX_LENGTH)(jobId); + + formState.destinationIndexNameEmpty = formState.destinationIndex === ''; + formState.destinationIndexNameValid = isValidIndexName(formState.destinationIndex || ''); + formState.destinationIndexPatternTitleExists = + state.indexPatternsMap[formState.destinationIndex || ''] !== undefined; + + if (formState.numTopFeatureImportanceValues !== undefined) { + formState.numTopFeatureImportanceValuesValid = validateNumTopFeatureImportanceValues( + formState.numTopFeatureImportanceValues + ); + } + + return validateForm({ + ...state, + // @ts-ignore + form: formState, + isAdvancedEditorEnabled: false, + advancedEditorRawString: JSON.stringify(config, null, 2), + jobConfig: config, + }); + case ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT: return { ...state, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts index d397dfc315da4..499318ebddc19 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getCloneFormStateFromJobConfig, - getInitialState, - getJobConfigFromFormState, -} from './state'; +import { getFormStateFromJobConfig, getInitialState, getJobConfigFromFormState } from './state'; const regJobConfig = { id: 'reg-test-01', @@ -96,8 +92,8 @@ describe('useCreateAnalyticsForm', () => { ]); }); - test('state: getCloneFormStateFromJobConfig() regression', () => { - const clonedState = getCloneFormStateFromJobConfig(regJobConfig); + test('state: getFormStateFromJobConfig() regression', () => { + const clonedState = getFormStateFromJobConfig(regJobConfig); expect(clonedState?.sourceIndex).toBe('reg-test-index'); expect(clonedState?.includes).toStrictEqual([]); @@ -112,8 +108,8 @@ describe('useCreateAnalyticsForm', () => { expect(clonedState?.jobId).toBe(undefined); }); - test('state: getCloneFormStateFromJobConfig() outlier detection', () => { - const clonedState = getCloneFormStateFromJobConfig(outlierJobConfig); + test('state: getFormStateFromJobConfig() outlier detection', () => { + const clonedState = getFormStateFromJobConfig(outlierJobConfig); expect(clonedState?.sourceIndex).toBe('outlier-test-index'); expect(clonedState?.includes).toStrictEqual(['field', 'other_field']); 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 725fc8751408e..69599f43ef297 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 @@ -12,6 +12,7 @@ import { DataFrameAnalyticsId, DataFrameAnalyticsConfig, ANALYSIS_CONFIG_TYPE, + defaultSearchQuery, } from '../../../../common/analytics'; import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; @@ -44,6 +45,7 @@ export interface FormMessage { export interface State { advancedEditorMessages: FormMessage[]; advancedEditorRawString: string; + disableSwitchToForm: boolean; form: { computeFeatureInfluence: string; createIndexPattern: boolean; @@ -97,6 +99,7 @@ export interface State { indexPatternsMap: SourceIndexMap; isAdvancedEditorEnabled: boolean; isAdvancedEditorValidJson: boolean; + hasSwitchedToEditor: boolean; isJobCreated: boolean; isJobStarted: boolean; isValid: boolean; @@ -110,6 +113,7 @@ export interface State { export const getInitialState = (): State => ({ advancedEditorMessages: [], advancedEditorRawString: '', + disableSwitchToForm: false, form: { computeFeatureInfluence: 'true', createIndexPattern: true, @@ -131,7 +135,7 @@ export const getInitialState = (): State => ({ jobIdInvalidMaxLength: false, jobIdValid: false, jobType: undefined, - jobConfigQuery: { match_all: {} }, + jobConfigQuery: defaultSearchQuery, jobConfigQueryString: undefined, lambda: undefined, loadingFieldOptions: false, @@ -167,6 +171,7 @@ export const getInitialState = (): State => ({ indexPatternsMap: {}, isAdvancedEditorEnabled: false, isAdvancedEditorValidJson: true, + hasSwitchedToEditor: false, isJobCreated: false, isJobStarted: false, isValid: false, @@ -283,8 +288,9 @@ function toCamelCase(property: string): string { * Extracts form state for a job clone from the analytics job configuration. * For cloning we keep job id and destination index empty. */ -export function getCloneFormStateFromJobConfig( - analyticsJobConfig: Readonly +export function getFormStateFromJobConfig( + analyticsJobConfig: Readonly, + isClone: boolean = true ): Partial { const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE; @@ -300,6 +306,10 @@ export function getCloneFormStateFromJobConfig( includes: analyticsJobConfig.analyzed_fields.includes, }; + if (isClone === false) { + resultState.destinationIndex = analyticsJobConfig?.dest.index ?? ''; + } + const analysisConfig = analyticsJobConfig.analysis[jobType]; for (const key in analysisConfig) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 035610684d556..9612b9213d120 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -28,7 +28,7 @@ import { FormMessage, State, SourceIndexMap, - getCloneFormStateFromJobConfig, + getFormStateFromJobConfig, } from './state'; import { ANALYTICS_STEPS } from '../../../analytics_creation/page'; @@ -283,6 +283,10 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { dispatch({ type: ACTION.SWITCH_TO_ADVANCED_EDITOR }); }; + const switchToForm = () => { + dispatch({ type: ACTION.SWITCH_TO_FORM }); + }; + const setEstimatedModelMemoryLimit = (value: State['estimatedModelMemoryLimit']) => { dispatch({ type: ACTION.SET_ESTIMATED_MODEL_MEMORY_LIMIT, value }); }; @@ -294,7 +298,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { setJobConfig(config); switchToAdvancedEditor(); } else { - setFormState(getCloneFormStateFromJobConfig(config)); + setFormState(getFormStateFromJobConfig(config)); setEstimatedModelMemoryLimit(config.model_memory_limit); } @@ -311,6 +315,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { setJobConfig, startAnalyticsJob, switchToAdvancedEditor, + switchToForm, setEstimatedModelMemoryLimit, setJobClone, }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index fd86d9f48f46d..769b83c03110b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { isFullLicense } from '../license'; -import { useTimefilter, useMlKibana } from '../contexts/kibana'; +import { useTimefilter, useMlKibana, useNavigateToPath } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; import { getMaxBytesFormatted } from './file_based/components/utils'; @@ -54,6 +54,7 @@ export const DatavisualizerSelector: FC = () => { const { services: { licenseManagement }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const startTrialVisible = licenseManagement !== undefined && @@ -124,7 +125,7 @@ export const DatavisualizerSelector: FC = () => { footer={ navigateToPath('/filedatavisualizer')} data-test-subj="mlDataVisualizerUploadFileButton" > { footer={ navigateToPath('/datavisualizer_index_select')} data-test-subj="mlDataVisualizerSelectIndexButton" > = ({ job, customUrls, setCustomUrls }) => { const { - services: { notifications }, + services: { http, notifications }, } = useMlKibana(); const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); @@ -103,7 +103,7 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust if (index < customUrls.length) { getTestUrl(job, customUrls[index]) .then((testUrl) => { - openCustomUrlWindow(testUrl, customUrls[index]); + openCustomUrlWindow(testUrl, customUrls[index], http.basePath.get()); }) .catch((resp) => { // eslint-disable-next-line no-console 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 7af27fc22e34c..468efcf013e9b 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 @@ -160,13 +160,16 @@ class CustomUrlsUI extends Component { }; onTestButtonClick = () => { - const { toasts } = this.props.kibana.services.notifications; + const { + http: { basePath }, + notifications: { toasts }, + } = this.props.kibana.services; const job = this.props.job; buildCustomUrlFromSettings(this.state.editorSettings as CustomUrlSettings) .then((customUrl) => { getTestUrl(job, customUrl) .then((testUrl) => { - openCustomUrlWindow(testUrl, customUrl); + openCustomUrlWindow(testUrl, customUrl, basePath.get()); }) .catch((resp) => { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 92e65e580fc01..8ab45dc24aa17 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -5,8 +5,10 @@ */ import { i18n } from '@kbn/i18n'; + import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { NavigateToPath } from '../../../../../contexts/kibana'; import { ML_JOB_AGGREGATION, SPARSE_DATA_AGGREGATIONS, @@ -20,12 +22,7 @@ import { mlCategory, } from '../../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; -import { - JobCreatorType, - isMultiMetricJobCreator, - isPopulationJobCreator, - isCategorizationJobCreator, -} from '../index'; +import { JobCreatorType } from '../index'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job'; const getFieldByIdFactory = (additionalFields: Field[]) => (id: string) => { @@ -247,43 +244,33 @@ function stashCombinedJob( mlJobService.tempJobCloningObjects.calendars = jobCreator.calendars; } -export function convertToMultiMetricJob(jobCreator: JobCreatorType) { +export function convertToMultiMetricJob( + jobCreator: JobCreatorType, + navigateToPath: NavigateToPath +) { jobCreator.createdBy = CREATED_BY_LABEL.MULTI_METRIC; jobCreator.modelPlot = false; stashCombinedJob(jobCreator, true, true); - window.location.href = window.location.href.replace( - JOB_TYPE.SINGLE_METRIC, - JOB_TYPE.MULTI_METRIC - ); + navigateToPath(`jobs/new_job/${JOB_TYPE.MULTI_METRIC}`, true); } -export function convertToAdvancedJob(jobCreator: JobCreatorType) { +export function convertToAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.createdBy = null; stashCombinedJob(jobCreator, true, true); - let jobType = JOB_TYPE.SINGLE_METRIC; - if (isMultiMetricJobCreator(jobCreator)) { - jobType = JOB_TYPE.MULTI_METRIC; - } else if (isPopulationJobCreator(jobCreator)) { - jobType = JOB_TYPE.POPULATION; - } else if (isCategorizationJobCreator(jobCreator)) { - jobType = JOB_TYPE.CATEGORIZATION; - } - - window.location.href = window.location.href.replace(jobType, JOB_TYPE.ADVANCED); + navigateToPath(`jobs/new_job/${JOB_TYPE.ADVANCED}`, true); } -export function resetJob(jobCreator: JobCreatorType) { +export function resetJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.jobId = ''; stashCombinedJob(jobCreator, true, true); - - window.location.href = '#/jobs/new_job'; + navigateToPath('/jobs/new_job'); } -export function advancedStartDatafeed(jobCreator: JobCreatorType) { +export function advancedStartDatafeed(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { stashCombinedJob(jobCreator, false, false); - window.location.href = '#/jobs'; + navigateToPath('/jobs'); } export function aggFieldPairsCanBeCharted(afs: AggFieldPair[]) { 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 new file mode 100644 index 0000000000000..0dd802855ea67 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect, useMemo, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiLoadingSpinner, + EuiButton, +} from '@elastic/eui'; + +import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; +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'; + +export const DatafeedPreview: FC<{ + combinedJob: CombinedJob | null; + heightOffset?: number; +}> = ({ combinedJob, heightOffset = 0 }) => { + // the ace editor requires a fixed height + const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ + heightOffset, + ]); + const [loading, setLoading] = useState(false); + const [previewJsonString, setPreviewJsonString] = useState(''); + const [outOfDate, setOutOfDate] = useState(false); + const [combinedJobString, setCombinedJobString] = useState(''); + + useEffect(() => { + try { + if (combinedJob !== null) { + if (combinedJobString === '') { + // first time, set the string and load the preview + loadDataPreview(); + } else { + setOutOfDate(JSON.stringify(combinedJob) !== combinedJobString); + } + } + } catch (error) { + // fail silently + } + }, [combinedJob]); + + const loadDataPreview = useCallback(async () => { + setPreviewJsonString(''); + if (combinedJob === null) { + return; + } + + setLoading(true); + setCombinedJobString(JSON.stringify(combinedJob)); + + if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { + try { + const resp = await mlJobService.searchPreview(combinedJob); + const data = resp.aggregations + ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) + : resp.hits.hits; + + setPreviewJsonString(JSON.stringify(data, null, 2)); + } catch (error) { + setPreviewJsonString(JSON.stringify(error, null, 2)); + } + setLoading(false); + setOutOfDate(false); + } else { + const errorText = i18n.translate( + 'xpack.ml.newJob.wizard.datafeedPreviewFlyout.datafeedDoesNotExistLabel', + { + defaultMessage: 'Datafeed does not exist', + } + ); + setPreviewJsonString(errorText); + } + }, [combinedJob]); + + return ( + + + + +
+ +
+
+
+ + {outOfDate && ( + + Refresh + + )} + +
+ + {loading === true ? ( + + + + + + + ) : ( + + )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx index 03be38adfbbe0..d35083ec6e479 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useContext, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { Fragment, FC, useState, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, @@ -13,18 +12,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, - EuiTitle, EuiFlyoutBody, - EuiSpacer, - EuiLoadingSpinner, } from '@elastic/eui'; -import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; -import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; + import { JobCreatorContext } from '../../job_creator_context'; -import { mlJobService } from '../../../../../../services/job_service'; -import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; +import { DatafeedPreview } from './datafeed_preview'; -const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { HIDDEN, READONLY, @@ -36,50 +29,11 @@ interface Props { export const DatafeedPreviewFlyout: FC = ({ isDisabled }) => { const { jobCreator } = useContext(JobCreatorContext); const [showFlyout, setShowFlyout] = useState(false); - const [previewJsonString, setPreviewJsonString] = useState(''); - const [loading, setLoading] = useState(false); function toggleFlyout() { setShowFlyout(!showFlyout); } - useEffect(() => { - if (showFlyout === true) { - loadDataPreview(); - } - }, [showFlyout]); - - async function loadDataPreview() { - setLoading(true); - setPreviewJsonString(''); - const combinedJob: CombinedJob = { - ...jobCreator.jobConfig, - datafeed_config: jobCreator.datafeedConfig, - }; - - if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { - try { - const resp = await mlJobService.searchPreview(combinedJob); - const data = resp.aggregations - ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) - : resp.hits.hits; - - setPreviewJsonString(JSON.stringify(data, null, 2)); - } catch (error) { - setPreviewJsonString(JSON.stringify(error, null, 2)); - } - setLoading(false); - } else { - const errorText = i18n.translate( - 'xpack.ml.newJob.wizard.datafeedPreviewFlyout.datafeedDoesNotExistLabel', - { - defaultMessage: 'Datafeed does not exist', - } - ); - setPreviewJsonString(errorText); - } - } - return ( @@ -87,12 +41,11 @@ export const DatafeedPreviewFlyout: FC = ({ isDisabled }) => { {showFlyout === true && isDisabled === false && ( setShowFlyout(false)} hideCloseButton size="m"> - @@ -127,28 +80,3 @@ const FlyoutButton: FC<{ isDisabled: boolean; onClick(): void }> = ({ isDisabled ); }; - -const Contents: FC<{ - title: string; - value: string; - loading: boolean; -}> = ({ title, value, loading }) => { - return ( - - -
{title}
-
- - {loading === true ? ( - - - - - - - ) : ( - - )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts index d52ed1364452a..e96f374213eb3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts @@ -4,3 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export { DatafeedPreviewFlyout } from './datafeed_preview_flyout'; +export { DatafeedPreview } from './datafeed_preview'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index dd5c8aa3e280a..29d55e6ae48e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useContext, useEffect } from 'react'; +import React, { Fragment, FC, useState, useContext, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -17,19 +17,21 @@ import { EuiTitle, EuiFlyoutBody, EuiSpacer, + EuiCallOut, } from '@elastic/eui'; import { collapseLiteralStrings } from '../../../../../../../../shared_imports'; -import { Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; +import { CombinedJob, Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; import { ML_EDITOR_MODE, MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { isValidJson } from '../../../../../../../../common/util/validation_utils'; import { JobCreatorContext } from '../../job_creator_context'; +import { DatafeedPreview } from '../datafeed_preview_flyout'; -const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { HIDDEN, READONLY, EDITABLE, } +const WARNING_CALLOUT_OFFSET = 100; interface Props { isDisabled: boolean; jobEditorMode: EDITOR_MODE; @@ -38,21 +40,38 @@ interface Props { export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafeedEditorMode }) => { const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const [showJsonFlyout, setShowJsonFlyout] = useState(false); + const [showChangedIndicesWarning, setShowChangedIndicesWarning] = useState(false); const [jobConfigString, setJobConfigString] = useState(jobCreator.formattedJobJson); const [datafeedConfigString, setDatafeedConfigString] = useState( jobCreator.formattedDatafeedJson ); const [saveable, setSaveable] = useState(false); + const [tempCombinedJob, setTempCombinedJob] = useState(null); useEffect(() => { setJobConfigString(jobCreator.formattedJobJson); setDatafeedConfigString(jobCreator.formattedDatafeedJson); }, [jobCreatorUpdated]); + useEffect(() => { + if (showJsonFlyout === true) { + // when the flyout opens, update the JSON + setJobConfigString(jobCreator.formattedJobJson); + setDatafeedConfigString(jobCreator.formattedDatafeedJson); + setTempCombinedJob({ + ...JSON.parse(jobCreator.formattedJobJson), + datafeed_config: JSON.parse(jobCreator.formattedDatafeedJson), + }); + + setShowChangedIndicesWarning(false); + } else { + setTempCombinedJob(null); + } + }, [showJsonFlyout]); + const editJsonMode = - jobEditorMode === EDITOR_MODE.HIDDEN || datafeedEditorMode === EDITOR_MODE.HIDDEN; - const flyOutSize = editJsonMode ? 'm' : 'l'; + jobEditorMode === EDITOR_MODE.EDITABLE || datafeedEditorMode === EDITOR_MODE.EDITABLE; const readOnlyMode = jobEditorMode === EDITOR_MODE.READONLY && datafeedEditorMode === EDITOR_MODE.READONLY; @@ -64,6 +83,14 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee function onJobChange(json: string) { setJobConfigString(json); const valid = isValidJson(json); + setTempCombinedJob( + valid + ? { + ...JSON.parse(json), + datafeed_config: JSON.parse(datafeedConfigString), + } + : null + ); setSaveable(valid); } @@ -73,12 +100,22 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee let valid = isValidJson(jsonValue); if (valid) { // ensure that the user hasn't altered the indices list in the json. - const { indices }: Datafeed = JSON.parse(jsonValue); + const datafeed: Datafeed = JSON.parse(jsonValue); const originalIndices = jobCreator.indices.sort(); valid = - originalIndices.length === indices.length && - originalIndices.every((value, index) => value === indices[index]); + originalIndices.length === datafeed.indices.length && + originalIndices.every((value, index) => value === datafeed.indices[index]); + setShowChangedIndicesWarning(valid === false); + + setTempCombinedJob({ + ...JSON.parse(jobConfigString), + datafeed_config: datafeed, + }); + } else { + setShowChangedIndicesWarning(false); + setTempCombinedJob(null); } + setSaveable(valid); } @@ -99,7 +136,7 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee /> {showJsonFlyout === true && isDisabled === false && ( - setShowJsonFlyout(false)} hideCloseButton size={flyOutSize}> + setShowJsonFlyout(false)} hideCloseButton size={'l'}> {jobEditorMode !== EDITOR_MODE.HIDDEN && ( @@ -110,19 +147,51 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee defaultMessage: 'Job configuration JSON', })} value={jobConfigString} + heightOffset={showChangedIndicesWarning ? WARNING_CALLOUT_OFFSET : 0} /> )} {datafeedEditorMode !== EDITOR_MODE.HIDDEN && ( - + <> + + {datafeedEditorMode === EDITOR_MODE.EDITABLE && ( + + + + )} + )} + {showChangedIndicesWarning && ( + <> + + + + + + )} @@ -183,7 +252,12 @@ const Contents: FC<{ value: string; editJson: boolean; onChange(s: string): void; -}> = ({ title, value, editJson, onChange }) => { + heightOffset?: number; +}> = ({ title, value, editJson, onChange, heightOffset = 0 }) => { + // the ace editor requires a fixed height + const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ + heightOffset, + ]); return ( @@ -192,7 +266,7 @@ const Contents: FC<{ { + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); + const [confirmModalVisible, setConfirmModalVisible] = useState(false); + const [defaultQueryString] = useState(JSON.stringify(getDefaultDatafeedQuery(), null, 2)); + + const closeModal = () => setConfirmModalVisible(false); + const showModal = () => setConfirmModalVisible(true); + + function resetDatafeed() { + jobCreator.query = getDefaultDatafeedQuery(); + jobCreatorUpdate(); + closeModal(); + } + return ( + <> + {confirmModalVisible && ( + + + + + + + + {defaultQueryString} + + + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx index 7e895ca206849..b9250c3ecdce5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx @@ -11,11 +11,11 @@ import { QueryInput } from './components/query'; import { QueryDelayInput } from './components/query_delay'; import { FrequencyInput } from './components/frequency'; import { ScrollSizeInput } from './components/scroll_size'; +import { ResetQueryButton } from './components/reset_query'; import { TimeField } from './components/time_field'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); @@ -47,19 +47,13 @@ export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) = + setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} nextActive={nextActive}> - - - - - - - - +
)} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index 60b034b516939..7999ce46bc9ed 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -22,10 +22,18 @@ import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; import { ml } from '../../../../../../../../../services/ml_api_service'; +import { PLUGIN_ID } from '../../../../../../../../../../../common/constants/app'; import { Calendar } from '../../../../../../../../../../../common/types/calendars'; +import { useMlKibana } from '../../../../../../../../../contexts/kibana'; import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constants/calendars'; export const CalendarsSelection: FC = () => { + const { + services: { + application: { getUrlForApp }, + }, + } = useMlKibana(); + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); const [selectedCalendars, setSelectedCalendars] = useState(jobCreator.calendars); const [selectedOptions, setSelectedOptions] = useState>>( @@ -64,7 +72,9 @@ export const CalendarsSelection: FC = () => { }, }; - const manageCalendarsHref = '#/settings/calendars_list'; + const manageCalendarsHref = getUrlForApp(PLUGIN_ID, { + path: '/settings/calendars_list', + }); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx index b0fb2e7267f7f..bff99ad7c5281 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx @@ -14,6 +14,8 @@ import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { AdvancedSection } from './components/advanced_section'; import { AdditionalSection } from './components/additional_section'; +import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; +import { isAdvancedJobCreator } from '../../../common/job_creator'; interface Props extends StepProps { advancedExpanded: boolean; @@ -30,7 +32,7 @@ export const JobDetailsStep: FC = ({ additionalExpanded, setAdditionalExpanded, }) => { - const { jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); + const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); useEffect(() => { @@ -70,7 +72,15 @@ export const JobDetailsStep: FC = ({ previous={() => setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} next={() => setCurrentStep(WIZARD_STEPS.VALIDATION)} nextActive={nextActive} - /> + > + {isAdvancedJobCreator(jobCreator) && ( + + )} + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx index 3bcac1cf6876c..e14e29cc965d3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx @@ -8,21 +8,25 @@ import React, { Fragment, FC, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { useNavigateToPath } from '../../../../../../../contexts/kibana'; + +import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; + import { JobCreatorContext } from '../../../job_creator_context'; + import { BucketSpan } from '../bucket_span'; import { SparseDataSwitch } from '../sparse_data'; -import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; - interface Props { setIsValid: (proceed: boolean) => void; } export const SingleMetricSettings: FC = ({ setIsValid }) => { const { jobCreator } = useContext(JobCreatorContext); + const navigateToPath = useNavigateToPath(); const convertToMultiMetric = () => { - convertToMultiMetricJob(jobCreator); + convertToMultiMetricJob(jobCreator, navigateToPath); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx index 6f03b9a3c3321..2316383709164 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx @@ -6,23 +6,26 @@ import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { JobCreatorContext } from '../job_creator_context'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; -import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { SingleMetricView } from './components/single_metric_view'; import { MultiMetricView } from './components/multi_metric_view'; import { PopulationView } from './components/population_view'; import { AdvancedView } from './components/advanced_view'; import { CategorizationView } from './components/categorization_view'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; +import { + isSingleMetricJobCreator, + isMultiMetricJobCreator, + isPopulationJobCreator, + isCategorizationJobCreator, + isAdvancedJobCreator, +} from '../../../common/job_creator'; export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); - const jobType = jobCreator.type; useEffect(() => { setNextActive(jobValidator.isPickFieldsStepValid); @@ -32,25 +35,25 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) {isCurrentStep && ( - {jobType === JOB_TYPE.SINGLE_METRIC && ( + {isSingleMetricJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.MULTI_METRIC && ( + {isMultiMetricJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.POPULATION && ( + {isPopulationJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.ADVANCED && ( + {isAdvancedJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.CATEGORIZATION && ( + {isCategorizationJobCreator(jobCreator) && ( )} setCurrentStep( - jobCreator.type === JOB_TYPE.ADVANCED + isAdvancedJobCreator(jobCreator) ? WIZARD_STEPS.ADVANCED_CONFIGURE_DATAFEED : WIZARD_STEPS.TIME_RANGE ) @@ -58,19 +61,12 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) next={() => setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} nextActive={nextActive} > - {jobType === JOB_TYPE.ADVANCED && ( - - - - - - - - + {isAdvancedJobCreator(jobCreator) && ( + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index d8cd0f5e4f1f0..24d7fb9fc2a40 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -15,15 +15,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { PreviousButton } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { JobRunner } from '../../../common/job_runner'; import { mlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; -import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { getErrorMessage } from '../../../../../../../common/util/errors'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; @@ -42,6 +40,9 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const { services: { notifications }, } = useMlKibana(); + + const navigateToPath = useNavigateToPath(); + const { jobCreator, jobValidator, jobValidatorUpdated, resultsLoader } = useContext( JobCreatorContext ); @@ -51,13 +52,14 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const [jobRunner, setJobRunner] = useState(null); const isAdvanced = isAdvancedJobCreator(jobCreator); + const jsonEditorMode = isAdvanced ? EDITOR_MODE.EDITABLE : EDITOR_MODE.READONLY; useEffect(() => { jobCreator.subscribeToProgress(setProgress); }, []); async function start() { - if (jobCreator.type === JOB_TYPE.ADVANCED) { + if (isAdvanced) { await startAdvanced(); } else { await startInline(); @@ -87,7 +89,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => try { await jobCreator.createJob(); await jobCreator.createDatafeed(); - advancedStartDatafeed(jobCreator); + advancedStartDatafeed(jobCreator, navigateToPath); } catch (error) { // catch and display all job creation errors const { toasts } = notifications; @@ -112,11 +114,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => } function clickResetJob() { - resetJob(jobCreator); + resetJob(jobCreator, navigateToPath); } const convertToAdvanced = () => { - convertToAdvancedJob(jobCreator); + convertToAdvancedJob(jobCreator, navigateToPath); }; useEffect(() => { @@ -173,15 +175,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => 0} - jobEditorMode={EDITOR_MODE.READONLY} - datafeedEditorMode={EDITOR_MODE.READONLY} + jobEditorMode={jsonEditorMode} + datafeedEditorMode={jsonEditorMode} /> - {jobCreator.type === JOB_TYPE.ADVANCED ? ( - - - - ) : ( + {isAdvanced === false && ( = ({ nextStepPath }) => { const RESULTS_PER_PAGE = 20; const { uiSettings, savedObjects } = useMlKibana().services; + const navigateToPath = useNavigateToPath(); const onObjectSelection = (id: string, type: string) => { - window.location.href = `${nextStepPath}?${ - type === 'index-pattern' ? 'index' : 'savedSearchId' - }=${encodeURIComponent(id)}`; + navigateToPath( + `${nextStepPath}?${type === 'index-pattern' ? 'index' : 'savedSearchId'}=${encodeURIComponent( + id + )}` + ); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index 3bfe0569e75be..be0135ec3f1e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -18,6 +18,7 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useNavigateToPath } from '../../../../contexts/kibana'; import { useMlContext } from '../../../../contexts/ml'; import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; @@ -28,6 +29,8 @@ import { CategorizationIcon } from './categorization_job_icon'; export const Page: FC = () => { const mlContext = useMlContext(); + const navigateToPath = useNavigateToPath(); + const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); const { currentSavedSearch, currentIndexPattern } = mlContext; @@ -68,25 +71,23 @@ export const Page: FC = () => { }, }; - const getUrl = (basePath: string) => { + const getUrlParams = () => { return !isSavedSearchSavedObject(currentSavedSearch) - ? `${basePath}?index=${currentIndexPattern.id}` - : `${basePath}?savedSearchId=${currentSavedSearch.id}`; + ? `?index=${currentIndexPattern.id}` + : `?savedSearchId=${currentSavedSearch.id}`; }; const addSelectionToRecentlyAccessed = () => { const title = !isSavedSearchSavedObject(currentSavedSearch) ? currentIndexPattern.title : (currentSavedSearch.attributes.title as string); - const url = getUrl(''); - addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); - - window.location.href = getUrl('#jobs/new_job/datavisualizer'); + addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, ''); + navigateToPath(`/jobs/new_job/datavisualizer${getUrlParams()}`); }; const jobTypes = [ { - href: getUrl('#jobs/new_job/single_metric'), + onClick: () => navigateToPath(`/jobs/new_job/single_metric${getUrlParams()}`), icon: { type: 'createSingleMetricJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricAriaLabel', { @@ -102,7 +103,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkSingleMetricJob', }, { - href: getUrl('#jobs/new_job/multi_metric'), + onClick: () => navigateToPath(`/jobs/new_job/multi_metric${getUrlParams()}`), icon: { type: 'createMultiMetricJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricAriaLabel', { @@ -119,7 +120,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkMultiMetricJob', }, { - href: getUrl('#jobs/new_job/population'), + onClick: () => navigateToPath(`/jobs/new_job/population${getUrlParams()}`), icon: { type: 'createPopulationJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.populationAriaLabel', { @@ -136,7 +137,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkPopulationJob', }, { - href: getUrl('#jobs/new_job/advanced'), + onClick: () => navigateToPath(`/jobs/new_job/advanced${getUrlParams()}`), icon: { type: 'createAdvancedJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedAriaLabel', { @@ -153,7 +154,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkAdvancedJob', }, { - href: getUrl('#jobs/new_job/categorization'), + onClick: () => navigateToPath(`/jobs/new_job/categorization${getUrlParams()}`), icon: { type: CategorizationIcon, ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.categorizationAriaLabel', { @@ -247,11 +248,11 @@ export const Page: FC = () => { - {jobTypes.map(({ href, icon, title, description, id }) => ( + {jobTypes.map(({ onClick, icon, title, description, id }) => ( { /> } onClick={addSelectionToRecentlyAccessed} - href={getUrl('#jobs/new_job/datavisualizer')} /> diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 5fa6c817ec4c1..b7b6aa15ffe44 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { cloneDeep } from 'lodash'; import { IUiSettingsClient } from 'kibana/public'; import { esQuery, Query, esKuery } from '../../../../../../../../src/plugins/data/public'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; @@ -13,6 +14,20 @@ import { getQueryFromSavedSearch } from '../../../util/index_utils'; // Provider for creating the items used for searching and job creation. +const DEFAULT_QUERY = { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, +}; + +export function getDefaultDatafeedQuery() { + return cloneDeep(DEFAULT_QUERY); +} + export function createSearchItems( kibanaConfig: IUiSettingsClient, indexPattern: IIndexPattern, @@ -27,16 +42,7 @@ export function createSearchItems( language: 'lucene', }; - let combinedQuery: any = { - bool: { - must: [ - { - match_all: {}, - }, - ], - }, - }; - + let combinedQuery: any = getDefaultDatafeedQuery(); if (savedSearch !== null) { const data = getQueryFromSavedSearch(savedSearch); diff --git a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx index 119346ec8035a..903a3c467a38b 100644 --- a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx @@ -9,29 +9,12 @@ import { EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui import { FormattedMessage } from '@kbn/i18n/react'; import { useMlKibana } from '../../contexts/kibana'; -const createJobLink = '#/jobs/new_job/step/index_or_search'; const feedbackLink = 'https://www.elastic.co/community/'; interface Props { createAnomalyDetectionJobDisabled: boolean; } -function getCreateJobLink(createAnomalyDetectionJobDisabled: boolean) { - return createAnomalyDetectionJobDisabled === true ? ( - - ) : ( - - - - ); -} - export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }) => { const { services: { @@ -59,7 +42,7 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

@@ -69,7 +52,6 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled } /> ), - createJob: getCreateJobLink(createAnomalyDetectionJobDisabled), transforms: ( { + return (e) => { + e.preventDefault(); + navigateToPath(path); + }; +}; + +export const getBreadcrumbWithUrlForApp = ( + breadcrumbName: Breadcrumb, + navigateToPath: NavigateToPath +): EuiBreadcrumb => { + return { + ...breadcrumbs[breadcrumbName], + onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath), + }; +}; diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index f1b8083f19ccf..56c9a19723fba 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { HashRouter, Route, RouteProps } from 'react-router-dom'; +import React, { useEffect, FC } from 'react'; +import { useHistory, useLocation, Router, Route, RouteProps } from 'react-router-dom'; import { Location } from 'history'; -import { IUiSettingsClient, ChromeStart } from 'kibana/public'; +import { AppMountParameters, IUiSettingsClient, ChromeStart } from 'kibana/public'; import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; + +import { useNavigateToPath } from '../contexts/kibana'; import { MlContext, MlContextValue } from '../contexts/ml'; import { UrlStateProvider } from '../util/url_state'; @@ -33,9 +35,10 @@ export interface PageProps { } interface PageDependencies { - setBreadcrumbs: ChromeStart['setBreadcrumbs']; - indexPatterns: IndexPatternsContract; config: IUiSettingsClient; + history: AppMountParameters['history']; + indexPatterns: IndexPatternsContract; + setBreadcrumbs: ChromeStart['setBreadcrumbs']; } export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => { @@ -44,28 +47,74 @@ export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children ); }; -export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => { - const setBreadcrumbs = pageDeps.setBreadcrumbs; +/** + * This component provides compatibility with the previous hash based + * URL format used by HashRouter. Even if we migrate all internal URLs + * to one without hashes, we should keep this redirect in place to + * support legacy bookmarks and as a fallback for unmigrated URLs + * from other plugins. + */ +const LegacyHashUrlRedirect: FC = ({ children }) => { + const history = useHistory(); + const location = useLocation(); + + useEffect(() => { + if (location.hash.startsWith('#/')) { + history.push(location.hash.replace('#', '')); + } + }, [location.hash]); + + return <>{children}; +}; + +/** + * `MlRoutes` creates a React Router Route for every routeFactory + * and passes on the `navigateToPath` helper. + */ +const MlRoutes: FC<{ + pageDeps: PageDependencies; +}> = ({ pageDeps }) => { + const navigateToPath = useNavigateToPath(); return ( - + <> + {Object.entries(routes).map(([name, routeFactory]) => { + const route = routeFactory(navigateToPath); + + return ( + { + window.setTimeout(() => { + pageDeps.setBreadcrumbs(route.breadcrumbs); + }); + return route.render(props, pageDeps); + }} + /> + ); + })} + + ); +}; + +/** + * `MlRouter` is based on `BrowserRouter` and takes in `ScopedHistory` provided + * by Kibana. `LegacyHashUrlRedirect` provides compatibility with legacy hash based URLs. + * `UrlStateProvider` manages state stored in `_g/_a` URL parameters which can be + * use in components further down via `useUrlState()`. + */ +export const MlRouter: FC<{ + pageDeps: PageDependencies; +}> = ({ pageDeps }) => ( + +

- {Object.entries(routes).map(([name, route]) => ( - { - window.setTimeout(() => { - setBreadcrumbs(route.breadcrumbs); - }); - return route.render(props, pageDeps); - }} - /> - ))} +
- - ); -}; + + +); diff --git a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx index bd7fc434b36ac..42d9a59d15bfa 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx @@ -19,11 +19,11 @@ const breadcrumbs = [ }, ]; -export const accessDeniedRoute: MlRoute = { +export const accessDeniedRouteFactory = (): MlRoute => ({ path: '/access-denied', render: (props, deps) => , breadcrumbs, -}; +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, {}); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx index ebc7bd95fb0c3..8c45398098b2f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -5,29 +5,31 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { parse } from 'query-string'; + +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_creation'; -import { ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { - defaultMessage: 'Data Frame Analytics', - }), - href: '#/data_frame_analytics', - }, -]; - -export const analyticsJobsCreationRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsJobsCreationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/new_job', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { + defaultMessage: 'Data Frame Analytics', + }), + onClick: breadcrumbOnClickFactory('/data_frame_analytics', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, jobId, savedSearchId }: Record = parse(location.search, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 1ffea2c06faf4..47cc002ab4d83 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -4,33 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; +import { parse } from 'query-string'; import { decode } from 'rison-node'; + +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; -import { ML_BREADCRUMB, DATA_FRAME_ANALYTICS_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_FRAME_ANALYTICS_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { - defaultMessage: 'Exploration', - }), - href: '', - }, -]; - -export const analyticsJobExplorationRoute: MlRoute = { +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsJobExplorationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/exploration', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { + defaultMessage: 'Exploration', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index 2623136d1e98f..b6ef9ea81b4ba 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -7,28 +7,28 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_management'; -import { ML_BREADCRUMB, DATA_FRAME_ANALYTICS_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_FRAME_ANALYTICS_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const analyticsJobsListRoute: MlRoute = { +export const analyticsJobsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index fc2d517b2edb1..efe5c3cba04a5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -11,21 +11,24 @@ import React, { FC } from 'react'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const selectorRoute: MlRoute = { +export const selectorRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/datavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 1115a38870821..485af52c45a55 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; @@ -20,24 +22,22 @@ import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { - defaultMessage: 'File', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const fileBasedRoute: MlRoute = { +export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/filedatavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { + defaultMessage: 'File', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index 1ec73fced82fe..358b8773e3460 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; +import { parse } from 'query-string'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; @@ -15,24 +19,22 @@ import { checkBasicLicense } from '../../../license'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { - defaultMessage: 'Index', - }), - href: '', - }, -]; - -export const indexBasedRoute: MlRoute = { +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/datavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { + defaultMessage: 'Index', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); 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 7d09797a0ff1b..a2030776773a9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -9,6 +9,8 @@ import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../contexts/kibana'; + import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { MlRoute, PageLoader, PageProps } from '../router'; @@ -27,26 +29,24 @@ import { useShowCharts } from '../../components/controls/checkbox_showcharts'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; import { isViewBySwimLaneData } from '../../explorer/swimlane_container'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { - defaultMessage: 'Anomaly Explorer', - }), - href: '', - }, -]; - -export const explorerRoute: MlRoute = { +export const explorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/explorer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { + defaultMessage: 'Anomaly Explorer', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context, results } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/index.ts b/x-pack/plugins/ml/public/application/routing/routes/index.ts index 44111ae32cd30..fe7ecd129ebef 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/index.ts @@ -10,6 +10,6 @@ export * from './new_job'; export * from './datavisualizer'; export * from './settings'; export * from './data_frame_analytics'; -export { timeSeriesExplorerRoute } from './timeseriesexplorer'; +export { timeSeriesExplorerRouteFactory } from './timeseriesexplorer'; export * from './explorer'; export * from './access_denied'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx index c1d686d356dda..db58b6a537e06 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -7,6 +7,9 @@ import React, { useEffect, FC } from 'react'; import { useObservable } from 'react-use'; import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../contexts/kibana'; + import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useUrlState } from '../../util/url_state'; @@ -15,24 +18,22 @@ import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; import { JobsPage } from '../../jobs/jobs_list'; import { useTimefilter } from '../../contexts/kibana'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, -]; - -export const jobListRoute: MlRoute = { +export const jobListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index b630b09b1a46d..d8605c4cc9115 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -5,12 +5,16 @@ */ import React, { FC } from 'react'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; @@ -26,9 +30,9 @@ interface IndexOrSearchPageProps extends PageProps { mode: MODE; } -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, +const getBreadcrumbs = (navigateToPath: NavigateToPath) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { defaultMessage: 'Create job', @@ -37,31 +41,31 @@ const breadcrumbs = [ }, ]; -export const indexOrSearchRoute: MlRoute = { +export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/step/index_or_search', render: (props, deps) => ( ), - breadcrumbs, -}; + breadcrumbs: getBreadcrumbs(navigateToPath), +}); -export const dataVizIndexOrSearchRoute: MlRoute = { +export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/datavisualizer_index_select', render: (props, deps) => ( ), - breadcrumbs, -}; + breadcrumbs: getBreadcrumbs(navigateToPath), +}); const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { const newJobResolvers = { diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index f0a25d880a082..b8ab29d40fa1f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -4,31 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; +import { parse } from 'query-string'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/pages/job_type'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { - defaultMessage: 'Create job', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const jobTypeRoute: MlRoute = { +export const jobTypeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/step/job_type', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { + defaultMessage: 'Create job', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx index b110434f6f0a8..b230da44c8d6d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx @@ -5,28 +5,16 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { MlRoute } from '../../router'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.jobWizardLabel', { - defaultMessage: 'Create job', - }), - href: '#/jobs/new_job', - }, -]; - -export const newJobRoute: MlRoute = { +export const newJobRouteFactory = (): MlRoute => ({ path: '/jobs/new_job', render: () => , - breadcrumbs, -}; + // no breadcrumbs since it's just a redirect + breadcrumbs: [], +}); const Page: FC = () => { return ; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 2cd40cbcd95e6..6be58828ee1a5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -6,42 +6,41 @@ import { parse } from 'query-string'; import React, { FC } from 'react'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/recognize'; import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; import { mlJobService } from '../../../services/job_service'; -import { - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { - defaultMessage: 'Recognized index', - }), - href: '', - }, -]; - -export const recognizeRoute: MlRoute = { +export const recognizeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/recognize', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { + defaultMessage: 'Recognized index', + }), + href: '', + }, + ], +}); -export const checkViewOrCreateRoute: MlRoute = { +export const checkViewOrCreateRouteFactory = (): MlRoute => ({ path: '/modules/check_view_or_create', render: (props, deps) => , + // no breadcrumbs since it's just a redirect breadcrumbs: [], -}; +}); const PageWrapper: FC = ({ location, deps }) => { const { id, index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 14df9a1d44a85..35085fd557577 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -8,6 +8,8 @@ import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { basicResolvers } from '../../resolvers'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -16,20 +18,20 @@ import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; -import { - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; interface WizardPageProps extends PageProps { jobType: JOB_TYPE; } -const baseBreadcrumbs = [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, CREATE_JOB_BREADCRUMB]; +const getBaseBreadcrumbs = (navigateToPath: NavigateToPath) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), +]; -const singleMetricBreadcrumbs = [ - ...baseBreadcrumbs, +const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { defaultMessage: 'Single metric', @@ -38,8 +40,8 @@ const singleMetricBreadcrumbs = [ }, ]; -const multiMetricBreadcrumbs = [ - ...baseBreadcrumbs, +const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { defaultMessage: 'Multi-metric', @@ -48,8 +50,8 @@ const multiMetricBreadcrumbs = [ }, ]; -const populationBreadcrumbs = [ - ...baseBreadcrumbs, +const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { defaultMessage: 'Population', @@ -58,8 +60,8 @@ const populationBreadcrumbs = [ }, ]; -const advancedBreadcrumbs = [ - ...baseBreadcrumbs, +const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { defaultMessage: 'Advanced configuration', @@ -68,8 +70,8 @@ const advancedBreadcrumbs = [ }, ]; -const categorizationBreadcrumbs = [ - ...baseBreadcrumbs, +const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.categorizationLabel', { defaultMessage: 'Categorization', @@ -78,35 +80,35 @@ const categorizationBreadcrumbs = [ }, ]; -export const singleMetricRoute: MlRoute = { +export const singleMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/single_metric', render: (props, deps) => , - breadcrumbs: singleMetricBreadcrumbs, -}; + breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath), +}); -export const multiMetricRoute: MlRoute = { +export const multiMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/multi_metric', render: (props, deps) => , - breadcrumbs: multiMetricBreadcrumbs, -}; + breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath), +}); -export const populationRoute: MlRoute = { +export const populationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/population', render: (props, deps) => , - breadcrumbs: populationBreadcrumbs, -}; + breadcrumbs: getPopulationBreadcrumbs(navigateToPath), +}); -export const advancedRoute: MlRoute = { +export const advancedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/advanced', render: (props, deps) => , - breadcrumbs: advancedBreadcrumbs, -}; + breadcrumbs: getAdvancedBreadcrumbs(navigateToPath), +}); -export const categorizationRoute: MlRoute = { +export const categorizationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/categorization', render: (props, deps) => , - breadcrumbs: categorizationBreadcrumbs, -}; + breadcrumbs: getCategorizationBreadcrumbs(navigateToPath), +}); const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index 9b08bbf35c448..174e9804b9689 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -8,6 +8,9 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; + +import { NavigateToPath } from '../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../router'; import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; @@ -17,23 +20,21 @@ import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capab import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; -import { ML_BREADCRUMB } from '../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.overview.overviewLabel', { - defaultMessage: 'Overview', - }), - href: '#/overview', - }, -]; - -export const overviewRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs'; + +export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/overview', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.overview.overviewLabel', { + defaultMessage: 'Overview', + }), + onClick: breadcrumbOnClickFactory('/overview', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { @@ -51,11 +52,11 @@ const PageWrapper: FC = ({ deps }) => { ); }; -export const appRootRoute: MlRoute = { +export const appRootRouteFactory = (): MlRoute => ({ path: '/', render: () => , breadcrumbs: [], -}; +}); const Page: FC = () => { return ; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index e015a3292acc4..f2ae57f1ec961 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,24 +25,22 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - href: '#/settings/calendars_list', - }, -]; - -export const calendarListRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const calendarListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index ebd58120853a9..a5c30e1eaaacc 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,7 +25,7 @@ import { } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; enum MODE { NEW, @@ -34,39 +36,35 @@ interface NewCalendarPageProps extends PageProps { mode: MODE; } -const newBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/calendars_list/new_calendar', - }, -]; - -const editBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/calendars_list/edit_calendar', - }, -]; - -export const newCalendarRoute: MlRoute = { +export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list/new_calendar', render: (props, deps) => , - breadcrumbs: newBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { + defaultMessage: 'Create', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list/new_calendar', navigateToPath), + }, + ], +}); -export const editCalendarRoute: MlRoute = { +export const editCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list/edit_calendar/:calendarId', render: (props, deps) => , - breadcrumbs: editBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { + defaultMessage: 'Edit', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list/edit_calendar', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, mode, deps }) => { let calendarId: string | undefined; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 25bded1a52db1..d734e18d72bab 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -24,24 +26,22 @@ import { import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - href: '#/settings/filter_lists', - }, -]; +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const filterListRoute: MlRoute = { +export const filterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 2f4ccecf2f1a2..c6f17bc7f6f68 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,7 +25,8 @@ import { } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; enum MODE { NEW, @@ -34,39 +37,35 @@ interface NewFilterPageProps extends PageProps { mode: MODE; } -const newBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/filter_lists/new', - }, -]; - -const editBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/filter_lists/edit', - }, -]; - -export const newFilterListRoute: MlRoute = { +export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists/new_filter_list', render: (props, deps) => , - breadcrumbs: newBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { + defaultMessage: 'Create', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists/new', navigateToPath), + }, + ], +}); -export const editFilterListRoute: MlRoute = { +export const editFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists/edit_filter_list/:filterId', render: (props, deps) => , - breadcrumbs: editBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { + defaultMessage: 'Edit', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists/edit', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, mode, deps }) => { let filterId: string | undefined; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index a80c173dbca34..3f4b269851469 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -11,6 +11,8 @@ import React, { FC } from 'react'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -22,15 +24,16 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { AnomalyDetectionSettingsContext, Settings } from '../../../settings'; -import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; - -const breadcrumbs = [ML_BREADCRUMB, SETTINGS]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const settingsRoute: MlRoute = { +export const settingsRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index fdf29406893ad..6486db818e113 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -11,6 +11,8 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../contexts/kibana'; + import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { TimeSeriesExplorer } from '../../timeseriesexplorer'; @@ -34,15 +36,15 @@ import { MlRoute, PageLoader, PageProps } from '../router'; import { useRefresh } from '../use_refresh'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; -export const timeSeriesExplorerRoute: MlRoute = { +export const timeSeriesExplorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/timeseriesexplorer', render: (props, deps) => , breadcrumbs: [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), { text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { defaultMessage: 'Single Metric Viewer', @@ -50,7 +52,7 @@ export const timeSeriesExplorerRoute: MlRoute = { href: '', }, ], -}; +}); const PageWrapper: FC = ({ deps }) => { const { context, results } = useResolver('', undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts index b5c01a1c26144..428060dd2c31b 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts @@ -9,6 +9,7 @@ import { getUrlForRecord, isValidLabel, isValidTimeRange, + openCustomUrlWindow, } from './custom_url_utils'; import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { @@ -474,4 +475,49 @@ describe('ML - custom URL utils', () => { expect(isValidTimeRange('AUTO')).toBe(false); }); }); + + describe('openCustomUrlWindow', () => { + const originalOpen = window.open; + + beforeEach(() => { + delete (window as any).open; + const mockOpen = jest.fn(); + window.open = mockOpen; + }); + + afterEach(() => { + window.open = originalOpen; + }); + + it('should add the base path to a relative non-kibana url', () => { + openCustomUrlWindow( + 'the-url', + { url_name: 'the-url-name', url_value: 'the-url-value' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith('the-base-path/the-url', '_blank'); + }); + + it('should add the base path and `app` prefix to a relative kibana url', () => { + openCustomUrlWindow( + 'discover#/the-url', + { url_name: 'the-url-name', url_value: 'discover#/the-url-value' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith('the-base-path/app/discover#/the-url', '_blank'); + }); + + it('should use an absolute url with protocol as is', () => { + openCustomUrlWindow( + 'http://example.com', + { url_name: 'the-url-name', url_value: 'http://example.com' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith( + 'http://example.com', + '_blank', + 'noopener,noreferrer' + ); + }); + }); }); diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts index 20bb1c7f60597..9c843af36192e 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts @@ -76,15 +76,20 @@ export function getUrlForRecord( // Opens the specified URL in a new window. The behaviour (for example whether // it opens in a new tab or window) is determined from the original configuration // object which indicates whether it is opening a Kibana page running on the same server. -// fullUrl is the complete URL, including the base path, with any dollar delimited tokens -// from the urlConfig having been substituted with values from an anomaly record. -export function openCustomUrlWindow(fullUrl: string, urlConfig: UrlConfig) { +// `url` is the URL with any dollar delimited tokens from the urlConfig +// having been substituted with values from an anomaly record. +export function openCustomUrlWindow(url: string, urlConfig: UrlConfig, basePath: string) { // Run through a regex to test whether the url_value starts with a protocol scheme. if (/^(?:[a-z]+:)?\/\//i.test(urlConfig.url_value) === false) { - window.open(fullUrl, '_blank'); + // If `url` is a relative path, we need to prefix the base path. + if (url.charAt(0) !== '/') { + url = `${basePath}${isKibanaUrl(urlConfig) ? '/app/' : '/'}${url}`; + } + + window.open(url, '_blank'); } else { // Add noopener and noreferrr properties for external URLs. - const newWindow = window.open(fullUrl, '_blank', 'noopener,noreferrer'); + const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); // Expect newWindow to be null, but just in case if not, reset the opener link. if (newWindow !== undefined && newWindow !== null) { @@ -94,13 +99,24 @@ export function openCustomUrlWindow(fullUrl: string, urlConfig: UrlConfig) { } // Returns whether the url_value of the supplied config is for -// a Kibana Discover or Dashboard page running on the same server as this ML plugin. +// a Kibana Discover, Dashboard or supported solution page running +// on the same server as this ML plugin. This is necessary so we can have +// backwards compatibility with custom URLs created before the move to +// BrowserRouter and URLs without hashes. If we add another solution to +// recognize modules or with custom UI in the custom URL builder we'd +// need to add the solution here. Manually created custom URLs for other +// solution pages need to be prefixed with `app/` in the custom URL builder. function isKibanaUrl(urlConfig: UrlConfig) { const urlValue = urlConfig.url_value; return ( + // HashRouter based plugins urlValue.startsWith('discover#/') || urlValue.startsWith('dashboards#/') || - urlValue.startsWith('apm#/') + urlValue.startsWith('apm#/') || + // BrowserRouter based plugins + urlValue.startsWith('security/') || + // Legacy links + urlValue.startsWith('siem#/') ); } diff --git a/x-pack/plugins/ml/public/url_generator.test.ts b/x-pack/plugins/ml/public/url_generator.test.ts index 45e2932b7781a..21dde12404957 100644 --- a/x-pack/plugins/ml/public/url_generator.test.ts +++ b/x-pack/plugins/ml/public/url_generator.test.ts @@ -19,7 +19,7 @@ describe('MlUrlGenerator', () => { mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, }); expect(url).toBe( - '/app/ml#/explorer?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' + '/app/ml/explorer#?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' ); }); diff --git a/x-pack/plugins/ml/public/url_generator.ts b/x-pack/plugins/ml/public/url_generator.ts index c2b57f6349d81..b7cf64159a827 100644 --- a/x-pack/plugins/ml/public/url_generator.ts +++ b/x-pack/plugins/ml/public/url_generator.ts @@ -83,7 +83,7 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition('_g', queryState, { useHash: false }, url); url = setStateToKbnUrl('_a', appState, { useHash: false }, url); diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js b/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js index e56627369475b..083ebfb27fd51 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js @@ -10,7 +10,12 @@ import { getCollectionStatus } from '..'; import { getIndexPatterns } from '../../../cluster/get_index_patterns'; const liveClusterUuid = 'a12'; -const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = true) => { +const mockReq = ( + searchResult = {}, + securityEnabled = true, + userHasPermissions = true, + securityErrorMessage = null +) => { return { server: { newPlatform: { @@ -37,12 +42,14 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = }, }, plugins: { - xpack_main: { + monitoring: { info: { - isAvailable: () => true, - feature: () => ({ - isEnabled: () => securityEnabled, - }), + getSecurityFeature: () => { + return { + isAvailable: securityEnabled, + isEnabled: securityEnabled, + }; + }, }, }, elasticsearch: { @@ -61,6 +68,11 @@ const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = params && params.path === '/_security/user/_has_privileges' ) { + if (securityErrorMessage !== null) { + return Promise.reject({ + message: securityErrorMessage, + }); + } return Promise.resolve({ has_all_requested: userHasPermissions }); } if (type === 'transport.request' && params && params.path === '/_nodes') { @@ -245,6 +257,34 @@ describe('getCollectionStatus', () => { expect(result.kibana.detected.doesExist).to.be(true); }); + it('should work properly with an unknown security message', async () => { + const req = mockReq({ hits: { total: { value: 1 } } }, true, true, 'foobar'); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result._meta.hasPermissions).to.be(false); + }); + + it('should work properly with a known security message', async () => { + const req = mockReq( + { hits: { total: { value: 1 } } }, + true, + true, + 'no handler found for uri [/_security/user/_has_privileges] and method [POST]' + ); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + + it('should work properly with another known security message', async () => { + const req = mockReq( + { hits: { total: { value: 1 } } }, + true, + true, + 'Invalid index name [_security]' + ); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + it('should not work if the user does not have the necessary permissions', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, true, false); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 607503673276b..81cdfd6ecd172 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -233,6 +233,10 @@ function isBeatFromAPM(bucket) { } async function hasNecessaryPermissions(req) { + const securityFeature = req.server.plugins.monitoring.info.getSecurityFeature(); + if (!securityFeature.isAvailable || !securityFeature.isEnabled) { + return true; + } try { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); const response = await callWithRequest(req, 'transport.request', { @@ -250,6 +254,9 @@ async function hasNecessaryPermissions(req) { ) { return true; } + if (err.message.includes('Invalid index name [_security]')) { + return true; + } return false; } } diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts index 2cebaacc67681..2d37d4a345fa1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts @@ -15,86 +15,13 @@ import { getLanguageBooleanOperator, buildNested, } from './build_exceptions_query'; -import { - EntryNested, - EntryExists, - EntryMatch, - EntryMatchAny, - EntriesArray, - Operator, -} from '../../../lists/common/schemas'; +import { EntryNested, EntryMatchAny, EntriesArray } from '../../../lists/common/schemas'; import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../lists/common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../lists/common/schemas/types/entry_match_any.mock'; import { getEntryExistsMock } from '../../../lists/common/schemas/types/entry_exists.mock'; describe('build_exceptions_query', () => { - const makeMatchEntry = ({ - field, - value = 'value-1', - operator = 'included', - }: { - field: string; - value?: string; - operator?: Operator; - }): EntryMatch => { - return { - field, - operator, - type: 'match', - value, - }; - }; - const makeMatchAnyEntry = ({ - field, - operator = 'included', - value = ['value-1', 'value-2'], - }: { - field: string; - operator?: Operator; - value?: string[]; - }): EntryMatchAny => { - return { - field, - operator, - value, - type: 'match_any', - }; - }; - const makeExistsEntry = ({ - field, - operator = 'included', - }: { - field: string; - operator?: Operator; - }): EntryExists => { - return { - field, - operator, - type: 'exists', - }; - }; - const matchEntryWithIncluded: EntryMatch = makeMatchEntry({ - field: 'host.name', - value: 'suricata', - }); - const matchEntryWithExcluded: EntryMatch = makeMatchEntry({ - field: 'host.name', - value: 'suricata', - operator: 'excluded', - }); - const matchAnyEntryWithIncludedAndTwoValues: EntryMatchAny = makeMatchAnyEntry({ - field: 'host.name', - value: ['suricata', 'auditd'], - }); - const existsEntryWithIncluded: EntryExists = makeExistsEntry({ - field: 'host.name', - }); - const existsEntryWithExcluded: EntryExists = makeExistsEntry({ - field: 'host.name', - operator: 'excluded', - }); - describe('getLanguageBooleanOperator', () => { test('it returns value as uppercase if language is "lucene"', () => { const result = getLanguageBooleanOperator({ language: 'lucene', value: 'not' }); @@ -137,14 +64,14 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { const query = buildExists({ - entry: existsEntryWithExcluded, + entry: { ...getEntryExistsMock(), operator: 'excluded' }, language: 'kuery', }); expect(query).toEqual('not host.name:*'); }); test('it returns formatted wildcard string when operator is "included"', () => { const query = buildExists({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'kuery', }); expect(query).toEqual('host.name:*'); @@ -154,14 +81,14 @@ describe('build_exceptions_query', () => { describe('lucene', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { const query = buildExists({ - entry: existsEntryWithExcluded, + entry: { ...getEntryExistsMock(), operator: 'excluded' }, language: 'lucene', }); expect(query).toEqual('NOT _exists_host.name'); }); test('it returns formatted wildcard string when operator is "included"', () => { const query = buildExists({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'lucene', }); expect(query).toEqual('_exists_host.name'); @@ -173,52 +100,55 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted string when operator is "included"', () => { const query = buildMatch({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'kuery', }); - expect(query).toEqual('host.name:"suricata"'); + expect(query).toEqual('host.name:"some host name"'); }); test('it returns formatted string when operator is "excluded"', () => { const query = buildMatch({ - entry: matchEntryWithExcluded, + entry: { ...getEntryMatchMock(), operator: 'excluded' }, language: 'kuery', }); - expect(query).toEqual('not host.name:"suricata"'); + expect(query).toEqual('not host.name:"some host name"'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { const query = buildMatch({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'lucene', }); - expect(query).toEqual('host.name:"suricata"'); + expect(query).toEqual('host.name:"some host name"'); }); test('it returns formatted string when operator is "excluded"', () => { const query = buildMatch({ - entry: matchEntryWithExcluded, + entry: { ...getEntryMatchMock(), operator: 'excluded' }, language: 'lucene', }); - expect(query).toEqual('NOT host.name:"suricata"'); + expect(query).toEqual('NOT host.name:"some host name"'); }); }); }); describe('buildMatchAny', () => { - const entryWithIncludedAndNoValues: EntryMatchAny = makeMatchAnyEntry({ + const entryWithIncludedAndNoValues: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', value: [], - }); - const entryWithIncludedAndOneValue: EntryMatchAny = makeMatchAnyEntry({ + }; + const entryWithIncludedAndOneValue: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', - value: ['suricata'], - }); - const entryWithExcludedAndTwoValues: EntryMatchAny = makeMatchAnyEntry({ + value: ['some host name'], + }; + const entryWithExcludedAndTwoValues: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', - value: ['suricata', 'auditd'], + value: ['some host name', 'auditd'], operator: 'excluded', - }); + }; describe('kuery', () => { test('it returns empty string if given an empty array for "values"', () => { @@ -235,16 +165,16 @@ describe('build_exceptions_query', () => { language: 'kuery', }); - expect(exceptionSegment).toEqual('host.name:("suricata")'); + expect(exceptionSegment).toEqual('host.name:("some host name")'); }); test('it returns formatted string when operator is "included"', () => { const exceptionSegment = buildMatchAny({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'kuery', }); - expect(exceptionSegment).toEqual('host.name:("suricata" or "auditd")'); + expect(exceptionSegment).toEqual('host.name:("some host name" or "auditd")'); }); test('it returns formatted string when operator is "excluded"', () => { @@ -253,18 +183,18 @@ describe('build_exceptions_query', () => { language: 'kuery', }); - expect(exceptionSegment).toEqual('not host.name:("suricata" or "auditd")'); + expect(exceptionSegment).toEqual('not host.name:("some host name" or "auditd")'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { const exceptionSegment = buildMatchAny({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'lucene', }); - expect(exceptionSegment).toEqual('host.name:("suricata" OR "auditd")'); + expect(exceptionSegment).toEqual('host.name:("some host name" OR "auditd")'); }); test('it returns formatted string when operator is "excluded"', () => { const exceptionSegment = buildMatchAny({ @@ -272,7 +202,7 @@ describe('build_exceptions_query', () => { language: 'lucene', }); - expect(exceptionSegment).toEqual('NOT host.name:("suricata" OR "auditd")'); + expect(exceptionSegment).toEqual('NOT host.name:("some host name" OR "auditd")'); }); test('it returns formatted string when "values" includes only one item', () => { const exceptionSegment = buildMatchAny({ @@ -280,7 +210,7 @@ describe('build_exceptions_query', () => { language: 'lucene', }); - expect(exceptionSegment).toEqual('host.name:("suricata")'); + expect(exceptionSegment).toEqual('host.name:("some host name")'); }); }); }); @@ -394,7 +324,7 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = buildEntry({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'kuery', }); expect(result).toEqual('host.name:*'); @@ -402,25 +332,25 @@ describe('build_exceptions_query', () => { test('it returns formatted string when "type" is "match"', () => { const result = buildEntry({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'kuery', }); - expect(result).toEqual('host.name:"suricata"'); + expect(result).toEqual('host.name:"some host name"'); }); test('it returns formatted string when "type" is "match_any"', () => { const result = buildEntry({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'kuery', }); - expect(result).toEqual('host.name:("suricata" or "auditd")'); + expect(result).toEqual('host.name:("some host name" or "auditd")'); }); }); describe('lucene', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = buildEntry({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'lucene', }); expect(result).toEqual('_exists_host.name'); @@ -428,18 +358,18 @@ describe('build_exceptions_query', () => { test('it returns formatted string when "type" is "match"', () => { const result = buildEntry({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'lucene', }); - expect(result).toEqual('host.name:"suricata"'); + expect(result).toEqual('host.name:"some host name"'); }); test('it returns formatted string when "type" is "match_any"', () => { const result = buildEntry({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'lucene', }); - expect(result).toEqual('host.name:("suricata" OR "auditd")'); + expect(result).toEqual('host.name:("some host name" OR "auditd")'); }); }); }); @@ -456,26 +386,31 @@ describe('build_exceptions_query', () => { test('it returns expected query when more than one item in exception item', () => { const payload: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-3' }), + { ...getEntryMatchAnyMock(), field: 'b' }, + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'value-3' }, ]; const query = buildExceptionItem({ language: 'kuery', entries: payload, }); - const expectedQuery = 'b:("value-1" or "value-2") and not c:"value-3"'; + const expectedQuery = 'b:("some host name") and not c:"value-3"'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when exception item includes nested value', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-3', + }, ], }, ]; @@ -483,56 +418,65 @@ describe('build_exceptions_query', () => { language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" }'; + const expectedQuery = 'b:("some host name") and parent:{ nestedField:"value-3" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when exception item includes multiple items and nested "and" values', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-3', + }, ], }, - makeExistsEntry({ field: 'd' }), + { ...getEntryExistsMock(), field: 'd' }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = - 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" } and d:*'; + const expectedQuery = 'b:("some host name") and parent:{ nestedField:"value-3" } and d:*'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when language is "lucene"', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'excluded', + value: 'value-3', + }, ], }, - makeExistsEntry({ field: 'e', operator: 'excluded' }), + { ...getEntryExistsMock(), field: 'e', operator: 'excluded' }, ]; const query = buildExceptionItem({ language: 'lucene', entries, }); const expectedQuery = - 'b:("value-1" OR "value-2") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; + 'b:("some host name") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; expect(query).toEqual(expectedQuery); }); describe('exists', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeExistsEntry({ field: 'b' })]; + const entries: EntriesArray = [{ ...getEntryExistsMock(), field: 'b' }]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -543,7 +487,9 @@ describe('build_exceptions_query', () => { }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - const entries: EntriesArray = [makeExistsEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [ + { ...getEntryExistsMock(), field: 'b', operator: 'excluded' }, + ]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -555,11 +501,13 @@ describe('build_exceptions_query', () => { test('it returns expected query when exception item includes entry item with "and" values', () => { const entries: EntriesArray = [ - makeExistsEntry({ field: 'b', operator: 'excluded' }), + { ...getEntryExistsMock(), field: 'b', operator: 'excluded' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'value-1' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'value-1' }, + ], }, ]; const query = buildExceptionItem({ @@ -573,16 +521,16 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeExistsEntry({ field: 'b' }), + { ...getEntryExistsMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-1' }), - makeMatchEntry({ field: 'd', value: 'value-2' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'value-1' }, + { ...getEntryMatchMock(), field: 'd', value: 'value-2' }, ], }, - makeExistsEntry({ field: 'e' }), + { ...getEntryExistsMock(), field: 'e' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -596,7 +544,7 @@ describe('build_exceptions_query', () => { describe('match', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeMatchEntry({ field: 'b', value: 'value' })]; + const entries: EntriesArray = [{ ...getEntryMatchMock(), field: 'b', value: 'value' }]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -608,7 +556,7 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes single list item with operator of "excluded"', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', operator: 'excluded', value: 'value' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -621,11 +569,13 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes list item with "and" values', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', operator: 'excluded', value: 'value' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'valueC' }, + ], }, ]; const query = buildExceptionItem({ @@ -639,16 +589,16 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', value: 'value' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'excluded', value: 'valueD' }, ], }, - makeMatchEntry({ field: 'e', value: 'valueE' }), + { ...getEntryMatchMock(), field: 'e', value: 'valueE' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -663,55 +613,59 @@ describe('build_exceptions_query', () => { describe('match_any', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b' })]; + const entries: EntriesArray = [{ ...getEntryMatchAnyMock(), field: 'b' }]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2")'; + const expectedQuery = 'b:("some host name")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [ + { ...getEntryMatchAnyMock(), field: 'b', operator: 'excluded' }, + ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2")'; + const expectedQuery = 'not b:("some host name")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes list item with nested values', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b', operator: 'excluded' }), + { ...getEntryMatchAnyMock(), field: 'b', operator: 'excluded' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + ], }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ not c:"valueC" }'; + const expectedQuery = 'not b:("some host name") and parent:{ not c:"valueC" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchAnyEntry({ field: 'c' }), + { ...getEntryMatchAnyMock(), field: 'b' }, + { ...getEntryMatchAnyMock(), field: 'c' }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2") and c:("value-1" or "value-2")'; + const expectedQuery = 'b:("some host name") and c:("some host name")'; expect(query).toEqual(expectedQuery); }); @@ -735,16 +689,16 @@ describe('build_exceptions_query', () => { const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'included', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'included', value: 'valueD' }, ], }, - makeMatchAnyEntry({ field: 'e', operator: 'excluded' }), + { ...getEntryMatchAnyMock(), field: 'e', operator: 'excluded' }, ]; const queries = buildExceptionListQueries({ language: 'kuery', @@ -758,7 +712,7 @@ describe('build_exceptions_query', () => { }, { query: - 'b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and not e:("value-1" or "value-2")', + 'b:("some host name") and parent:{ c:"valueC" and d:"valueD" } and not e:("some host name")', language: 'kuery', }, ]; @@ -768,20 +722,26 @@ describe('build_exceptions_query', () => { test('it returns expected query when lists exist and language is "lucene"', () => { const payload = getExceptionListItemSchemaMock(); - payload.entries = [makeMatchAnyEntry({ field: 'a' }), makeMatchAnyEntry({ field: 'b' })]; + payload.entries = [ + { ...getEntryMatchAnyMock(), field: 'a' }, + { ...getEntryMatchAnyMock(), field: 'b' }, + ]; const payload2 = getExceptionListItemSchemaMock(); - payload2.entries = [makeMatchAnyEntry({ field: 'c' }), makeMatchAnyEntry({ field: 'd' })]; + payload2.entries = [ + { ...getEntryMatchAnyMock(), field: 'c' }, + { ...getEntryMatchAnyMock(), field: 'd' }, + ]; const queries = buildExceptionListQueries({ language: 'lucene', lists: [payload, payload2], }); const expectedQueries = [ { - query: 'a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")', + query: 'a:("some host name") AND b:("some host name")', language: 'lucene', }, { - query: 'c:("value-1" OR "value-2") AND d:("value-1" OR "value-2")', + query: 'c:("some host name") AND d:("some host name")', language: 'lucene', }, ]; @@ -793,17 +753,17 @@ describe('build_exceptions_query', () => { const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ // TODO: these operators are not being respected. buildNested needs to be updated - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'excluded', value: 'valueD' }, ], }, - makeMatchAnyEntry({ field: 'e' }), + { ...getEntryMatchAnyMock(), field: 'e' }, ]; const queries = buildExceptionListQueries({ language: 'kuery', @@ -817,7 +777,7 @@ describe('build_exceptions_query', () => { }, { query: - 'b:("value-1" or "value-2") and parent:{ not c:"valueC" and not d:"valueD" } and e:("value-1" or "value-2")', + 'b:("some host name") and parent:{ not c:"valueC" and not d:"valueD" } and e:("some host name")', language: 'kuery', }, ]; 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 9a92270fc9c14..aa3f0bf287fca 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -8,16 +8,26 @@ import seedrandom from 'seedrandom'; import { AlertEvent, EndpointEvent, + EndpointStatus, Host, HostMetadata, - OSFields, HostPolicyResponse, HostPolicyResponseActionStatus, + OSFields, PolicyData, - EndpointStatus, } from './types'; import { factory as policyFactory } from './models/policy_config'; import { parentEntityId } from './models/event'; +import { + GetAgentConfigsResponseItem, + GetPackagesResponse, +} from '../../../ingest_manager/common/types/rest_spec'; +import { + AgentConfigStatus, + EsAssetReference, + InstallationStatus, + KibanaAssetReference, +} from '../../../ingest_manager/common/types/models'; export type Event = AlertEvent | EndpointEvent; /** @@ -1062,6 +1072,97 @@ export class EndpointDocGenerator { }; } + /** + * Generate an Agent Configuration (ingest) + */ + public generateAgentConfig(): GetAgentConfigsResponseItem { + return { + id: this.seededUUIDv4(), + name: 'Agent Config', + status: AgentConfigStatus.Active, + description: 'Some description', + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + revision: 2, + updated_at: '2020-07-22T16:36:49.196Z', + updated_by: 'elastic', + package_configs: ['852491f0-cc39-11ea-bac2-cdbf95b4b41a'], + agents: 0, + }; + } + + /** + * Generate an EPM Package for Endpoint + */ + public generateEpmPackage(): GetPackagesResponse['response'][0] { + return { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + description: 'This is the Elastic Endpoint package.', + type: 'solution', + download: '/epr/endpoint/endpoint-0.5.0.tar.gz', + path: '/package/endpoint/0.5.0', + icons: [ + { + src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + size: '16x16', + type: 'image/svg+xml', + }, + ], + status: 'installed' as InstallationStatus, + savedObject: { + type: 'epm-packages', + id: 'endpoint', + attributes: { + installed_kibana: [ + { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, + { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, + ] as KibanaAssetReference[], + installed_es: [ + { id: 'logs-endpoint.alerts', type: 'index_template' }, + { id: 'events-endpoint', type: 'index_template' }, + { id: 'logs-endpoint.events.file', type: 'index_template' }, + { id: 'logs-endpoint.events.library', type: 'index_template' }, + { id: 'metrics-endpoint.metadata', type: 'index_template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, + { id: 'logs-endpoint.events.network', type: 'index_template' }, + { id: 'metrics-endpoint.policy', type: 'index_template' }, + { id: 'logs-endpoint.events.process', type: 'index_template' }, + { id: 'logs-endpoint.events.registry', type: 'index_template' }, + { id: 'logs-endpoint.events.security', type: 'index_template' }, + { id: 'metrics-endpoint.telemetry', type: 'index_template' }, + ] as EsAssetReference[], + es_index_patterns: { + alerts: 'logs-endpoint.alerts-*', + events: 'events-endpoint-*', + file: 'logs-endpoint.events.file-*', + library: 'logs-endpoint.events.library-*', + metadata: 'metrics-endpoint.metadata-*', + metadata_mirror: 'metrics-endpoint.metadata_mirror-*', + network: 'logs-endpoint.events.network-*', + policy: 'metrics-endpoint.policy-*', + process: 'logs-endpoint.events.process-*', + registry: 'logs-endpoint.events.registry-*', + security: 'logs-endpoint.events.security-*', + telemetry: 'metrics-endpoint.telemetry-*', + }, + name: 'endpoint', + version: '0.5.0', + internal: false, + removable: false, + }, + references: [], + updated_at: '2020-06-24T14:41:23.098Z', + version: 'Wzc0LDFd', + }, + }; + } + /** * Generates a Host Policy response message */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index a982f9ffe8f21..1c24e1abe5a57 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -104,6 +104,8 @@ export interface ResolverChildNode extends ResolverLifecycleNode { * * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants * using this node's entity_id + * + * For more information see the resolver docs on pagination [here](../../server/endpoint/routes/resolver/docs/README.md#L129) */ nextChild?: string | null; } diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx index 72467a62f57c1..998ed1f3351c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx @@ -232,7 +232,6 @@ describe('AutocompleteFieldMatchComponent', () => { fields, }, value: 'value 1', - signal: new AbortController().signal, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 137f6803dc54e..dfb3761bb3497 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -72,14 +72,13 @@ export const AutocompleteFieldMatchComponent: React.FC { - const signal = new AbortController().signal; - - updateSuggestions({ - fieldSelected: selectedField, - value: `${searchVal}`, - patterns: indexPattern, - signal, - }); + if (updateSuggestions != null) { + updateSuggestions({ + fieldSelected: selectedField, + value: searchVal, + patterns: indexPattern, + }); + } }; const isValid = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx index f3f0f2e2a44b1..0a0281a9c4a51 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx @@ -232,7 +232,6 @@ describe('AutocompleteFieldMatchAnyComponent', () => { fields, }, value: 'value 1', - signal: new AbortController().signal, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx index 5a15c1f7238de..1952ef865e045 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx @@ -65,14 +65,13 @@ export const AutocompleteFieldMatchAnyComponent: React.FC { - const signal = new AbortController().signal; - - updateSuggestions({ - fieldSelected: selectedField, - value: `${searchVal}`, - patterns: indexPattern, - signal, - }); + if (updateSuggestions != null) { + updateSuggestions({ + fieldSelected: selectedField, + value: searchVal, + patterns: indexPattern, + }); + } }; const onCreateOption = (option: string) => onChange([...(selectedValue || []), option]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index def2a303f6038..a76b50d11a875 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -199,12 +199,17 @@ describe('useFieldValueAutocomplete', () => { await waitForNextUpdate(); await waitForNextUpdate(); - result.current[2]({ - fieldSelected: getField('@tags'), - value: 'hello', - patterns: stubIndexPatternWithFields, - signal: new AbortController().signal, - }); + expect(result.current[2]).not.toBeNull(); + + // Added check for typescripts sake, if null, + // would not reach below logic as test would stop above + if (result.current[2] != null) { + result.current[2]({ + fieldSelected: getField('@tags'), + value: 'hello', + patterns: stubIndexPatternWithFields, + }); + } await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts index 541c0a8d3fbae..a53914da93f27 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -11,16 +11,13 @@ import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/d import { useKibana } from '../../../../common/lib/kibana'; import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; -export type UseFieldValueAutocompleteReturn = [ - boolean, - string[], - (args: { - fieldSelected: IFieldType | undefined; - value: string | string[] | undefined; - patterns: IIndexPattern | undefined; - signal: AbortSignal; - }) => void -]; +type Func = (args: { + fieldSelected: IFieldType | undefined; + value: string | string[] | undefined; + patterns: IIndexPattern | undefined; +}) => void; + +export type UseFieldValueAutocompleteReturn = [boolean, string[], Func | null]; export interface UseFieldValueAutocompleteProps { selectedField: IFieldType | undefined; @@ -41,62 +38,77 @@ export const useFieldValueAutocomplete = ({ const { services } = useKibana(); const [isLoading, setIsLoading] = useState(false); const [suggestions, setSuggestions] = useState([]); - const updateSuggestions = useRef( - debounce( + const updateSuggestions = useRef(null); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchSuggestions = debounce( async ({ fieldSelected, value, patterns, - signal, }: { fieldSelected: IFieldType | undefined; value: string | string[] | undefined; patterns: IIndexPattern | undefined; - signal: AbortSignal; }) => { - if (fieldSelected == null || patterns == null) { - return; - } + const inputValue: string | string[] = value ?? ''; + const userSuggestion: string = Array.isArray(inputValue) + ? inputValue[inputValue.length - 1] ?? '' + : inputValue; - setIsLoading(true); + try { + if (isSubscribed) { + if (fieldSelected == null || patterns == null) { + return; + } - // Fields of type boolean should only display two options - if (fieldSelected.type === 'boolean') { - setIsLoading(false); - setSuggestions(['true', 'false']); - return; - } + setIsLoading(true); - const newSuggestions = await services.data.autocomplete.getValueSuggestions({ - indexPattern: patterns, - field: fieldSelected, - query: '', - signal, - }); + // Fields of type boolean should only display two options + if (fieldSelected.type === 'boolean') { + setIsLoading(false); + setSuggestions(['true', 'false']); + return; + } - setIsLoading(false); - setSuggestions(newSuggestions); + const newSuggestions = await services.data.autocomplete.getValueSuggestions({ + indexPattern: patterns, + field: fieldSelected, + query: userSuggestion.trim(), + signal: abortCtrl.signal, + }); + + setIsLoading(false); + setSuggestions([...newSuggestions]); + } + } catch (error) { + if (isSubscribed) { + setSuggestions([]); + setIsLoading(false); + } + } }, 500 - ) - ); - - useEffect(() => { - const abortCtrl = new AbortController(); + ); if (operatorType !== OperatorTypeEnum.EXISTS) { - updateSuggestions.current({ + fetchSuggestions({ fieldSelected: selectedField, value: fieldValue, patterns: indexPattern, - signal: abortCtrl.signal, }); } + updateSuggestions.current = fetchSuggestions; + return (): void => { + isSubscribed = false; abortCtrl.abort(); }; - }, [updateSuggestions, selectedField, operatorType, fieldValue, indexPattern]); + }, [services.data.autocomplete, selectedField, operatorType, fieldValue, indexPattern]); return [isLoading, suggestions, updateSuggestions.current]; }; 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 bb547f05090b7..e6eaa4947e404 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 @@ -40,6 +40,7 @@ import { AddExceptionComments } from '../add_exception_comments'; import { enrichNewExceptionItemsWithComments, enrichExceptionItemsWithOS, + lowercaseHashValues, defaultEndpointExceptionItems, entryHasListType, entryHasNonEcsType, @@ -256,7 +257,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ : exceptionItemsToAdd; if (exceptionListType === 'endpoint') { const osTypes = retrieveAlertOsTypes(); - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx index 3dcc3eb5a8329..0f54ec29cc540 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx @@ -55,6 +55,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -81,6 +82,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -111,6 +113,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -143,6 +146,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -175,6 +179,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -207,6 +212,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -239,6 +245,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -271,6 +278,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -306,6 +314,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -331,6 +340,62 @@ describe('BuilderEntryItem', () => { ).toBeTruthy(); }); + test('it uses "correspondingKeywordField" if it exists', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').prop('selectedField') + ).toEqual({ + name: 'extension', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); + }); + test('it invokes "onChange" when new field is selected and resets operator and value fields', () => { const mockOnChange = jest.fn(); const wrapper = mount( @@ -342,6 +407,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -376,6 +442,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -410,6 +477,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -444,6 +512,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -478,6 +547,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx index 5939a5a1b576e..3883a2fad2cf2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx @@ -95,7 +95,7 @@ export const BuilderEntryItem: React.FC = ({ const renderFieldInput = useCallback( (isFirst: boolean): JSX.Element => { - const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry); + const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry, listType); const comboBox = ( = ({ return comboBox; } }, - [handleFieldChange, indexPattern, entry] + [handleFieldChange, indexPattern, entry, listType] ); const renderOperatorInput = (isFirst: boolean): JSX.Element => { @@ -170,7 +170,11 @@ export const BuilderEntryItem: React.FC = ({ return ( = ({ return ( { + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + autocomplete: { + getValueSuggestions: getValueSuggestionsMock, + }, + }, + }, + }); + }); + + afterEach(() => { + getValueSuggestionsMock.mockClear(); + }); + describe('and badge logic', () => { test('it renders "and" badge with extra top padding for the first exception item when "andLogicIncluded" is "true"', () => { const exceptionItem = { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index 17c94adf42648..224c99756eb5c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -41,6 +41,7 @@ import { getOperatorOptions, getUpdatedEntriesOnDelete, isEntryNested, + getCorrespondingKeywordField, } from './helpers'; import { OperatorOption } from '../../autocomplete/types'; @@ -57,6 +58,7 @@ const getMockBuilderEntry = (): FormattedBuilderEntry => ({ nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }); const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({ @@ -73,6 +75,7 @@ const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({ parentIndex: 0, }, entryIndex: 0, + correspondingKeywordField: undefined, }); const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({ @@ -82,69 +85,305 @@ const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({ nested: 'parent', parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }); +const mockEndpointFields = [ + { + name: 'file.path.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, +]; + +export const getEndpointField = (name: string) => + mockEndpointFields.find((field) => field.name === name) as IFieldType; + describe('Exception builder helpers', () => { + describe('#getCorrespondingKeywordField', () => { + test('it returns matching keyword field if "selectedFieldIsTextType" is true and keyword field exists', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: 'machine.os.raw.text', + }); + + expect(output).toEqual(getField('machine.os.raw')); + }); + + test('it returns undefined if "selectedFieldIsTextType" is false', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: 'machine.os.raw', + }); + + expect(output).toEqual(undefined); + }); + + test('it returns undefined if "selectedField" is empty string', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: '', + }); + + expect(output).toEqual(undefined); + }); + + test('it returns undefined if "selectedField" is undefined', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: undefined, + }); + + expect(output).toEqual(undefined); + }); + }); + describe('#getFilteredIndexPatterns', () => { - test('it returns nested fields that match parent value when "item.nested" is "child"', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [ - { ...getField('nestedField.child') }, - { ...getField('nestedField.nestedChild.doublyNestedChild') }, - ], - id: '1234', - title: 'logstash-*', - }; - expect(output).toEqual(expected); + describe('list type detections', () => { + test('it returns nested fields that match parent value when "item.nested" is "child"', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [ + { ...getField('nestedField.child') }, + { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [{ ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] }], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [ + { ...getField('nestedField.child') }, + { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns all fields unfiletered if "item.nested" is not "child" or "parent"', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [...fields], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); }); - test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [{ ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] }], - id: '1234', - title: 'logstash-*', - }; - expect(output).toEqual(expected); + describe('list type endpoint', () => { + let payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + + beforeAll(() => { + payloadIndexPattern = { + ...payloadIndexPattern, + fields: [...payloadIndexPattern.fields, ...mockEndpointFields], + }; + }); + + test('it returns nested fields that match parent value when "item.nested" is "child"', () => { + const payloadItem: FormattedBuilderEntry = { + field: getEndpointField('file.Ext.code_signature.status'), + operator: isOperator, + value: 'some value', + nested: 'child', + parent: { + parent: { + ...getEntryNestedMock(), + field: 'file.Ext.code_signature', + entries: [{ ...getEntryMatchMock(), field: 'child' }], + }, + parentIndex: 0, + }, + entryIndex: 0, + correspondingKeywordField: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [getEndpointField('file.Ext.code_signature.status')], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: { + ...getEndpointField('file.Ext.code_signature.status'), + name: 'file.Ext.code_signature', + esTypes: ['nested'], + }, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [ + { + aggregatable: false, + count: 0, + esTypes: ['nested'], + name: 'file.Ext.code_signature', + readFromDocValues: false, + scripted: false, + searchable: true, + subType: { + nested: { + path: 'file.Ext.code_signature', + }, + }, + type: 'string', + }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [getEndpointField('file.Ext.code_signature.status')], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns all fields that matched those in "exceptionable_fields.json" with no further filtering if "item.nested" is not "child" or "parent"', () => { + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [ + { + aggregatable: false, + count: 0, + esTypes: ['text'], + name: 'file.path.text', + readFromDocValues: false, + scripted: false, + searchable: true, + type: 'string', + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); }); + }); - test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = { - ...getMockNestedParentBuilderEntry(), - field: undefined, - }; - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { + describe('#getFormattedBuilderEntry', () => { + test('it returns entry with a value for "correspondingKeywordField" when "item.field" is of type "text" and matching keyword field exists', () => { + const payloadIndexPattern: IIndexPattern = { + ...getMockIndexPattern(), fields: [ - { ...getField('nestedField.child') }, - { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ...fields, + { + name: 'machine.os.raw.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: true, + }, ], - id: '1234', - title: 'logstash-*', }; - expect(output).toEqual(expected); - }); - - test('it returns all fields unfiletered if "item.nested" is not "child" or "parent"', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [...fields], - id: '1234', - title: 'logstash-*', + const payloadItem: BuilderEntry = { + ...getEntryMatchMock(), + field: 'machine.os.raw.text', + value: 'some os', + }; + const output = getFormattedBuilderEntry( + payloadIndexPattern, + payloadItem, + 0, + undefined, + undefined + ); + const expected: FormattedBuilderEntry = { + entryIndex: 0, + field: { + name: 'machine.os.raw.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: true, + }, + nested: undefined, + operator: isOperator, + parent: undefined, + value: 'some os', + correspondingKeywordField: getField('machine.os.raw'), }; expect(output).toEqual(expected); }); - }); - describe('#getFormattedBuilderEntry', () => { test('it returns "FormattedBuilderEntry" with value "nested" of "child" when "parent" and "parentIndex" are defined', () => { const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); const payloadItem: BuilderEntry = { ...getEntryMatchMock(), field: 'child' }; @@ -188,6 +427,7 @@ describe('Exception builder helpers', () => { parentIndex: 1, }, value: 'some host name', + correspondingKeywordField: undefined, }; expect(output).toEqual(expected); }); @@ -218,6 +458,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }; expect(output).toEqual(expected); }); @@ -225,7 +466,7 @@ describe('Exception builder helpers', () => { describe('#isEntryNested', () => { test('it returns "false" if payload is not of type EntryNested', () => { - const payload: BuilderEntry = { ...getEntryMatchMock() }; + const payload: BuilderEntry = getEntryMatchMock(); const output = isEntryNested(payload); const expected = false; expect(output).toEqual(expected); @@ -242,7 +483,7 @@ describe('Exception builder helpers', () => { describe('#getFormattedBuilderEntries', () => { test('it returns formatted entry with field undefined if it unable to find a matching index pattern field', () => { const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItems: BuilderEntry[] = [{ ...getEntryMatchMock() }]; + const payloadItems: BuilderEntry[] = [getEntryMatchMock()]; const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems); const expected: FormattedBuilderEntry[] = [ { @@ -252,6 +493,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some host name', + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); @@ -281,6 +523,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }, { entryIndex: 1, @@ -298,6 +541,7 @@ describe('Exception builder helpers', () => { operator: isOneOfOperator, parent: undefined, value: ['some extension'], + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); @@ -333,6 +577,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }, { entryIndex: 1, @@ -347,6 +592,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: undefined, + correspondingKeywordField: undefined, }, { entryIndex: 0, @@ -383,6 +629,7 @@ describe('Exception builder helpers', () => { parentIndex: 1, }, value: 'some host name', + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx index 93bae091885c1..8585f58504e31 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx @@ -33,6 +33,8 @@ import { EmptyNestedEntry, } from '../types'; import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import exceptionableFields from '../exceptionable_fields.json'; /** * Returns filtered index patterns based on the field - if a user selects to @@ -45,13 +47,21 @@ import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; */ export const getFilteredIndexPatterns = ( patterns: IIndexPattern, - item: FormattedBuilderEntry + item: FormattedBuilderEntry, + type: ExceptionListType ): IIndexPattern => { + const indexPatterns = { + ...patterns, + fields: patterns.fields.filter(({ name }) => + type === 'endpoint' ? exceptionableFields.includes(name) : true + ), + }; + if (item.nested === 'child' && item.parent != null) { // when user has selected a nested entry, only fields with the common parent are shown return { - ...patterns, - fields: patterns.fields.filter( + ...indexPatterns, + fields: indexPatterns.fields.filter( (field) => field.subType != null && field.subType.nested != null && @@ -61,20 +71,53 @@ export const getFilteredIndexPatterns = ( }; } else if (item.nested === 'parent' && item.field != null) { // when user has selected a nested entry, right above it we show the common parent - return { ...patterns, fields: [item.field] }; + return { ...indexPatterns, fields: [item.field] }; } else if (item.nested === 'parent' && item.field == null) { // when user selects to add a nested entry, only nested fields are shown as options return { - ...patterns, - fields: patterns.fields.filter( + ...indexPatterns, + fields: indexPatterns.fields.filter( (field) => field.subType != null && field.subType.nested != null ), }; } else { - return patterns; + return indexPatterns; } }; +/** + * Fields of type 'text' do not generate autocomplete values, we want + * to find it's corresponding keyword type (if available) which does + * generate autocomplete values + * + * @param fields IFieldType fields + * @param selectedField the field name that was selected + * @param isTextType we only want a corresponding keyword field if + * the selected field is of type 'text' + * + */ +export const getCorrespondingKeywordField = ({ + fields, + selectedField, +}: { + fields: IFieldType[]; + selectedField: string | undefined; +}): IFieldType | undefined => { + const selectedFieldBits = + selectedField != null && selectedField !== '' ? selectedField.split('.') : []; + const selectedFieldIsTextType = selectedFieldBits.slice(-1)[0] === 'text'; + + if (selectedFieldIsTextType && selectedFieldBits.length > 0) { + const keywordField = selectedFieldBits.slice(0, selectedFieldBits.length - 1).join('.'); + const [foundKeywordField] = fields.filter( + ({ name }) => keywordField !== '' && keywordField === name + ); + return foundKeywordField; + } + + return undefined; +}; + /** * Formats the entry into one that is easily usable for the UI, most of the * complexity was introduced with nested fields @@ -95,11 +138,16 @@ export const getFormattedBuilderEntry = ( ): FormattedBuilderEntry => { const { fields } = indexPattern; const field = parent != null ? `${parent.field}.${item.field}` : item.field; - const [selectedField] = fields.filter(({ name }) => field != null && field === name); + const [foundField] = fields.filter(({ name }) => field != null && field === name); + const correspondingKeywordField = getCorrespondingKeywordField({ + fields, + selectedField: field, + }); if (parent != null && parentIndex != null) { return { - field: selectedField, + field: foundField, + correspondingKeywordField, operator: getExceptionOperatorSelect(item), value: getEntryValue(item), nested: 'child', @@ -108,7 +156,8 @@ export const getFormattedBuilderEntry = ( }; } else { return { - field: selectedField, + field: foundField, + correspondingKeywordField, operator: getExceptionOperatorSelect(item), value: getEntryValue(item), nested: undefined, @@ -167,6 +216,7 @@ export const getFormattedBuilderEntries = ( value: undefined, entryIndex: index, parent: undefined, + correspondingKeywordField: undefined, }; // User has selected to add a nested field, but not yet selected the field diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 734434484fb4c..b82607a541aaa 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; @@ -29,8 +29,6 @@ import { getDefaultEmptyEntry, getDefaultNestedEmptyEntry, } from './helpers'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import exceptionableFields from '../exceptionable_fields.json'; const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; @@ -244,17 +242,6 @@ export const ExceptionBuilder = ({ setUpdateExceptions([...exceptions, { ...newException }]); }, [setUpdateExceptions, exceptions, listType, listId, listNamespaceType, ruleName]); - // Filters index pattern fields by exceptionable fields if list type is endpoint - const filterIndexPatterns = useMemo((): IIndexPattern => { - if (listType === 'endpoint') { - return { - ...indexPatterns, - fields: indexPatterns.fields.filter(({ name }) => exceptionableFields.includes(name)), - }; - } - return indexPatterns; - }, [indexPatterns, listType]); - // The builder can have existing exception items, or new exception items that have yet // to be created (and thus lack an id), this was creating some React bugs with relying // on the index, as a result, created a temporary id when new exception items are first @@ -368,7 +355,7 @@ export const ExceptionBuilder = ({ key={getExceptionListItemId(exceptionListItem, index)} exceptionItem={exceptionListItem} exceptionId={getExceptionListItemId(exceptionListItem, index)} - indexPattern={filterIndexPatterns} + indexPattern={indexPatterns} listType={listType} addNested={addNested} exceptionItemIndex={index} 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 341d2f2bab37a..6109b85f2da5a 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 @@ -40,6 +40,7 @@ import { getOperatingSystems, entryHasListType, entryHasNonEcsType, + lowercaseHashValues, } from '../helpers'; import { Loader } from '../../loader'; @@ -195,7 +196,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ ]; if (exceptionListType === 'endpoint') { const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : []; - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [exceptionItemsToAdd, exceptionItem, comment, exceptionListType]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json index fdf0ea60ecf6a..037e340ee7fa2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json @@ -6,32 +6,25 @@ "Target.process.Ext.code_signature.valid", "Target.process.Ext.services", "Target.process.Ext.user", - "Target.process.command_line", "Target.process.command_line.text", - "Target.process.executable", "Target.process.executable.text", "Target.process.hash.md5", "Target.process.hash.sha1", "Target.process.hash.sha256", "Target.process.hash.sha512", - "Target.process.name", "Target.process.name.text", "Target.process.parent.Ext.code_signature.status", "Target.process.parent.Ext.code_signature.subject_name", "Target.process.parent.Ext.code_signature.trusted", "Target.process.parent.Ext.code_signature.valid", - "Target.process.parent.command_line", "Target.process.parent.command_line.text", - "Target.process.parent.executable", "Target.process.parent.executable.text", "Target.process.parent.hash.md5", "Target.process.parent.hash.sha1", "Target.process.parent.hash.sha256", "Target.process.parent.hash.sha512", - "Target.process.parent.name", "Target.process.parent.name.text", "Target.process.parent.pgid", - "Target.process.parent.working_directory", "Target.process.parent.working_directory.text", "Target.process.pe.company", "Target.process.pe.description", @@ -39,7 +32,6 @@ "Target.process.pe.original_file_name", "Target.process.pe.product", "Target.process.pgid", - "Target.process.working_directory", "Target.process.working_directory.text", "agent.id", "agent.type", @@ -74,7 +66,6 @@ "file.mode", "file.name", "file.owner", - "file.path", "file.path.text", "file.pe.company", "file.pe.description", @@ -82,7 +73,6 @@ "file.pe.original_file_name", "file.pe.product", "file.size", - "file.target_path", "file.target_path.text", "file.type", "file.uid", @@ -94,10 +84,8 @@ "host.id", "host.os.Ext.variant", "host.os.family", - "host.os.full", "host.os.full.text", "host.os.kernel", - "host.os.name", "host.os.name.text", "host.os.platform", "host.os.version", @@ -108,32 +96,25 @@ "process.Ext.code_signature.valid", "process.Ext.services", "process.Ext.user", - "process.command_line", "process.command_line.text", - "process.executable", "process.executable.text", "process.hash.md5", "process.hash.sha1", "process.hash.sha256", "process.hash.sha512", - "process.name", "process.name.text", "process.parent.Ext.code_signature.status", "process.parent.Ext.code_signature.subject_name", "process.parent.Ext.code_signature.trusted", "process.parent.Ext.code_signature.valid", - "process.parent.command_line", "process.parent.command_line.text", - "process.parent.executable", "process.parent.executable.text", "process.parent.hash.md5", "process.parent.hash.sha1", "process.parent.hash.sha256", "process.parent.hash.sha512", - "process.parent.name", "process.parent.name.text", "process.parent.pgid", - "process.parent.working_directory", "process.parent.working_directory.text", "process.pe.company", "process.pe.description", @@ -141,7 +122,10 @@ "process.pe.original_file_name", "process.pe.product", "process.pgid", - "process.working_directory", "process.working_directory.text", - "rule.uuid" + "rule.uuid", + "user.domain", + "user.email", + "user.hash", + "user.id" ] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 5cb65ee6db8ff..4236f347ac7ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -24,6 +24,7 @@ import { entryHasListType, entryHasNonEcsType, prepareExceptionItemsForBulkClose, + lowercaseHashValues, } from './helpers'; import { EmptyEntry } from './types'; import { @@ -364,7 +365,7 @@ describe('Exception helpers', () => { const mockEmptyException: EntryNested = { field: '', type: OperatorTypeEnum.NESTED, - entries: [{ ...getEntryMatchMock() }], + entries: [getEntryMatchMock()], }; const output: Array< ExceptionListItemSchema | CreateExceptionListItemSchema @@ -663,4 +664,48 @@ describe('Exception helpers', () => { expect(result).toEqual(expected); }); }); + + describe('#lowercaseHashValues', () => { + test('it should return an empty array with an empty array', () => { + const payload: ExceptionListItemSchema[] = []; + const result = lowercaseHashValues(payload); + expect(result).toEqual([]); + }); + + test('it should return all list items with entry hashes lowercased', () => { + const payload = [ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'DDDFFF' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'DDDFFF'] }, + ] as EntriesArray, + }, + ]; + const result = lowercaseHashValues(payload); + expect(result).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'dddfff' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'dddfff'] }, + ] as EntriesArray, + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3abb788312ff4..2b526ede12acf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -335,6 +335,36 @@ export const enrichExceptionItemsWithOS = ( }); }; +/** + * Returns given exceptionItems with all hash-related entries lowercased + */ +export const lowercaseHashValues = ( + exceptionItems: Array +): Array => { + return exceptionItems.map((item) => { + const newEntries = item.entries.map((itemEntry) => { + if (itemEntry.field.includes('.hash')) { + if (itemEntry.type === 'match') { + return { + ...itemEntry, + value: itemEntry.value.toLowerCase(), + }; + } else if (itemEntry.type === 'match_any') { + return { + ...itemEntry, + value: itemEntry.value.map((val) => val.toLowerCase()), + }; + } + } + return itemEntry; + }); + return { + ...item, + entries: newEntries, + }; + }); +}; + /** * Returns the value for the given fieldname within TimelineNonEcsData if it exists */ @@ -413,7 +443,7 @@ export const defaultEndpointExceptionItems = ( data: alertData, fieldName: 'file.Ext.code_signature.trusted', }); - const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' }); + const [sha256Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha256' }); const [eventCode] = getMappedNonEcsValue({ data: alertData, fieldName: 'event.code' }); const namespaceType = 'agnostic'; @@ -446,10 +476,10 @@ export const defaultEndpointExceptionItems = ( value: filePath ?? '', }, { - field: 'file.hash.sha1', + field: 'file.hash.sha256', operator: 'included', type: 'match', - value: sha1Hash ?? '', + value: sha256Hash ?? '', }, { field: 'event.code', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 83367e5b9e739..9b7c68848c41f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -62,6 +62,7 @@ export interface FormattedBuilderEntry { nested: 'parent' | 'child' | undefined; entryIndex: number; parent: { parent: EntryNested; parentIndex: number } | undefined; + correspondingKeywordField: IFieldType | undefined; } export interface EmptyEntry { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx index 7069e99943f7b..bcc4adf54c9d7 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx @@ -52,6 +52,14 @@ const MyActionButton = styled(EuiFlexItem)` align-self: flex-end; `; +const MyNestedValueContainer = styled.div` + margin-left: ${({ theme }) => theme.eui.euiSizeL}; +`; + +const MyNestedValue = styled.span` + margin-left: ${({ theme }) => theme.eui.euiSizeS}; +`; + interface ExceptionEntriesComponentProps { entries: FormattedEntry[]; disableDelete: boolean; @@ -78,10 +86,10 @@ const ExceptionEntriesComponent = ({ render: (value: string | null, data: FormattedEntry) => { if (value != null && data.isNested) { return ( - <> - - {value} - + + + {value} + ); } else { return value ?? getEmptyValue(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx index 5d4340db9a448..5f6e54b0d3cff 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx @@ -106,13 +106,13 @@ describe('Exception viewer helpers', () => { value: undefined, }, { - fieldName: 'host.name.host.name', + fieldName: 'host.name', isNested: true, operator: 'is', value: 'some host name', }, { - fieldName: 'host.name.host.name', + fieldName: 'host.name', isNested: true, operator: 'is one of', value: ['some host name'], @@ -138,9 +138,9 @@ describe('Exception viewer helpers', () => { test('it formats as expected when "isNested" is "true"', () => { const payload = getEntryMatchMock(); - const formattedEntry = formatEntry({ isNested: true, parent: 'parent', item: payload }); + const formattedEntry = formatEntry({ isNested: true, item: payload }); const expected: FormattedEntry = { - fieldName: 'parent.host.name', + fieldName: 'host.name', isNested: true, operator: 'is', value: 'some host name', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index 345db5bf1e75e..86b0512410e6f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -20,18 +20,16 @@ import * as i18n from '../translations'; */ export const formatEntry = ({ isNested, - parent, item, }: { isNested: boolean; - parent?: string; item: BuilderEntry; }): FormattedEntry => { const operator = getExceptionOperatorSelect(item); const value = getEntryValue(item); return { - fieldName: isNested ? `${parent}.${item.field}` : item.field ?? '', + fieldName: item.field ?? '', operator: operator.message, value, isNested, @@ -57,7 +55,6 @@ export const getFormattedEntries = (entries: BuilderEntry[]): FormattedEntry[] = (acc, nestedEntry) => { const formattedEntry = formatEntry({ isNested: true, - parent: item.field, item: nestedEntry, }); return [...acc, { ...formattedEntry }]; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index cbbe43cc03568..0ba9764cf24af 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -5,14 +5,15 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; -import { AlertsUtilityBar } from './index'; +import { AlertsUtilityBar, AlertsUtilityBarProps } from './index'; +import { TestProviders } from '../../../../common/mock/test_providers'; jest.mock('../../../../common/lib/kibana'); describe('AlertsUtilityBar', () => { - it('renders correctly', () => { + test('renders correctly', () => { const wrapper = shallow( { expect(wrapper.find('[dataTestSubj="alertActionPopover"]')).toBeTruthy(); }); + + describe('UtilityBarAdditionalFiltersContent', () => { + test('does not show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is false', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be false + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + }); + + test('does show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is true', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + + test('calls the onShowBuildingBlockAlertsChanged when the check box is clicked', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // check the box + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .simulate('change', { target: { checked: true } }); + + // Make sure our callback is called + expect(onShowBuildingBlockAlertsChanged).toHaveBeenCalled(); + }); + + test('can update showBuildingBlockAlerts from false to true', () => { + const Proxy = (props: AlertsUtilityBarProps) => ( + + + + ); + + const wrapper = mount( + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should false now since we initially set the showBuildingBlockAlerts to false + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + + wrapper.setProps({ showBuildingBlockAlerts: true }); + wrapper.update(); + + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true now since we changed the showBuildingBlockAlerts from false to true + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index bedc23790541c..bdad380f59ae9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -28,7 +28,7 @@ import { TimelineNonEcsData } from '../../../../graphql/types'; import { UpdateAlertsStatus } from '../types'; import { FILTER_CLOSED, FILTER_IN_PROGRESS, FILTER_OPEN } from '../alerts_filter_group'; -interface AlertsUtilityBarProps { +export interface AlertsUtilityBarProps { canUserCRUD: boolean; hasIndexWrite: boolean; areEventsLoading: boolean; @@ -223,5 +223,6 @@ export const AlertsUtilityBar = React.memo( prevProps.areEventsLoading === nextProps.areEventsLoading && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection + prevProps.showClearSelection === nextProps.showClearSelection && + prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 010129d2d4593..f38a9107afca9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -202,7 +202,7 @@ export const requiredFieldsForActions = [ 'file.path', 'file.Ext.code_signature.subject_name', 'file.Ext.code_signature.trusted', - 'file.hash.sha1', + 'file.hash.sha256', 'host.os.family', 'event.code', ]; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts index 49da7dbf6d514..9b0cec99b1b38 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts @@ -17,7 +17,7 @@ export const PRE_BUILT_MSG = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage', { defaultMessage: - 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', + 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules except the Elastic Endpoint Security rule are disabled. You can select additional rules you want to activate.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index 35816e82540d1..0f16cb99862a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -70,7 +70,7 @@ export const RiskScoreField = ({ { field: newField?.name ?? '', operator: 'equals', - value: undefined, + value: '', riskScore: undefined, }, ], diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 46829b9cb8f7b..f58c95ed71e29 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -20,6 +20,7 @@ import { getPrePackagedRulesStatus, } from './api'; import { ruleMock, rulesMock } from './mock'; +import { buildEsQuery } from 'src/plugins/data/common'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -165,7 +166,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { method: 'GET', query: { - filter: 'alert.attributes.tags: hello AND alert.attributes.tags: world', + filter: 'alert.attributes.tags: "hello" AND alert.attributes.tags: "world"', page: 1, per_page: 20, sort_field: 'enabled', @@ -175,6 +176,75 @@ describe('Detections Rules API', () => { }); }); + test('query with tags KQL parses without errors when tags contain characters such as left parenthesis (', async () => { + await fetchRules({ + filterOptions: { + filter: 'ruleName', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['('], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + + test('query KQL parses without errors when filter contains characters such as double quotes', async () => { + await fetchRules({ + filterOptions: { + filter: '"test"', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: [], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + + test('query KQL parses without errors when tags contains characters such as double quotes', async () => { + await fetchRules({ + filterOptions: { + filter: '"test"', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['"test"'], + }, + signal: abortCtrl.signal, + }); + const [ + [ + , + { + query: { filter }, + }, + ], + ] = fetchMock.mock.calls; + expect(() => buildEsQuery(undefined, { query: filter, language: 'kuery' }, [])).not.toThrow(); + }); + test('check parameter url, query with all options', async () => { await fetchRules({ filterOptions: { @@ -191,7 +261,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: hello AND alert.attributes.tags: world', + 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: "hello" AND alert.attributes.tags: "world"', page: 1, per_page: 20, sort_field: 'enabled', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 08d564230b85f..3538d8ec8c9b9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -97,7 +97,7 @@ export const fetchRules = async ({ ...(filterOptions.showElasticRules ? [`alert.attributes.tags: "__internal_immutable:true"`] : []), - ...(filterOptions.tags?.map((t) => `alert.attributes.tags: ${t}`) ?? []), + ...(filterOptions.tags?.map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) ?? []), ]; const query = { diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 3636358ebe842..eeb1533f57a67 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -54,7 +54,8 @@ export const getHostListPath = ( }; export const getHostDetailsPath = ( - props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostDetailsUrlProps, + props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostIndexUIQueryParams & + HostDetailsUrlProps, search?: string ) => { const { name, ...queryParams } = props; diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx index aa562b9a20201..54d9131209d0d 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx @@ -5,10 +5,15 @@ */ import React, { memo } from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; import { PageView, PageViewProps } from '../../common/components/endpoint/page_view'; export const ManagementPageView = memo>((options) => { - return ; + return ( + + + + ); }); ManagementPageView.displayName = 'ManagementPageView'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 621fab2e4ee11..4a4326d5b2919 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -81,6 +81,11 @@ interface ServerReturnedHostNonExistingPolicies { payload: HostState['nonExistingPolicies']; } +interface ServerReturnedHostExistValue { + type: 'serverReturnedHostExistValue'; + payload: boolean; +} + export type HostAction = | ServerReturnedHostList | ServerFailedToReturnHostList @@ -92,6 +97,7 @@ export type HostAction = | ServerFailedToReturnPoliciesForOnboarding | UserSelectedEndpointPolicy | ServerCancelledHostListLoading + | ServerReturnedHostExistValue | ServerCancelledPolicyItemsLoading | ServerReturnedEndpointPackageInfo | ServerReturnedHostNonExistingPolicies; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index b6e18506b6111..8ff4ad5a043b5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -51,6 +51,7 @@ describe('HostList store concerns', () => { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + hostsExist: true, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index edeca5659ee38..74bebf211258a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'kibana/public'; +import { HttpStart } from 'kibana/public'; import { HostInfo, HostResultList } from '../../../../../common/endpoint/types'; import { GetPolicyListResponse } from '../../policy/types'; import { ImmutableMiddlewareFactory } from '../../../../common/store'; @@ -28,6 +28,8 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor return ({ getState, dispatch }) => (next) => async (action) => { next(action); const state = getState(); + + // Host list if ( action.type === 'userChangedUrl' && isOnHostPage(state) && @@ -89,6 +91,19 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor // No hosts, so we should check to see if there are policies for onboarding if (hostResponse && hostResponse.hosts.length === 0) { const http = coreStart.http; + + // The original query to the list could have had an invalid param (ex. invalid page_size), + // so we check first if hosts actually do exist before pulling in data for the onboarding + // messages. + if (await doHostsExist(http)) { + return; + } + + dispatch({ + type: 'serverReturnedHostExistValue', + payload: false, + }); + try { const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificPackageConfigs( http, @@ -119,6 +134,8 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }); } } + + // Host Details if (action.type === 'userChangedUrl' && hasSelectedHost(state) === true) { dispatch({ type: 'serverCancelledPolicyItemsLoading', @@ -160,7 +177,6 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor type: 'serverFailedToReturnHostList', payload: error, }); - return; } } else { dispatch({ @@ -217,7 +233,7 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }; const getNonExistingPoliciesForHostsList = async ( - http: HttpSetup, + http: HttpStart, hosts: HostResultList['hosts'], currentNonExistingPolicies: HostState['nonExistingPolicies'] ): Promise => { @@ -274,3 +290,23 @@ const getNonExistingPoliciesForHostsList = async ( return nonExisting; }; + +const doHostsExist = async (http: HttpStart): Promise => { + try { + return ( + ( + await http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: 0 }, { page_size: 1 }], + }), + }) + ).hosts.length !== 0 + ); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`error while trying to check if hosts exist`); + // eslint-disable-next-line no-console + console.error(error); + } + return false; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts index 05af1ee062de6..355c2bb5c19fc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts @@ -4,8 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostInfo, HostResultList, HostStatus } from '../../../../../common/endpoint/types'; +import { HttpStart } from 'kibana/public'; +import { + GetHostPolicyResponse, + HostInfo, + HostPolicyResponse, + HostResultList, + HostStatus, +} from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { + INGEST_API_AGENT_CONFIGS, + INGEST_API_EPM_PACKAGES, + INGEST_API_PACKAGE_CONFIGS, +} from '../../policy/store/policy_list/services/ingest'; +import { + GetAgentConfigsResponse, + GetPackagesResponse, +} from '../../../../../../ingest_manager/common/types/rest_spec'; +import { GetPolicyListResponse } from '../../policy/types'; + +const generator = new EndpointDocGenerator('seed'); export const mockHostResultList: (options?: { total?: number; @@ -26,7 +45,6 @@ export const mockHostResultList: (options?: { const hosts = []; for (let index = 0; index < actualCountToReturn; index++) { - const generator = new EndpointDocGenerator('seed'); hosts.push({ metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, @@ -45,9 +63,133 @@ export const mockHostResultList: (options?: { * returns a mocked API response for retrieving a single host metadata */ export const mockHostDetailsApiResult = (): HostInfo => { - const generator = new EndpointDocGenerator('seed'); return { metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, }; }; + +/** + * Mock API handlers used by the Endpoint Host list. It also sets up a list of + * API handlers for Host details based on a list of Host results. + */ +const hostListApiPathHandlerMocks = ({ + hostsResults = mockHostResultList({ total: 3 }).hosts, + epmPackages = [generator.generateEpmPackage()], + endpointPackageConfigs = [], + policyResponse = generator.generatePolicyResponse(), +}: { + /** route handlers will be setup for each individual host in this array */ + hostsResults?: HostResultList['hosts']; + epmPackages?: GetPackagesResponse['response']; + endpointPackageConfigs?: GetPolicyListResponse['items']; + policyResponse?: HostPolicyResponse; +} = {}) => { + const apiHandlers = { + // endpoint package info + [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { + return { + response: epmPackages, + success: true, + }; + }, + + // host list + '/api/endpoint/metadata': (): HostResultList => { + return { + hosts: hostsResults, + request_page_size: 10, + request_page_index: 0, + total: hostsResults?.length || 0, + }; + }, + + // Do policies referenced in host list exist + // just returns 1 single agent config that includes all of the packageConfig IDs provided + [INGEST_API_AGENT_CONFIGS]: (): GetAgentConfigsResponse => { + const agentConfig = generator.generateAgentConfig(); + (agentConfig.package_configs as string[]).push( + ...endpointPackageConfigs.map((packageConfig) => packageConfig.id) + ); + return { + items: [agentConfig], + total: 10, + success: true, + perPage: 10, + page: 1, + }; + }, + + // Policy Response + '/api/endpoint/policy_response': (): GetHostPolicyResponse => { + return { policy_response: policyResponse }; + }, + + // List of Policies (package configs) for onboarding + [INGEST_API_PACKAGE_CONFIGS]: (): GetPolicyListResponse => { + return { + items: endpointPackageConfigs, + page: 1, + perPage: 10, + total: endpointPackageConfigs?.length, + success: true, + }; + }, + }; + + // Build a GET route handler for each host details based on the list of Hosts passed on input + if (hostsResults) { + hostsResults.forEach((host) => { + // @ts-ignore + apiHandlers[`/api/endpoint/metadata/${host.metadata.host.id}`] = () => host; + }); + } + + return apiHandlers; +}; + +/** + * Sets up mock impelementations in support of the Hosts list view + * + * @param mockedHttpService + * @param hostsResults + * @param pathHandlersOptions + */ +export const setHostListApiMockImplementation: ( + mockedHttpService: jest.Mocked, + apiResponses?: Parameters[0] +) => void = ( + mockedHttpService, + { hostsResults = mockHostResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {} +) => { + const apiHandlers = hostListApiPathHandlerMocks({ ...pathHandlersOptions, hostsResults }); + + mockedHttpService.post + .mockImplementation(async (...args) => { + throw new Error(`un-expected call to http.post: ${args}`); + }) + // First time called, return list of hosts + .mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }); + + // If the hosts list results is zero, then mock the second call to `/metadata` to return + // empty list - indicating there are no hosts currently present on the system + if (!hostsResults.length) { + mockedHttpService.post.mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }); + } + + // Setup handling of GET requests + mockedHttpService.get.mockImplementation(async (...args) => { + const [path] = args; + if (typeof path === 'string') { + if (apiHandlers[path]) { + return apiHandlers[path](); + } + } + + throw new Error(`MOCK: api request does not have a mocked handler: ${path}`); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 7f68baa4b85bd..e54f7df4d4f75 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -29,6 +29,7 @@ export const initialHostListState: Immutable = { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + hostsExist: true, }; /* eslint-disable-next-line complexity */ @@ -125,6 +126,11 @@ export const hostListReducer: ImmutableReducer = ( ...state, endpointPackageInfo: action.payload, }; + } else if (action.type === 'serverReturnedHostExistValue') { + return { + ...state, + hostsExist: action.payload, + }; } else if (action.type === 'userChangedUrl') { const newState: Immutable = { ...state, @@ -181,6 +187,7 @@ export const hostListReducer: ImmutableReducer = ( error: undefined, detailsError: undefined, policyResponseError: undefined, + hostsExist: true, }; } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 6e0823a920413..ca006f21c29ac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -203,3 +203,9 @@ export const policyResponseStatus: (state: Immutable) => string = cre export const nonExistingPolicies: ( state: Immutable ) => Immutable = (state) => state.nonExistingPolicies; + +/** + * Return boolean that indicates whether hosts exist + * @param state + */ +export const hostsExist: (state: Immutable) => boolean = (state) => state.hostsExist; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 582a59cfd7605..6c949e9700b9a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -52,6 +52,8 @@ export interface HostState { endpointPackageInfo?: GetPackagesResponse['response'][0]; /** tracks the list of policies IDs used in Host metadata that may no longer exist */ nonExistingPolicies: Record; + /** Tracks whether hosts exist and helps control if onboarding should be visible */ + hostsExist: boolean; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx index 4cdfaad69eb72..3a1dd180405e0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx @@ -140,20 +140,16 @@ export const PolicyResponse = memo( responseActions: Immutable; responseAttentionCount: Map; }) => { + const generateId = useMemo(() => htmlIdGenerator(), []); + return ( <> {Object.entries(responseConfig).map(([key, val]) => { const attentionCount = responseAttentionCount.get(key); return ( htmlIdGenerator()(), []) - } - key={ - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - useMemo(() => htmlIdGenerator()(), []) - } + id={generateId(`id_${key}`)} + key={generateId(`key_${key}`)} data-test-subj="hostDetailsPolicyResponseConfigAccordion" buttonContent={ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9d49c8705affe..3e00a5cc33db1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -8,18 +8,22 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; import { HostList } from './index'; -import { mockHostDetailsApiResult, mockHostResultList } from '../store/mock_host_result_list'; -import { mockPolicyResultList } from '../../policy/store/policy_list/mock_policy_result_list'; +import { + mockHostDetailsApiResult, + mockHostResultList, + setHostListApiMockImplementation, +} from '../store/mock_host_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { HostInfo, + HostPolicyResponse, HostPolicyResponseActionStatus, HostPolicyResponseAppliedAction, HostStatus, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; -import { AppAction } from '../../../../common/store/actions'; import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; +import { mockPolicyResultList } from '../../policy/store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -35,6 +39,9 @@ describe('when on the hosts page', () => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); + reactTestingLibrary.act(() => { + history.push('/hosts'); + }); }); it('should NOT display timeline', async () => { @@ -43,35 +50,29 @@ describe('when on the hosts page', () => { expect(timelineFlyout).toBeNull(); }); - it('should show the empty state when there are no hosts or polices', async () => { - const renderResult = render(); - // Initially, there are no hosts or policies, so we prompt to add policies first. - const table = await renderResult.findByTestId('emptyPolicyTable'); - expect(table).not.toBeNull(); - }); - - describe('when there are policies, but no hosts', () => { + describe('when there are no hosts or polices', () => { beforeEach(() => { - reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 0 }); - coreStart.http.get.mockReturnValue(Promise.resolve(hostListData)); - const hostAction: AppAction = { - type: 'serverReturnedHostList', - payload: hostListData, - }; - store.dispatch(hostAction); + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [], + }); + }); - jest.clearAllMocks(); + it('should show the empty state when there are no hosts or polices', async () => { + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); + // Initially, there are no hosts or policies, so we prompt to add policies first. + const table = await renderResult.findByTestId('emptyPolicyTable'); + expect(table).not.toBeNull(); + }); + }); - const policyListData = mockPolicyResultList({ total: 3 }); - coreStart.http.get.mockReturnValue(Promise.resolve(policyListData)); - const policyAction: AppAction = { - type: 'serverReturnedPoliciesForOnboarding', - payload: { - policyItems: policyListData.items, - }, - }; - store.dispatch(policyAction); + describe('when there are policies, but no hosts', () => { + beforeEach(async () => { + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [], + endpointPackageConfigs: mockPolicyResultList({ total: 3 }).items, }); }); afterEach(() => { @@ -80,18 +81,27 @@ describe('when on the hosts page', () => { it('should show the no hosts empty state', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable'); expect(emptyHostsTable).not.toBeNull(); }); it('should display the onboarding steps', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); expect(onboardingSteps).not.toBeNull(); }); it('should show policy selection', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect'); expect(onboardingPolicySelect).not.toBeNull(); }); @@ -112,39 +122,54 @@ describe('when on the hosts page', () => { let firstPolicyID: string; beforeEach(() => { reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 4 }); - firstPolicyID = hostListData.hosts[0].metadata.Endpoint.policy.applied.id; + const hostListData = mockHostResultList({ total: 4 }).hosts; + + firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; + [HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE, HostStatus.UNENROLLING].forEach( (status, index) => { - hostListData.hosts[index] = { - metadata: hostListData.hosts[index].metadata, + hostListData[index] = { + metadata: hostListData[index].metadata, host_status: status, }; } ); - hostListData.hosts.forEach((item, index) => { + hostListData.forEach((item, index) => { generatedPolicyStatuses[index] = item.metadata.Endpoint.policy.applied.status; }); - const action: AppAction = { - type: 'serverReturnedHostList', - payload: hostListData, - }; - store.dispatch(action); + + // Make sure that the first policy id in the host result is not set as non-existent + const ingestPackageConfigs = mockPolicyResultList({ total: 1 }).items; + ingestPackageConfigs[0].id = firstPolicyID; + + setHostListApiMockImplementation(coreStart.http, { + hostsResults: hostListData, + endpointPackageConfigs: ingestPackageConfigs, + }); }); }); it('should display rows in the table', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(5); }); it('should show total', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const total = await renderResult.findByTestId('hostListTableTotal'); expect(total.textContent).toEqual('4 Hosts'); }); it('should display correct status', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const hostStatuses = await renderResult.findAllByTestId('rowHostStatus'); expect(hostStatuses[0].textContent).toEqual('Error'); @@ -168,6 +193,9 @@ describe('when on the hosts page', () => { it('should display correct policy status', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const policyStatuses = await renderResult.findAllByTestId('rowPolicyStatus'); policyStatuses.forEach((status, index) => { @@ -184,6 +212,9 @@ describe('when on the hosts page', () => { it('should display policy name as a link', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink'))[0]; expect(firstPolicyName).not.toBeNull(); expect(firstPolicyName.getAttribute('href')).toContain(`policy/${firstPolicyID}`); @@ -192,17 +223,10 @@ describe('when on the hosts page', () => { describe('when the user clicks the first hostname in the table', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { - const hostDetailsApiResponse = mockHostDetailsApiResult(); - - coreStart.http.get.mockReturnValue(Promise.resolve(hostDetailsApiResponse)); - reactTestingLibrary.act(() => { - store.dispatch({ - type: 'serverReturnedHostDetails', - payload: hostDetailsApiResponse, - }); - }); - renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const hostNameLinks = await renderResult.findAllByTestId('hostnameCellLink'); if (hostNameLinks.length) { reactTestingLibrary.fireEvent.click(hostNameLinks[0]); @@ -221,9 +245,11 @@ describe('when on the hosts page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; let agentId: string; - const dispatchServerReturnedHostPolicyResponse = ( + let renderAndWaitForData: () => Promise>; + + const createPolicyResponse = ( overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success - ) => { + ): HostPolicyResponse => { const policyResponse = docGenerator.generatePolicyResponse(); const malwareResponseConfigurations = policyResponse.Endpoint.policy.applied.response.configurations.malware; @@ -269,21 +295,28 @@ describe('when on the hosts page', () => { policyResponse.Endpoint.policy.applied.actions.push(unknownAction); malwareResponseConfigurations.concerned_actions.push(unknownAction.name); + return policyResponse; + }; + + const dispatchServerReturnedHostPolicyResponse = ( + overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success + ) => { reactTestingLibrary.act(() => { store.dispatch({ type: 'serverReturnedHostPolicyResponse', payload: { - policy_response: policyResponse, + policy_response: createPolicyResponse(overallStatus), }, }); }); }; - beforeEach(() => { + beforeEach(async () => { const { host_status, metadata: { host, ...details }, } = mockHostDetailsApiResult(); + hostDetails = { host_status, metadata: { @@ -297,34 +330,37 @@ describe('when on the hosts page', () => { agentId = hostDetails.metadata.elastic.agent.id; - coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); + const policy = docGenerator.generatePolicyPackageConfig(); + policy.id = hostDetails.metadata.Endpoint.policy.applied.id; - reactTestingLibrary.act(() => { - history.push({ - ...history.location, - search: '?selected_host=1', - }); + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [hostDetails], + endpointPackageConfigs: [policy], }); + reactTestingLibrary.act(() => { - store.dispatch({ - type: 'serverReturnedHostDetails', - payload: hostDetails, - }); + history.push('/hosts?selected_host=1'); }); + + renderAndWaitForData = async () => { + const renderResult = render(); + await middlewareSpy.waitForAction('serverReturnedHostDetails'); + return renderResult; + }; }); afterEach(() => { jest.clearAllMocks(); }); - it('should show the flyout', () => { - const renderResult = render(); + it('should show the flyout', async () => { + const renderResult = await renderAndWaitForData(); return renderResult.findByTestId('hostDetailsFlyout').then((flyout) => { expect(flyout).not.toBeNull(); }); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); expect(policyDetailsLink).not.toBeNull(); expect(policyDetailsLink.getAttribute('href')).toEqual( @@ -333,10 +369,7 @@ describe('when on the hosts page', () => { }); it('should update the URL when policy name link is clicked', async () => { - const policyItem = mockPolicyResultList({ total: 1 }).items[0]; - coreStart.http.get.mockReturnValue(Promise.resolve({ item: policyItem })); - - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -349,7 +382,7 @@ describe('when on the hosts page', () => { }); it('should display policy status value as a link', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( @@ -358,7 +391,7 @@ describe('when on the hosts page', () => { }); it('should update the URL when policy status link is clicked', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -371,7 +404,7 @@ describe('when on the hosts page', () => { }); it('should display Success overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.success); }); @@ -385,7 +418,7 @@ describe('when on the hosts page', () => { }); it('should display Warning overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning); }); @@ -399,7 +432,7 @@ describe('when on the hosts page', () => { }); it('should display Failed overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure); }); @@ -413,7 +446,7 @@ describe('when on the hosts page', () => { }); it('should display Unknown overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse('' as HostPolicyResponseActionStatus); }); @@ -428,7 +461,7 @@ describe('when on the hosts page', () => { it('should include the link to reassignment in Ingest', async () => { coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); expect(linkToReassign).not.toBeNull(); expect(linkToReassign.textContent).toEqual('Reassign Configuration'); @@ -440,7 +473,7 @@ describe('when on the hosts page', () => { describe('when link to reassignment in Ingest is clicked', () => { beforeEach(async () => { coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(linkToReassign); @@ -461,13 +494,14 @@ describe('when on the hosts page', () => { } throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`); }); - renderResult = render(); + renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(policyStatusLink); }); await userChangedUrlChecker; + await middlewareSpy.waitForAction('serverReturnedHostPolicyResponse'); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 2692f7791b7c0..f91bba3e3125a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -89,6 +89,7 @@ export const HostList = () => { selectedPolicyId, policyItemsLoading, endpointPackageVersion, + hostsExist, } = useHostSelector(selector); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -262,6 +263,7 @@ export const HostList = () => { render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { const toRoutePath = getHostDetailsPath({ name: 'hostPolicyResponse', + ...queryParams, selected_host: item.metadata.host.id, }); const toRouteUrl = formatUrl(toRoutePath); @@ -329,7 +331,7 @@ export const HostList = () => { }, [formatUrl, queryParams, search]); const renderTableOrEmptyState = useMemo(() => { - if (!loading && listData && listData.length > 0) { + if (hostsExist) { return ( { error={listError?.message} pagination={paginationSetup} onChange={onTableChange} + loading={loading} /> ); } else if (!policyItemsLoading && policyItems && policyItems.length > 0) { @@ -356,19 +359,20 @@ export const HostList = () => { ); } }, [ - listData, + loading, + hostsExist, + policyItemsLoading, policyItems, + listData, columns, - loading, + listError?.message, paginationSetup, onTableChange, - listError?.message, - handleCreatePolicyClick, handleDeployEndpointsClick, - handleSelectableOnChange, selectedPolicyId, + handleSelectableOnChange, selectionOptions, - policyItemsLoading, + handleCreatePolicyClick, ]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index 8203aae244f24..6ee6a4232f7cf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -143,6 +143,7 @@ describe('policy list store concerns', () => { isLoading: false, isDeleting: false, deleteStatus: undefined, + endpointPackageInfo: undefined, pageIndex: 0, pageSize: 10, total: 0, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts index b4e1da4e43da3..6fe555113617d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts @@ -29,6 +29,7 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory GetPolicyListResponse = (options = {}) => { - const { - total = 1, - request_page_size: requestPageSize = 10, - request_page_index: requestPageIndex = 0, - } = options; - - // Skip any that are before the page we're on - const numberToSkip = requestPageSize * requestPageIndex; - - // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 - const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); - - const policies = []; - for (let index = 0; index < actualCountToReturn; index++) { - const generator = new EndpointDocGenerator('seed'); - policies.push(generator.generatePolicyPackageConfig()); - } - const mock: GetPolicyListResponse = { - items: policies, - total, - page: requestPageIndex, - perPage: requestPageSize, - success: true, - }; - return mock; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts index 7daa500ef1884..83cd8558306a0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -12,7 +12,7 @@ import { } from './ingest'; import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; -import { apiPathMockResponseProviders } from '../test_mock_utils'; +import { policyListApiPathHandlers } from '../test_mock_utils'; describe('ingest service', () => { let http: ReturnType; @@ -61,7 +61,9 @@ describe('ingest service', () => { describe('sendGetEndpointSecurityPackage()', () => { it('should query EPM with category=security', async () => { - http.get.mockReturnValue(apiPathMockResponseProviders[INGEST_API_EPM_PACKAGES]()); + http.get.mockReturnValue( + Promise.resolve(policyListApiPathHandlers()[INGEST_API_EPM_PACKAGES]()) + ); await sendGetEndpointSecurityPackage(http); expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/epm/packages', { query: { category: 'security' }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index c6e6146f4d5e4..266faf9eae32c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -20,7 +20,7 @@ import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_PACKAGE_CONFIGS = `${INGEST_API_ROOT}/package_configs`; -const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; +export const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index b5c67cc2c2014..3c9d5fde9b826 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -5,122 +5,84 @@ */ import { HttpStart } from 'kibana/public'; -import { INGEST_API_PACKAGE_CONFIGS, INGEST_API_EPM_PACKAGES } from './services/ingest'; +import { INGEST_API_EPM_PACKAGES, INGEST_API_PACKAGE_CONFIGS } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; -import { - KibanaAssetReference, - EsAssetReference, - GetPackagesResponse, - InstallationStatus, -} from '../../../../../../../ingest_manager/common'; +import { GetPackagesResponse } from '../../../../../../../ingest_manager/common'; const generator = new EndpointDocGenerator('policy-list'); -/** - * a list of API paths response mock providers - */ -export const apiPathMockResponseProviders = { - [INGEST_API_EPM_PACKAGES]: () => - Promise.resolve({ - response: [ - { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', - description: 'This is the Elastic Endpoint package.', - type: 'solution', - download: '/epr/endpoint/endpoint-0.5.0.tar.gz', - path: '/package/endpoint/0.5.0', - icons: [ - { - src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', - size: '16x16', - type: 'image/svg+xml', - }, - ], - status: 'installed' as InstallationStatus, - savedObject: { - type: 'epm-packages', - id: 'endpoint', - attributes: { - installed_kibana: [ - { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, - { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, - ] as KibanaAssetReference[], - installed_es: [ - { id: 'logs-endpoint.alerts', type: 'index_template' }, - { id: 'events-endpoint', type: 'index_template' }, - { id: 'logs-endpoint.events.file', type: 'index_template' }, - { id: 'logs-endpoint.events.library', type: 'index_template' }, - { id: 'metrics-endpoint.metadata', type: 'index_template' }, - { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, - { id: 'logs-endpoint.events.network', type: 'index_template' }, - { id: 'metrics-endpoint.policy', type: 'index_template' }, - { id: 'logs-endpoint.events.process', type: 'index_template' }, - { id: 'logs-endpoint.events.registry', type: 'index_template' }, - { id: 'logs-endpoint.events.security', type: 'index_template' }, - { id: 'metrics-endpoint.telemetry', type: 'index_template' }, - ] as EsAssetReference[], - es_index_patterns: { - alerts: 'logs-endpoint.alerts-*', - events: 'events-endpoint-*', - file: 'logs-endpoint.events.file-*', - library: 'logs-endpoint.events.library-*', - metadata: 'metrics-endpoint.metadata-*', - metadata_mirror: 'metrics-endpoint.metadata_mirror-*', - network: 'logs-endpoint.events.network-*', - policy: 'metrics-endpoint.policy-*', - process: 'logs-endpoint.events.process-*', - registry: 'logs-endpoint.events.registry-*', - security: 'logs-endpoint.events.security-*', - telemetry: 'metrics-endpoint.telemetry-*', - }, - name: 'endpoint', - version: '0.5.0', - internal: false, - removable: false, - }, - references: [], - updated_at: '2020-06-24T14:41:23.098Z', - version: 'Wzc0LDFd', - }, - }, - ], - success: true, - }), -}; - /** * It sets the mock implementation on the necessary http methods to support the policy list view * @param mockedHttpService - * @param responseItems + * @param totalPolicies */ export const setPolicyListApiMockImplementation = ( mockedHttpService: jest.Mocked, - responseItems: GetPolicyListResponse['items'] = [generator.generatePolicyPackageConfig()] + totalPolicies: number = 1 ): void => { - mockedHttpService.get.mockImplementation((...args) => { + const policyApiHandlers = policyListApiPathHandlers(totalPolicies); + + mockedHttpService.get.mockImplementation(async (...args) => { const [path] = args; if (typeof path === 'string') { - if (path === INGEST_API_PACKAGE_CONFIGS) { - return Promise.resolve({ - items: responseItems, - total: 10, - page: 1, - perPage: 10, - success: true, - }); - } - - if (apiPathMockResponseProviders[path]) { - return apiPathMockResponseProviders[path](); + if (policyApiHandlers[path]) { + return policyApiHandlers[path](); } } return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`)); }); }; + +/** + * Returns the response body for a call to get the list of Policies + * @param options + */ +export const mockPolicyResultList: (options?: { + total?: number; + request_page_size?: number; + request_page_index?: number; +}) => GetPolicyListResponse = (options = {}) => { + const { + total = 1, + request_page_size: requestPageSize = 10, + request_page_index: requestPageIndex = 0, + } = options; + + // Skip any that are before the page we're on + const numberToSkip = requestPageSize * requestPageIndex; + + // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 + const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); + + const policies = []; + for (let index = 0; index < actualCountToReturn; index++) { + policies.push(generator.generatePolicyPackageConfig()); + } + const mock: GetPolicyListResponse = { + items: policies, + total, + page: requestPageIndex, + perPage: requestPageSize, + success: true, + }; + return mock; +}; + +/** + * Returns an object comprised of the API path as the key along with a function that + * returns that API's result value + */ +export const policyListApiPathHandlers = (totalPolicies: number = 1) => { + return { + [INGEST_API_PACKAGE_CONFIGS]: () => { + return mockPolicyResultList({ total: totalPolicies }); + }, + [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { + return { + response: [generator.generateEpmPackage()], + success: true, + }; + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 03ab32dcb2b66..c81ffb0060c88 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -11,7 +11,7 @@ import { PolicyDetails } from './policy_details'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { getPolicyDetailPath, getHostListPath } from '../../../common/routing'; -import { apiPathMockResponseProviders } from '../store/policy_list/test_mock_utils'; +import { policyListApiPathHandlers } from '../store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -80,6 +80,8 @@ describe('Policy Details', () => { policyPackageConfig = generator.generatePolicyPackageConfig(); policyPackageConfig.id = '1'; + const policyListApiHandlers = policyListApiPathHandlers(); + http.get.mockImplementation((...args) => { const [path] = args; if (typeof path === 'string') { @@ -103,9 +105,9 @@ describe('Policy Details', () => { // Get package data // Used in tests that route back to the list - if (apiPathMockResponseProviders[path]) { + if (policyListApiHandlers[path]) { asyncActions = asyncActions.then(async () => sleep()); - return apiPathMockResponseProviders[path](); + return Promise.resolve(policyListApiHandlers[path]()); } } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx index 9ceade5d0264c..76077831c670b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx @@ -27,6 +27,7 @@ export const EventsCheckbox = React.memo(function ({ const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const selected = getter(policyDetailsConfig); const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); + const checkboxId = useMemo(() => htmlIdGenerator()(), []); const handleCheckboxChange = useCallback( (event: React.ChangeEvent) => { @@ -42,7 +43,7 @@ export const EventsCheckbox = React.memo(function ({ return ( htmlIdGenerator()(), [])} + id={checkboxId} label={name} checked={selected} onChange={handleCheckboxChange} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 84d4bf5355cd9..1698f5bc3fd0c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -41,6 +41,7 @@ const protection = 'malware'; const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: string }) => { const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const dispatch = useDispatch(); + const radioButtonId = useMemo(() => htmlIdGenerator()(), []); // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; @@ -66,7 +67,7 @@ const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: htmlIdGenerator()(), [])} + id={radioButtonId} checked={selected === id} onChange={handleRadioChange} disabled={selected === ProtectionModes.off} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index e35c97698f5cb..97eaceff91e9c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -5,12 +5,9 @@ */ import React from 'react'; -import * as reactTestingLibrary from '@testing-library/react'; - import { PolicyList } from './index'; -import { mockPolicyResultList } from '../store/policy_list/mock_policy_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; -import { AppAction } from '../../../../common/store/actions'; +import { setPolicyListApiMockImplementation } from '../store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -18,11 +15,12 @@ jest.mock('../../../../common/components/link_to'); describe.skip('when on the policies page', () => { let render: () => ReturnType; let history: AppContextTestRender['history']; - let store: AppContextTestRender['store']; + let coreStart: AppContextTestRender['coreStart']; + let middlewareSpy: AppContextTestRender['middlewareSpy']; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); - ({ history, store } = mockedContext); + ({ history, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); }); @@ -46,34 +44,30 @@ describe.skip('when on the policies page', () => { describe('when list data loads', () => { let firstPolicyID: string; - beforeEach(() => { - reactTestingLibrary.act(() => { - history.push('/policy'); - reactTestingLibrary.act(() => { - const policyListData = mockPolicyResultList({ total: 3 }); - firstPolicyID = policyListData.items[0].id; - const action: AppAction = { - type: 'serverReturnedPolicyListData', - payload: { - policyItems: policyListData.items, - total: policyListData.total, - pageSize: policyListData.perPage, - pageIndex: policyListData.page, - }, - }; - store.dispatch(action); - }); - }); + const renderList = async () => { + const renderResult = render(); + history.push('/policy'); + await Promise.all([ + middlewareSpy + .waitForAction('serverReturnedPolicyListData') + .then((action) => (firstPolicyID = action.payload.policyItems[0].id)), + // middlewareSpy.waitForAction('serverReturnedAgentConfigListData'), + ]); + return renderResult; + }; + + beforeEach(async () => { + setPolicyListApiMockImplementation(coreStart.http, 3); }); it('should display rows in the table', async () => { - const renderResult = render(); + const renderResult = await renderList(); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(4); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + const renderResult = await renderList(); const policyNameLink = (await renderResult.findAllByTestId('policyNameLink'))[0]; expect(policyNameLink).not.toBeNull(); expect(policyNameLink.getAttribute('href')).toContain(`policy/${firstPolicyID}`); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 246dbeb39886f..39b77d259add1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -377,6 +377,22 @@ export const PolicyList = React.memo(() => { [services.application, handleDeleteOnClick, formatUrl, search] ); + const bodyContent = useMemo(() => { + return policyItems && policyItems.length > 0 ? ( + + ) : ( + + ); + }, [policyItems, loading, columns, handleCreatePolicyClick, handleTableChange, paginationSetup]); + return ( <> {showDelete && ( @@ -449,32 +465,7 @@ export const PolicyList = React.memo(() => { )} - {useMemo(() => { - return ( - <> - {policyItems && policyItems.length > 0 ? ( - - ) : ( - - )} - - ); - }, [ - policyItems, - loading, - columns, - handleCreatePolicyClick, - handleTableChange, - paginationSetup, - ])} + {bodyContent} diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap index 30f7df940fa99..b72b9e5c73977 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap @@ -6,7 +6,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = ` Configure index patterns @@ -28,7 +28,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = ` beats , "defaultIndex": diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx index 62ffad515f424..b5595daa9cf47 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx @@ -13,7 +13,7 @@ import * as i18n from './translations'; export const IndexPatternsMissingPromptComponent = () => { const { docLinks } = useKibana().services; - const kibanaBasePath = `${useBasePath()}/app/kibana`; + const kibanaBasePath = `${useBasePath()}/app`; return ( { values={{ defaultIndex: ( @@ -61,7 +61,7 @@ export const IndexPatternsMissingPromptComponent = () => { } actions={ { + expect(simulator.panelElement().length).toBe(1); + expect(simulator.panelContentElement().length).toBe(1); + const testSubjectName = simulator + .panelContentElement() + .getDOMNode() + .getAttribute('data-test-subj'); + expect(testSubjectName).toMatch(/process-list/g); + }); + describe("when the second child node's first button has been clicked", () => { beforeEach(() => { // Click the first button under the second child element. diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 83d3930065da6..f378ab36bac94 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -220,7 +220,7 @@ PanelContent.displayName = 'PanelContent'; export const Panel = memo(function Event({ className }: { className?: string }) { return ( - + ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx index efb96cde431e5..8ca002ace26fe 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx @@ -187,7 +187,12 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ {showWarning && } - items={processTableView} columns={columns} sorting /> + + data-test-subj="resolver:panel:process-list" + items={processTableView} + columns={columns} + sorting + /> ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 6c1c88f511edb..75b6413bf08f9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -17,11 +17,25 @@ import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; -import { TimelineTabsStyle } from './types'; import { StatefulOpenTimeline } from '.'; + import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; + +import { useParams } from 'react-router-dom'; +import { TimelineType } from '../../../../common/types/timeline'; + jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/link_to'); + +jest.mock('./helpers', () => { + const originalModule = jest.requireActual('./helpers'); + return { + ...originalModule, + queryTimelineById: jest.fn(), + }; +}); + jest.mock('../../containers/all', () => { const originalModule = jest.requireActual('../../containers/all'); return { @@ -30,19 +44,21 @@ jest.mock('../../containers/all', () => { getAllTimeline: originalModule.getAllTimeline, }; }); -jest.mock('./use_timeline_types', () => { + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { - useTimelineTypes: jest.fn().mockReturnValue({ - timelineType: 'default', - timelineTabs:
, - timelineFilters:
, - }), + ...originalModule, + useParams: jest.fn(), + useHistory: jest.fn().mockReturnValue([]), }; }); describe('StatefulOpenTimeline', () => { const title = 'All Timelines / Open Timelines'; beforeEach(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), timelines: getAllTimeline( @@ -433,10 +449,7 @@ describe('StatefulOpenTimeline', () => { }); }); - /** - * enable this test when createtTemplateTimeline is ready - */ - test.skip('it renders the tabs', async () => { + test('it has the expected initial state for openTimeline - templateTimelineFilter', () => { const wrapper = mount( @@ -451,11 +464,27 @@ describe('StatefulOpenTimeline', () => { ); - await waitFor(() => { - expect( - wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists() - ).toEqual(true); - }); + expect(wrapper.find('[data-test-subj="open-timeline-subtabs"]').exists()).toEqual(true); + }); + + test('it has the expected initial state for openTimelineModalBody - templateTimelineFilter', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="open-timeline-modal-body-filters"]').exists()).toEqual( + true + ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 188b8979f613c..4c5db80a6c916 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -326,6 +326,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortField={sortField} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} + timelineStatus={timelineStatus} timelineFilter={timelineTabs} title={title} totalSearchResultsCount={totalCount} @@ -356,6 +357,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortField={sortField} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} + timelineStatus={timelineStatus} timelineFilter={timelineFilters} title={title} totalSearchResultsCount={totalCount} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 57a6431a06b90..9de3242c5e303 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -17,7 +17,7 @@ import { TimelinesTableProps } from './timelines_table'; import { mockTimelineResults } from '../../../common/mock/timeline_results'; import { OpenTimeline } from './open_timeline'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/lib/kibana'); @@ -50,6 +50,7 @@ describe('OpenTimeline', () => { sortField: DEFAULT_SORT_FIELD, title, timelineType: TimelineType.default, + timelineStatus: TimelineStatus.active, templateTimelineFilter: [
], totalSearchResultsCount: mockSearchResults.length, }); @@ -263,4 +264,136 @@ describe('OpenTimeline', () => { `Showing: ${mockResults.length} timelines with "How was your day?"` ); }); + + test("it should render bulk actions if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); + }); + + test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']); + }); + + test("it should render selected count if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true); + }); + + test("it should not render bulk actions if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(false); + }); + + test("it should not render a selectable timeline table if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate']); + }); + + test("it should not render selected count if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(false); + }); + + test("it should render bulk actions if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); + }); + + test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']); + }); + + test("it should render selected count if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index d839a1deddf21..c9495c46d4acf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiBasicTable } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { ImportDataModal } from '../../../common/components/import_data_modal'; import { UtilityBarGroup, @@ -55,6 +55,7 @@ export const OpenTimeline = React.memo( setImportDataModalToggle, sortField, timelineType = TimelineType.default, + timelineStatus, timelineFilter, templateTimelineFilter, totalSearchResultsCount, @@ -140,19 +141,23 @@ export const OpenTimeline = React.memo( }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); const actionTimelineToShow = useMemo(() => { - const timelineActions: ActionTimelineToShow[] = [ - 'createFrom', - 'duplicate', - 'export', - 'selectable', - ]; + const timelineActions: ActionTimelineToShow[] = ['createFrom', 'duplicate']; - if (onDeleteSelected != null && deleteTimelines != null) { + if (timelineStatus !== TimelineStatus.immutable) { + timelineActions.push('export'); + timelineActions.push('selectable'); + } + + if ( + onDeleteSelected != null && + deleteTimelines != null && + timelineStatus !== TimelineStatus.immutable + ) { timelineActions.push('delete'); } return timelineActions; - }, [onDeleteSelected, deleteTimelines]); + }, [onDeleteSelected, deleteTimelines, timelineStatus]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); @@ -206,20 +211,24 @@ export const OpenTimeline = React.memo( - - - {timelineType === TimelineType.template - ? i18n.SELECTED_TEMPLATES(selectedItems.length) - : i18n.SELECTED_TIMELINES(selectedItems.length)} - - - {i18n.BATCH_ACTIONS} - + {timelineStatus !== TimelineStatus.immutable && ( + <> + + {timelineType === TimelineType.template + ? i18n.SELECTED_TEMPLATES(selectedItems.length) + : i18n.SELECTED_TIMELINES(selectedItems.length)} + + + {i18n.BATCH_ACTIONS} + + + )} {i18n.REFRESH} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 12df17ceba666..9632b0e6ecea4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -17,7 +17,7 @@ import { TimelinesTableProps } from '../timelines_table'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineModalBody } from './open_timeline_modal_body'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; jest.mock('../../../../common/lib/kibana'); @@ -48,6 +48,7 @@ describe('OpenTimelineModal', () => { sortDirection: DEFAULT_SORT_DIRECTION, sortField: DEFAULT_SORT_FIELD, timelineType: TimelineType.default, + timelineStatus: TimelineStatus.active, templateTimelineFilter: [
], title, totalSearchResultsCount: mockSearchResults.length, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx index eddfdf6e01df2..52b7a4293e847 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import '../../../../common/mock/match_media'; + import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; import { TimelinesTableProps } from '.'; @@ -233,4 +234,32 @@ describe('#getActionsColumns', () => { expect(enableExportTimelineDownloader).toBeCalledWith(mockResults[0]); }); + + test('it should not render "export timeline" if it is not included', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['createFrom', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toEqual(false); + }); + + test('it should not render "delete timeline" if it is not included', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['createFrom', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toEqual(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx index 2d3672b15dd10..d2fba696d9d54 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx @@ -24,7 +24,7 @@ import { getActionsColumns } from './actions_columns'; import { getCommonColumns } from './common_columns'; import { getExtendedColumns } from './extended_columns'; import { getIconHeaderColumns } from './icon_header_columns'; -import { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline'; +import { TimelineTypeLiteralWithNull, TimelineStatus } from '../../../../../common/types/timeline'; // there are a number of type mismatches across this file const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -159,7 +159,8 @@ export const TimelinesTable = React.memo( }; const selection = { - selectable: (timelineResult: OpenTimelineResult) => timelineResult.savedObjectId != null, + selectable: (timelineResult: OpenTimelineResult) => + timelineResult.savedObjectId != null && timelineResult.status !== TimelineStatus.immutable, selectableMessage: (selectable: boolean) => !selectable ? i18n.MISSING_SAVED_OBJECT_ID : undefined, onSelectionChange, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index eb5a03baad88c..8950f814d6965 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -14,6 +14,7 @@ import { TimelineStatus, TemplateTimelineTypeLiteral, RowRendererId, + TimelineStatusLiteralWithNull, } from '../../../../common/types/timeline'; /** The users who added a timeline to favorites */ @@ -174,6 +175,8 @@ export interface OpenTimelineProps { sortField: string; /** this affects timeline's behaviour like editable / duplicatible */ timelineType: TimelineTypeLiteralWithNull; + /* active or immutable */ + timelineStatus: TimelineStatusLiteralWithNull; /** when timelineType === template, templatetimelineFilter is a JSX.Element */ templateTimelineFilter: JSX.Element[] | null; /** timeline / timeline template */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 7d54bb2209850..55afe845cdfb3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -7,26 +7,31 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/link_to'; import * as i18n from './translations'; import { TimelineTabsStyle, TimelineTab } from './types'; -export const useTimelineTypes = ({ - defaultTimelineCount, - templateTimelineCount, -}: { +export interface UseTimelineTypesArgs { defaultTimelineCount?: number | null; templateTimelineCount?: number | null; -}): { +} + +export interface UseTimelineTypesResult { timelineType: TimelineTypeLiteralWithNull; timelineTabs: JSX.Element; timelineFilters: JSX.Element[]; -} => { +} + +export const useTimelineTypes = ({ + defaultTimelineCount, + templateTimelineCount, +}: UseTimelineTypesArgs): UseTimelineTypesResult => { const history = useHistory(); const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); - const { tabName } = useParams<{ pageName: string; tabName: string }>(); + const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [timelineType, setTimelineTypes] = useState( tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null ); @@ -61,7 +66,7 @@ export const useTimelineTypes = ({ timelineTabsStyle === TimelineTabsStyle.filter ? defaultTimelineCount ?? undefined : undefined, - onClick: goToTimeline, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop, }, { id: TimelineType.template, @@ -76,7 +81,7 @@ export const useTimelineTypes = ({ timelineTabsStyle === TimelineTabsStyle.filter ? templateTimelineCount ?? undefined : undefined, - onClick: goToTemplateTimeline, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop, }, ], [ @@ -106,7 +111,7 @@ export const useTimelineTypes = ({ const timelineTabs = useMemo(() => { return ( <> - + {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( { return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( { expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); }); }); + + describe('getPinOnClick', () => { + const eventId = 'abcd'; + + test('it invokes `onPinEvent` with the expected eventId when the event is NOT pinned, and allowUnpinning is true', () => { + const isEventPinned = false; // the event is NOT pinned + const allowUnpinning = true; + const onPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent: jest.fn(), + isEventPinned, + }); + + expect(onPinEvent).toBeCalledWith(eventId); + }); + + test('it does NOT invoke `onPinEvent` when the event is NOT pinned, and allowUnpinning is false', () => { + const isEventPinned = false; // the event is NOT pinned + const allowUnpinning = false; + const onPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent: jest.fn(), + isEventPinned, + }); + + expect(onPinEvent).not.toBeCalled(); + }); + + test('it invokes `onUnPinEvent` with the expected eventId when the event is pinned, and allowUnpinning is true', () => { + const isEventPinned = true; // the event is pinned + const allowUnpinning = true; + const onUnPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent: jest.fn(), + onUnPinEvent, + isEventPinned, + }); + + expect(onUnPinEvent).toBeCalledWith(eventId); + }); + + test('it does NOT invoke `onUnPinEvent` when the event is pinned, and allowUnpinning is false', () => { + const isEventPinned = true; // the event is pinned + const allowUnpinning = false; + const onUnPinEvent = jest.fn(); + + getPinOnClick({ + allowUnpinning, + eventId, + onPinEvent: jest.fn(), + onUnPinEvent, + isEventPinned, + }); + + expect(onUnPinEvent).not.toBeCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts index 6a5e25632c29b..73b5a58ef7b65 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty, noop } from 'lodash/fp'; + +import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; @@ -65,11 +66,16 @@ export const getPinOnClick = ({ onPinEvent, onUnPinEvent, isEventPinned, -}: GetPinOnClickParams): (() => void) => { +}: GetPinOnClickParams) => { if (!allowUnpinning) { - return noop; + return; + } + + if (isEventPinned) { + onUnPinEvent(eventId); + } else { + onPinEvent(eventId); } - return isEventPinned ? () => onUnPinEvent(eventId) : () => onPinEvent(eventId); }; /** diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx index 657976e2f4787..2ca27ded86c9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.test.tsx @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getPinIcon } from './'; +import { mount } from 'enzyme'; +import React from 'react'; + +import { TimelineType } from '../../../../../common/types/timeline'; + +import { getPinIcon, Pin } from './'; + +interface ButtonIcon { + isDisabled: boolean; +} describe('pin', () => { describe('getPinRotation', () => { @@ -16,4 +25,62 @@ describe('pin', () => { expect(getPinIcon(false)).toEqual('pin'); }); }); + + describe('disabled button behavior', () => { + test('the button is enabled when allowUnpinning is true, and timelineType is NOT `template` (the default)', () => { + const allowUnpinning = true; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(false); + }); + + test('the button is disabled when allowUnpinning is false, and timelineType is NOT `template` (the default)', () => { + const allowUnpinning = false; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + + test('the button is disabled when allowUnpinning is true, and timelineType is `template`', () => { + const allowUnpinning = true; + const timelineType = TimelineType.template; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + + test('the button is disabled when allowUnpinning is false, and timelineType is `template`', () => { + const allowUnpinning = false; + const timelineType = TimelineType.template; + const wrapper = mount( + + ); + + expect( + (wrapper.find('[data-test-subj="pin"]').first().props() as ButtonIcon).isDisabled + ).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx index 30fe8ae0ca1f6..27780c7754d00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx @@ -34,7 +34,7 @@ export const Pin = React.memo( iconSize={iconSize} iconType={getPinIcon(pinned)} onClick={onClick} - isDisabled={isTemplate} + isDisabled={isTemplate || !allowUnpinning} /> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index b59f9e90f8e74..c22acf6ba7cc1 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -33,7 +33,7 @@ const TimelinesContainer = styled.div` export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; export const TimelinesPageComponent: React.FC = () => { - const { tabName } = useParams(); + const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [importDataModalToggle, setImportDataModalToggle] = useState(false); const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md new file mode 100644 index 0000000000000..1c0692db344c4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md @@ -0,0 +1,216 @@ +# Resolver Backend + +This readme will describe the backend implementation for resolver. + +## Ancestry Array + +The ancestry array is an array of entity_ids. This array is included with each event sent by the elastic endpoint +and defines the ancestors of a particular process. The array is formatted such that [0] of the array contains the direct +parent of the process. [1] of the array contains the grandparent of the process. For example if Process A spawned process +B which spawned process C. Process C's array would be [B,A]. + +The presence of the ancestry array makes querying ancestors and children for a process more efficient. + +## Ancestry Array Limit + +The ancestry array is currently limited to 20 values. The exact limit should not be relied on though. + +## Ancestors + +To query for ancestors of a process leveraging the ancestry array, we first retrieve the lifecycle events for the event_id +passed in. Once we have the origin node we can check to see if the document has the `process.Ext.ancestry` array. If +it does we can perform a search for the values in the array. This will retrieve all the ancestors for the process of interest +up to the limit of the ancestry array. Since the array is capped at 20, if the request is asking for more than 20 +ancestors we will have to examine the most distant ancestor that has been retrieved and use its ancestry array to retrieve +the next set of results to fulfill the request. + +### Pagination + +After the backend gathers the results for an ancestry query, it will set a pagination cursor depending on the results from ES. + +If the number of ancestors we have gathered is equal to the size in the request we don't know if ES has more results or not. So we will set `nextAncestor` to the entity_id of the most distant ancestor retrieved. + +If the request asked for 10 and we only found 8 from ES, we know for sure that there aren't anymore results. In this case we will set `nextAncestor` to `null`. + +### Code + +The code for handling the ancestor logic is in [here](../utils/ancestry_query_handler.ts) + +### Ancestors Multiple Queries Example + +![alt text](./resolver_tree_ancestry.png 'Retrieve ancestors') + +For this example let's assume that the _ancestry array limit_ is 2. The process of interest is A (the entity_id of a node is the character in the circle). Process A has an ancestry array of `[3,2]`, its parent has an ancestry array of `[2,1]` etc. Here is the execution of a request for 3 ancestors for entity_id A. + +**Request:** `GET /resolver/A/ancestry?ancestors=3` + +1. Retrieve lifecycle events for entity_id `A` +2. Retrieve `A`'s start event's ancestry array + 1. In the event that the node of interest does not have an ancestry array, the entity id of it's parent will be used, essentially an ancestry array of length 1, [3] in the example here +3. `A`'s ancestry array is `[3,2]`, query for the lifecycle events for processes with `entity_id` 3 or 2 +4. Check to see if we have retrieved enough ancestors to fulfill the request (we have not, we only received 2 nodes of the 3 that were requested) +5. We haven't so use the most distant ancestor in our result set (process 2) +6. Use process 2's ancestry array to query for the next set of results to fulfill the request +7. Process 2's ancestry array is `[1]` so repeat the process in steps 3-4 and retrieve process with entity_id 1. This fulfills the request so we can return the results for the lifecycle events of A, 3, 2, and 1. + +If process 2 had an ancestry array of `[1,0]` we know that we only need 1 more process to fulfill the request so we can truncate the array to `[1]` instead of searching for all the entries in the array. + +More generically: In the event where our request stops at the x (non-final) position in an ancestry array, we won't search all items in the array, just those up to the x position. The next-cursor will be set to the last ancestor received since there might be more data. + +The `nextAncestor` cursor will be set to `1` in this scenario because we retrieved all 3 ancestors from ES but we don't know if ES has anymore. + +## Descendants + +We can also leverage the ancestry array to query for the descendants of a process. The basic query for the descendants of a process is: _find all processes where their ancestry array contains a particular entity_id_. The results of this query will be sorted in ascending order by the timestamp field. I will try to outline a couple different scenarios for retrieving descendants using the ancestry array below. + +### Start events vs all lifecycle events + +There are two parts to querying for descendant process nodes. When a request comes in for 7 process nodes we need to communicate to ES that we want all of the lifecycle nodes for 7 processes. We could use a query that retrieves all lifecycle events (start, end, etc) but the issue with this is that we need to indicate a `size` in our ES query. If we set the `size` to 7, we will only get 7 lifecycle events. These events could be start, end, or already_running events. It doesn't guarantee that we get all of the lifecycle events for 7 process nodes. + +Instead we can first query for 7 start events, which guarantees that we will have 7 unique process descendants and then we can gather all those entity_ids and do another query for all the lifecycle events for those 7 processes. The downside here is that you have to do two queries to retrieve all the lifecycle events. Optimizations can be made for the first query for the start events by reducing the `_source` that ES returns to only include the `entity_id` and `ancestry`. This will reduce the amount of data that ES has to send back and speed up the query. + +### Scenario Background + +In the scenarios below let's assume the _ancestry array limit_ is 2. The times next to the nodes are the time the node was spawned. The value in red indicates that the process terminated at the time in red. + +Let's also ignore the fact that retrieving the lifecycle events for a descendant actually takes two queries. Let's assume that it's taken care of, and when we say "query for lifecycle events" we get all the lifecycle events back for the descendants using the algorithm described in the [previous section](#start-events-vs-all-lifecycle-events) + +### Simple Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> E -> F -> G -> H. + +![alt text](./resolver_tree_children_simple.png 'Descendants Simple Scenario') + +**Request:** `GET /resolver/A/children?children=6` + +For this scenario we will retrieve all the lifecycle events for 6 descendants of the process with entity_id `A`. As shown in the diagram above ES has 6 descendants for A so the response to this request will be: `[B, C, E, F, G, H]` because the results are sorted in ascending ordering based on the `timestamp` field which is when the process was started. + +### Looping Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +**Request:** `GET /resolver/A/children?children=9` + +In this scenario the request is for more descendants than can be retrieved using a single querying with the entity_id `A`. This is because the ancestry array for the descendants in the red section do not have `A` in their ancestry array. So when we query for all process nodes that have `A` in their ancestry array we won't receive D, J, or K. + +Like in the previous scenario, for the first query we will receive `[B, C, E, F, G, H]`. What we want to do next is use a subset of that response that will get us 3 more descendants to fulfill the request for a total of 9. + +We _could_ use `B` and `G` to do this (mostly `B`) but the problem is that when we query for descendants that have `B` or `G` in their ancestry array we will get back some duplicates that we have already received before. For example if we use `B` and `G` we'd get `[C, D, E, F, J, K, H]` but this isn't efficient because we have already received `[E, F, G, H]` from the previous query. + +What we want to do is use the most distant descendants from `A` to make the next query to retrieve the last 3 process nodes to fulfill the request. Those would be `[C, E, F, H]`. So our next query will be: _find all process nodes where their ancestry array contains C or E or F or H_. This query can be limited to a size of 3 so that we will only receive `[D, J, K]`. + +We have now received all the nodes for the request and we can return the results as `[B, C, E, F, G, H, D, J, K]`. + +### Important Caveats + +#### Ordering + +In the previous example the final results are not sorted based on timestamp in ascending order. This is because we had to perform multiple queries to retrieve all the results. The backend will not return the results in sorted order. + +#### Tie breaks on timestamp + +In the previous example we saw that J and K had the same timestamp of `12:13 pm`. The reason they were returned in the order `[J, K]` is because the `event.id` field is used to break ties like this. The `event.id` field is unique for a particular event and an increasing value per ECS's guidelines. Therefore J comes before K because it has an `event.id` of 1 vs 2. + +#### Finding the most distant descendants + +In the previous scenario we saw that we needed to use the most distant descendants from a particular node. To determine if a node is a most distant descendant we can use the ancestry array. Nodes C, E, F, and H all have `A` as their last entry in the ancestry array. This indicates that they are a distant descendant that should be used in the next query. There's one problem with this approach. In a mostly impossible scenario where the node of interest (A) does not have any ancestors, its direct children will also have `A` as the last entry in their ancestry array. + +This edge case will likely never be encountered but we'll try to solve it anyway. To get around this as we iterate over the results from our first query (`[B, C, E, F, G, H]`) we can bucket the ones that have `A` as the last entry in their ancestry array. We bucket the results based on the length of their ancestry array (basically a `Map>`). So after bucketing our results will look like: + +```javascript +{ + 1: [B, G] + 2: [C, E, F, H] +} +``` + +While we are iterating we also keep track of the largest ancestry array that we have seen. In our scenario that will be a size of 2. Then to determine the distant descendants we simply get the nodes that had the largest ancestry array length. In this scenario that'd be `[C, E, F, H]`. + +### Handling Pagination + +#### Pagination Cursor Values + +There are 3 possible states for the pagination cursor for a child node and 2 possible states for the pagination cursor for the node of interest (the node that we are using in the API request). + +Potential cursors for the node of interest: +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. +**null:** indicates that no more results can be received using this process's entity_id + +Potential cursors for descendants of the node of interest (these apply to the results of a request): +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. This cursor should be used in conjunction with using this process's entity_id for a query. +**undefined:** the node may contain additional children, but we are not aware. To find out, perform additional queries on the node of interest that original returned these results or move down the tree to a descendant of this node to query for more descendants. +**null:** We have found all possible direct children for this node. There may be more descendants but not direct children for this node. + +#### Pagination Examples + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K. + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Handling pagination for the children API in a little tricky. Let's consider this scenario: + +**Request:** `GET /resolver/A/children?children=3` + +Let's use the diagram above to show the relationship between processes and the data in ES. The response for the request for 3 children is `[B, C, G]`. More process nodes exist in ES so it would be helpful to indicate in the response that there is more data and a way to skip `[B, C, G]` to get the next set of data. A cursor can be set on the response for `A` to point to the last process node in the response which can be sent in another request to retrieve the next set of data. This cursor will contain information from `G` because it has the latest timestamp (in ascending order). + +If another request was made using the returned cursor like the following: + +**Request:** `GET /resolver/A/children?children=5&after=` + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +For this request we will do a query for _find all process nodes where their ancestry array has entity_id `A` and use the cursor to skip old results_. The response for this request is `[F, H, E, K]`. The request actually asked for 5 nodes but there was only 4 in ES so only 4 were returned. + +The odd thing about this response is that it did not receive D and J. The problem is that the backend does not have any concept of C in this second request because it was received in the previous one. It will be skipped based on the pagination cursor returned previously. + +This example highlights a scenario where it is not easy for the backend to go back and continue to get the descendants for C because of the limitation of the ancestry array. + +#### Pagination cursor for descendant nodes + +Let's go back to the first request where we got `[B, C, G]`. How could we go about getting the rest of the children for `B`? We have two ways of solving this. First we could determine what the last descendant we had received of `B` and use that as the cursor when returning all the results for this request. That is actually kind of difficult. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Let's imagine for a moment that the _ancestry array limit_ is 3 instead of 2. Taking our previous request we would instead get `[B, C, D]` because D started before G. In this case the last descendant for `B` is actually `D` and not `C`. This gets complicated because we'd have to keep track of which descendant was the last (time wise) one for each intermediate process node. In this example we'd need to find the last descendant for both `B` and `C`. We'd have to track the descendants for each process node and build a map to quickly be able to retrieve the last descendant. + +Instead of doing that we could also continue to get the immediate children of `B` by doing another request for `A` like was shown in previous example when using the after cursor. This would guarantee that all children (first level descendants of a node) had been retrieved. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +To get to the response shown in the diagram above (the blue nodes is the response) a request was made for 3 nodes which returned `[B, C, D]` and then another request was made using the returned cursor for `A` to get an additional 4 nodes `[F, H, E, K]`. Let's assume that the request looked like: + +**Request:** `GET /resolver/A/children?children=5&after=` + +So 5 nodes were actually asked for. After `[F, H, E]` are returned during the first query to ES, we will use the most distant children (also `[F, H, E]`) and make another request for any nodes that have F or H or E in their ancestry array. Only a single node satisfies that query which returns `K`. Therefore we can know with certainty that E, F, and H have no more children because ES only returned K instead of K and one more node (since we requested a total of 5). With this knowledge we can mark A, E, F, H's pagination cursors in a way to communicate that they have no more descendants. + +The way the backend communicates this is by marking the cursor as null. + +If the request was actually for only 4 children like: + +**Request:** `GET /resolver/A/children?children=4&after=` + +Then we wouldn't know for sure whether ES had more results than K. But what we can know is that `A` does not have any more descendants that we can retrieve in a single query using its ancestry array. We have received all nodes where `A` is in their ancestry array when we made the second query for nodes E, F, and H and received `K`. Therefore at the moment when we received `[F, H, E]` we can mark `A`'s cursor as null. + +When we make the next query for any nodes that have F or H or E in their ancestry array and get back K, this satisfies our size of only needing one more node (4 total). At this point we don't know for sure if E, F, or H have more descendants. Since we don't know we will mark E, F, and H's cursors to point to K. + +#### Undefined Pagination + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. (Not the same as above). + +For this scenario let's assume ES has the data in the diagram below. Let's say the request looks like: + +**Request:** `GET /resolver/A/children?children=6` + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +The result for this request will be `[B, C, E, F, G, H]`. Since the request was looking for 6 nodes and we got that amount the cursor for `A` will be set to the last one: `H`. The cursors for the intermediate nodes `B` and `G` will be undefined. This is because we don't know if `B` or `G` have more children but `A` can be used to determine that. We also don't know if C, E, F, or H have more descendants so their cursor will also be marked as undefined. If we wanted to know if C had more descendants we can simply issue a new request like `GET /resolver/C/children` to get its descendants and we won't receive and duplicates because we never received D, J or K. + +If we want to know if `B` has more children we can issue another request using the cursor set for `A`. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png new file mode 100644 index 0000000000000..a2636c7cd38cb Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png new file mode 100644 index 0000000000000..9914031a3becb Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png new file mode 100644 index 0000000000000..42b691616a679 Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png new file mode 100644 index 0000000000000..5a6accb279760 Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png new file mode 100644 index 0000000000000..f05963c0f0b67 Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts index 9bf16dac791d7..7dd47658bc4c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -74,8 +74,28 @@ export class AncestryQueryHandler implements QueryHandler { // bucket the start and end events together for a single node const ancestryNodes = this.toMapOfNodes(results); - // the order of this array is going to be weird, it will look like this - // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent] + /** + * This array (this.ancestry.ancestors) is the accumulated ancestors of the node of interest. This array is different + * from the ancestry array of a specific document. The order of this array is going to be weird, it will look like this + * [most distant ancestor...closer ancestor, next recursive call most distant ancestor...closer ancestor] + * + * Here is an example of why this happens + * Consider the following tree: + * A -> B -> C -> D -> E -> Origin + * Where A was spawn before B, which was before C, etc + * + * Let's assume the ancestry array limit is 2 so Origin's array would be: [E, D] + * E's ancestry array would be: [D, C] etc + * + * If a request comes in to retrieve all the ancestors in this tree, the accumulate results will be: + * [D, E, B, C, A] + * + * The first iteration would retrieve D and E in that order because they are sorted in ascending order by timestamp. + * The next iteration would get the ancestors of D (since that's the most distant ancestor from Origin) which are + * [B, C] + * The next iteration would get the ancestors of B which is A + * Hence: [D, E, B, C, A] + */ this.ancestry.ancestors.push(...ancestryNodes.values()); this.ancestry.nextAncestor = parentEntityId(results[0]) || null; this.levels = this.levels - ancestryNodes.size; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts index 9e47f4eb94485..3f941851a4143 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts @@ -30,38 +30,9 @@ export interface Options { } /** - * This class aids in constructing a tree of process events. It works in the following way: + * This class aids in constructing a tree of process events. * - * 1. We construct a tree structure starting with the root node for the event we're requesting. - * 2. We leverage the ability to pass hashes and arrays by reference to construct a fast cache of - * process identifiers that updates the tree structure as we push values into the cache. - * - * When we query a single level of results for child process events we have a flattened, sorted result - * list that we need to add into a constructed tree. We also need to signal in an API response whether - * or not there are more child processes events that we have not yet retrieved, and, if so, for what parent - * process. So, at the end of our tree construction we have a relational layout of the events with no - * pagination information for the given parent nodes. In order to actually construct both the tree and - * insert the pagination information we basically do the following: - * - * 1. Using a terms aggregation query, we return an approximate roll-up of the number of child process - * "creation" events, this gives us an estimation of the number of associated children per parent - * 2. We feed these child process creation event "unique identifiers" (basically a process.entity_id) - * into a second query to get the current state of the process via its "lifecycle" events. - * 3. We construct the tree above with the "lifecycle" events. - * 4. Using the terms query results, we mark each non-leaf node with the number of expected children, if our - * tree has less children than expected, we create a pagination cursor to indicate "we have a truncated set - * of values". - * 5. We mark each leaf node (the last level of the tree we're constructing) with a "null" for the expected - * number of children to indicate "we have not yet attempted to get any children". - * - * Following this scheme, we use exactly 2 queries per level of children that we return--one for the pagination - * and one for the lifecycle events of the processes. The downside to this is that we need to dynamically expand - * the number of documents we can retrieve per level due to the exponential fanout of child processes, - * what this means is that noisy neighbors for a given level may hide other child process events that occur later - * temporally in the same level--so, while a heavily forking process might get shown, maybe the actually malicious - * event doesn't show up in the tree at the beginning. - * - * This Tree's root/origin could be in the middle of the tree. The origin corresponds to the id passed in when this + * This Tree's root/origin will likely be in the middle of the tree. The origin corresponds to the id passed in when this * Tree object is constructed. The tree can have ancestors and children coming from the origin. */ export class Tree { 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 592ffb0eae62a..34e18c5fe85fc 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 @@ -8,6 +8,7 @@ import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks import { Logger } from 'src/core/server'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; +import { ExceptionListClient } from '../../../../../../lists/server'; import { listMock } from '../../../../../../lists/server/mocks'; import LRU from 'lru-cache'; import { getArtifactClientMock } from '../artifact_client.mock'; @@ -23,22 +24,29 @@ import { export enum ManifestManagerMockType { InitialSystemState, + ListClientPromiseRejection, NormalFlow, } export const getManifestManagerMock = (opts?: { mockType?: ManifestManagerMockType; cache?: LRU; + exceptionListClient?: ExceptionListClient; packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; }): ManifestManager => { let cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); - if (opts?.cache !== undefined) { + if (opts?.cache != null) { cache = opts.cache; } + let exceptionListClient = listMock.getExceptionListClient(); + if (opts?.exceptionListClient != null) { + exceptionListClient = opts.exceptionListClient; + } + let packageConfigService = createPackageConfigServiceMock(); - if (opts?.packageConfigService !== undefined) { + if (opts?.packageConfigService != null) { packageConfigService = opts.packageConfigService; } packageConfigService.list = jest.fn().mockResolvedValue({ @@ -51,7 +59,7 @@ export const getManifestManagerMock = (opts?: { }); let savedObjectsClient = savedObjectsClientMock.create(); - if (opts?.savedObjectsClient !== undefined) { + if (opts?.savedObjectsClient != null) { savedObjectsClient = opts.savedObjectsClient; } @@ -61,6 +69,11 @@ export const getManifestManagerMock = (opts?: { switch (mockType) { case ManifestManagerMockType.InitialSystemState: return getEmptyMockArtifacts(); + case ManifestManagerMockType.ListClientPromiseRejection: + exceptionListClient.findExceptionListItem = jest + .fn() + .mockRejectedValue(new Error('unexpected thing happened')); + return super.buildExceptionListArtifacts('v1'); case ManifestManagerMockType.NormalFlow: return getMockArtifactsWithDiff(); } @@ -85,7 +98,7 @@ export const getManifestManagerMock = (opts?: { artifactClient: getArtifactClientMock(savedObjectsClient), cache, packageConfigService, - exceptionListClient: listMock.getExceptionListClient(), + exceptionListClient, logger: loggingSystemMock.create().get() as jest.Mocked, savedObjectsClient, }); 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 d99d6a959d7aa..8e0d55104fb7c 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 @@ -9,7 +9,7 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; import { ArtifactConstants, ManifestConstants, isCompleteArtifact } from '../../../lib/artifacts'; -import { getManifestManagerMock } from './manifest_manager.mock'; +import { getManifestManagerMock, ManifestManagerMockType } from './manifest_manager.mock'; import LRU from 'lru-cache'; describe('manifest_manager', () => { @@ -204,5 +204,14 @@ describe('manifest_manager', () => { oldArtifactId ); }); + + test('ManifestManager handles promise rejections when building artifacts', async () => { + // This test won't fail on an unhandled promise rejection, but it will cause + // an UnhandledPromiseRejectionWarning to be printed. + const manifestManager = getManifestManagerMock({ + mockType: ManifestManagerMockType.ListClientPromiseRejection, + }); + await expect(manifestManager.buildNewManifest()).rejects.toThrow(); + }); }); }); 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 217fd6de2ba68..7d700cdffc60d 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 @@ -82,18 +82,17 @@ export class ManifestManager { protected async buildExceptionListArtifacts( artifactSchemaVersion?: string ): Promise { - return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce< - Promise - >(async (acc, os) => { + const artifacts: InternalArtifactCompleteSchema[] = []; + for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) { const exceptionList = await getFullEndpointExceptionList( this.exceptionListClient, os, artifactSchemaVersion ?? 'v1' ); - const artifacts = await acc; const artifact = await buildArtifact(exceptionList, os, artifactSchemaVersion ?? 'v1'); - return Promise.resolve([...artifacts, artifact]); - }, Promise.resolve([])); + artifacts.push(artifact); + } + return artifacts; } /** diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts new file mode 100644 index 0000000000000..1735c6473bb3a --- /dev/null +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { filterIndexes } from './resolvers'; + +describe('resolvers', () => { + test('it should filter single index that has an empty string', () => { + const emptyArray = filterIndexes(['']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter single index that has blanks within it', () => { + const emptyArray = filterIndexes([' ']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter indexes that has an empty string and a valid index', () => { + const emptyArray = filterIndexes(['', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter indexes that have blanks within them and a valid index', () => { + const emptyArray = filterIndexes([' ', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter single index that has _all within it', () => { + const emptyArray = filterIndexes(['_all']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter single index that has _all within it surrounded by spaces', () => { + const emptyArray = filterIndexes([' _all ']); + expect(emptyArray).toEqual([]); + }); + + test('it should filter indexes that _all within them and a valid index', () => { + const emptyArray = filterIndexes(['_all', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); + + test('it should filter indexes that _all surrounded with spaces within them and a valid index', () => { + const emptyArray = filterIndexes([' _all ', 'valid-index']); + expect(emptyArray).toEqual(['valid-index']); + }); +}); diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts index 8d55e645d6791..84320b1699531 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts @@ -32,27 +32,34 @@ export const createSourceStatusResolvers = (libs: { }; } => ({ SourceStatus: { - async indicesExist(source, args, { req }) { - if ( - args.defaultIndex.length === 1 && - (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') - ) { + async indicesExist(_, args, { req }) { + const indexes = filterIndexes(args.defaultIndex); + if (indexes.length !== 0) { + return libs.sourceStatus.hasIndices(req, indexes); + } else { return false; } - return libs.sourceStatus.hasIndices(req, args.defaultIndex); }, - async indexFields(source, args, { req }) { - if ( - args.defaultIndex.length === 1 && - (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') - ) { + async indexFields(_, args, { req }) { + const indexes = filterIndexes(args.defaultIndex); + if (indexes.length !== 0) { + return libs.fields.getFields(req, indexes); + } else { return []; } - return libs.fields.getFields(req, args.defaultIndex); }, }, }); +/** + * Given a set of indexes this will remove anything that is: + * - blank or empty strings are removed as not valid indexes + * - _all is removed as that is not a valid index + * @param indexes Indexes with invalid values removed + */ +export const filterIndexes = (indexes: string[]): string[] => + indexes.filter((index) => index.trim() !== '' && index.trim() !== '_all'); + export const toIFieldSubTypeNonNullableScalar = new GraphQLScalarType({ name: 'IFieldSubType', description: 'Represents value in index pattern field item', diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts index 944fc588afc8a..bb0a4b9e2ba9b 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts @@ -17,26 +17,21 @@ import { import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { FieldsAdapter, IndexFieldDescriptor } from './types'; -type IndexesAliasIndices = Record; - export class ElasticsearchIndexFieldAdapter implements FieldsAdapter { constructor(private readonly framework: FrameworkAdapter) {} public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise { const indexPatternsService = this.framework.getIndexPatternsService(request); - const indexesAliasIndices: IndexesAliasIndices = indices.reduce( - (accumulator: IndexesAliasIndices, indice: string) => { - const key = getIndexAlias(indices, indice); + const indexesAliasIndices = indices.reduce>((accumulator, indice) => { + const key = getIndexAlias(indices, indice); - if (get(key, accumulator)) { - accumulator[key] = [...accumulator[key], indice]; - } else { - accumulator[key] = [indice]; - } - return accumulator; - }, - {} as IndexesAliasIndices - ); + if (get(key, accumulator)) { + accumulator[key] = [...accumulator[key], indice]; + } else { + accumulator[key] = [indice]; + } + return accumulator; + }, {}); const responsesIndexFields: IndexFieldDescriptor[][] = await Promise.all( Object.values(indexesAliasIndices).map((indicesByGroup) => indexPatternsService.getFieldsForWildcard({ diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 9a7ad6fc2db74..6fadf956ccaf1 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -6,7 +6,7 @@ import { LegacyAPICaller, CoreSetup } from '../../../../../src/core/server'; import { CollectorDependencies } from './types'; -import { DetectionsUsage, fetchDetectionsUsage } from './detections'; +import { DetectionsUsage, fetchDetectionsUsage, defaultDetectionsUsage } from './detections'; import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints'; export type RegisterCollector = (deps: CollectorDependencies) => void; @@ -76,9 +76,14 @@ export const registerCollector: RegisterCollector = ({ isReady: () => kibanaIndex.length > 0, fetch: async (callCluster: LegacyAPICaller): Promise => { const savedObjectsClient = await getInternalSavedObjectsClient(core); + const [detections, endpoints] = await Promise.allSettled([ + fetchDetectionsUsage(kibanaIndex, callCluster, ml), + getEndpointTelemetryFromFleet(savedObjectsClient), + ]); + return { - detections: await fetchDetectionsUsage(kibanaIndex, callCluster, ml), - endpoints: await getEndpointTelemetryFromFleet(savedObjectsClient), + detections: detections.status === 'fulfilled' ? detections.value : defaultDetectionsUsage, + endpoints: endpoints.status === 'fulfilled' ? endpoints.value : {}, }; }, }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index f9905c373291c..80a9dba26df8e 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -23,7 +23,10 @@ interface DetectionsMetric { const isElasticRule = (tags: string[]) => tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); -const initialRulesUsage: DetectionRulesUsage = { +/** + * Default detection rule usage count + */ +export const initialRulesUsage: DetectionRulesUsage = { custom: { enabled: 0, disabled: 0, @@ -34,7 +37,10 @@ const initialRulesUsage: DetectionRulesUsage = { }, }; -const initialMlJobsUsage: MlJobsUsage = { +/** + * Default ml job usage count + */ +export const initialMlJobsUsage: MlJobsUsage = { custom: { enabled: 0, disabled: 0, diff --git a/x-pack/plugins/security_solution/server/usage/detections/index.ts b/x-pack/plugins/security_solution/server/usage/detections/index.ts index dd50e79e22cc9..a366c86299b91 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/index.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/index.ts @@ -5,7 +5,12 @@ */ import { LegacyAPICaller } from '../../../../../../src/core/server'; -import { getMlJobsUsage, getRulesUsage } from './detections_helpers'; +import { + getMlJobsUsage, + getRulesUsage, + initialRulesUsage, + initialMlJobsUsage, +} from './detections_helpers'; import { MlPluginSetup } from '../../../../ml/server'; interface FeatureUsage { @@ -28,12 +33,23 @@ export interface DetectionsUsage { ml_jobs: MlJobsUsage; } +export const defaultDetectionsUsage = { + detection_rules: initialRulesUsage, + ml_jobs: initialMlJobsUsage, +}; + export const fetchDetectionsUsage = async ( kibanaIndex: string, callCluster: LegacyAPICaller, ml: MlPluginSetup | undefined ): Promise => { - const rulesUsage = await getRulesUsage(kibanaIndex, callCluster); - const mlJobsUsage = await getMlJobsUsage(ml); - return { detection_rules: rulesUsage, ml_jobs: mlJobsUsage }; + const [rulesUsage, mlJobsUsage] = await Promise.allSettled([ + getRulesUsage(kibanaIndex, callCluster), + getMlJobsUsage(ml), + ]); + + return { + detection_rules: rulesUsage.status === 'fulfilled' ? rulesUsage.value : initialRulesUsage, + ml_jobs: mlJobsUsage.status === 'fulfilled' ? mlJobsUsage.value : initialMlJobsUsage, + }; }; diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts index e3f0f7bde2fed..d753eeee93594 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts @@ -15,6 +15,7 @@ import { FLEET_ENDPOINT_PACKAGE_CONSTANT } from './fleet_saved_objects'; const testAgentId = 'testAgentId'; const testConfigId = 'testConfigId'; const testHostId = 'randoHostId'; +const testHostName = 'testDesktop'; /** Mock OS Platform for endpoint telemetry */ export const MockOSPlatform = 'somePlatform'; @@ -56,8 +57,8 @@ export const mockFleetObjectsResponse = ( }, }, host: { - hostname: 'testDesktop', - name: 'testDesktop', + hostname: testHostName, + name: testHostName, id: testHostId, }, os: { @@ -93,8 +94,8 @@ export const mockFleetObjectsResponse = ( }, }, host: { - hostname: 'testDesktop', - name: 'testDesktop', + hostname: hasDuplicates ? testHostName : 'oldRandoHostName', + name: hasDuplicates ? testHostName : 'oldRandoHostName', id: hasDuplicates ? testHostId : 'oldRandoHostId', }, os: { diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts index 42c1ec0e2eed2..c46610ec9388e 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts @@ -23,6 +23,8 @@ export const getFleetSavedObjectsMetadata = async (savedObjectsClient: ISavedObj 'last_checkin', 'local_metadata.agent.id', 'local_metadata.host.id', + 'local_metadata.host.name', + 'local_metadata.host.hostname', 'local_metadata.elastic.agent.id', 'local_metadata.os', ], diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts index 9e071f4adff25..19beda4554d93 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts @@ -42,7 +42,9 @@ export interface AgentLocalMetadata extends AgentMetadata { }; }; host: { + hostname: string; id: string; + name: string; }; os: { name: string; @@ -78,17 +80,20 @@ export const updateEndpointOSTelemetry = ( os: AgentLocalMetadata['os'], osTracker: OSTracker ): OSTracker => { - const updatedOSTracker = cloneDeep(osTracker); - const { version: osVersion, platform: osPlatform, full: osFullName } = os; - if (osFullName && osVersion) { - if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; - else { - updatedOSTracker[osFullName] = { - full_name: osFullName, - platform: osPlatform, - version: osVersion, - count: 1, - }; + let updatedOSTracker = osTracker; + if (os && typeof os === 'object') { + updatedOSTracker = cloneDeep(osTracker); + const { version: osVersion, platform: osPlatform, full: osFullName } = os; + if (osFullName && osVersion) { + if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; + else { + updatedOSTracker[osFullName] = { + full_name: osFullName, + platform: osPlatform, + version: osVersion, + count: 1, + }; + } } } @@ -211,46 +216,53 @@ export const getEndpointTelemetryFromFleet = async ( if (!endpointAgents || endpointAgentsCount < 1) return endpointTelemetry; // Use unique hosts to prevent any potential duplicates - const uniqueHostIds: Set = new Set(); + const uniqueHosts: Set = new Set(); let osTracker: OSTracker = {}; let dailyActiveCount = 0; let policyTracker: PoliciesTelemetry = { malware: { active: 0, inactive: 0, failure: 0 } }; for (let i = 0; i < endpointAgentsCount; i += 1) { - const { attributes: metadataAttributes } = endpointAgents[i]; - const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; - const { host, os, elastic } = localMetadata as AgentLocalMetadata; // AgentMetadata is just an empty blob, casting for our use case - - if (!uniqueHostIds.has(host.id)) { - uniqueHostIds.add(host.id); - const agentId = elastic?.agent?.id; - osTracker = updateEndpointOSTelemetry(os, osTracker); - - if (agentId) { - let agentEvents; - try { - const response = await getLatestFleetEndpointEvent(soClient, agentId); - agentEvents = response.saved_objects; - } catch (error) { - // If the request fails we do not obtain `active within last 24 hours for this agent` or policy specifics - } - - // AgentEvents will have a max length of 1 - if (agentEvents && agentEvents.length > 0) { - const latestEndpointEvent = agentEvents[0]; - dailyActiveCount = updateEndpointDailyActiveCount( - latestEndpointEvent, - lastCheckin, - dailyActiveCount + try { + const { attributes: metadataAttributes } = endpointAgents[i]; + const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; + const { host, os, elastic } = localMetadata as AgentLocalMetadata; + + // Although not perfect, the goal is to dedupe hosts to get the most recent data for a host + // An agent re-installed on the same host will have the same id and hostname + // A cloned VM will have the same id, but "may" have the same hostname, but it's really up to the user. + const compoundUniqueId = `${host?.id}-${host?.hostname}`; + if (!uniqueHosts.has(compoundUniqueId)) { + uniqueHosts.add(compoundUniqueId); + const agentId = elastic?.agent?.id; + osTracker = updateEndpointOSTelemetry(os, osTracker); + + if (agentId) { + const { saved_objects: agentEvents } = await getLatestFleetEndpointEvent( + soClient, + agentId ); - policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + + // AgentEvents will have a max length of 1 + if (agentEvents && agentEvents.length > 0) { + const latestEndpointEvent = agentEvents[0]; + dailyActiveCount = updateEndpointDailyActiveCount( + latestEndpointEvent, + lastCheckin, + dailyActiveCount + ); + policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + } } } + } catch (error) { + // All errors thrown in the loop would be handled here + // Not logging any errors to avoid leaking any potential PII + // Depending on when the error is thrown in the loop some specifics may be missing, but it allows the loop to continue } } // All unique hosts with an endpoint installed, thus all unique endpoint installs - endpointTelemetry.total_installed = uniqueHostIds.size; + endpointTelemetry.total_installed = uniqueHosts.size; // Set the daily active count for the endpoints endpointTelemetry.active_within_last_24_hours = dailyActiveCount; // Get the objects to populate our OS Telemetry diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts index 56ceca2b70e9c..5f002aa7fad7b 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts @@ -401,10 +401,17 @@ describe('Schema Beat', () => { const result = getIndexAlias([leadingWildcardIndex], leadingWildcardIndex); expect(result).toBe(leadingWildcardIndex); }); + test('getIndexAlias no match returns "unknown" string', () => { const index = 'auditbeat-*'; const result = getIndexAlias([index], 'hello'); expect(result).toBe('unknown'); }); + + test('empty index should not cause an error to return although it will cause an invalid regular expression to occur', () => { + const index = ''; + const result = getIndexAlias([index], 'hello'); + expect(result).toBe('unknown'); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts index ff7331cf39bc7..6ec15d328714d 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts @@ -77,10 +77,16 @@ const convertFieldsToAssociativeArray = ( : {}; export const getIndexAlias = (defaultIndex: string[], indexName: string): string => { - const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); - if (found != null) { - return found; - } else { + try { + const found = defaultIndex.find((index) => `\\${indexName}`.match(`\\${index}`) != null); + if (found != null) { + return found; + } else { + return 'unknown'; + } + } catch (error) { + // if we encounter an error because the index contains invalid regular expressions then we should return an unknown + // rather than blow up with a toaster error upstream return 'unknown'; } }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 69a8449c4dc70..e2f59f3fa910a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5008,7 +5008,6 @@ "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "以前の統合のレガシー機械学習ジョブが見つかりました。これは、APMアプリでは使用されていません。", "xpack.apm.settings.anomaly_detection.legacy_jobs.button": "ジョブの確認", "xpack.apm.settings.anomaly_detection.legacy_jobs.title": "レガシーMLジョブはAPMアプリで使用されていません。", - "xpack.apm.settings.anomaly_detection.license.text": "異常検知を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。このライセンスがあれば、機械学習を活用して、サービスを監視できます。", "xpack.apm.settings.anomalyDetection": "異常検知", "xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText": "キャンセル", "xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText": "ジョブの作成", @@ -11085,7 +11084,6 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "編集", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "Kibanaインデックスパターンの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。", - "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "JSONエディターからこのフォームには戻れません。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "データフレーム分析ジョブの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "既存のデータフレーム分析ジョブIDの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", @@ -11670,7 +11668,6 @@ "xpack.ml.jobMessages.timeLabel": "時間", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高度な構成", "xpack.ml.jobsBreadcrumbs.categorizationLabel": "カテゴリー分け", - "xpack.ml.jobsBreadcrumbs.jobWizardLabel": "ジョブを作成", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "マルチメトリック", "xpack.ml.jobsBreadcrumbs.populationLabel": "集団", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "ジョブを作成", @@ -12364,9 +12361,7 @@ "xpack.ml.overview.feedbackSectionLink": "オンラインでのフィードバック", "xpack.ml.overview.feedbackSectionText": "ご利用に際し、ご意見やご提案がありましたら、{feedbackLink}までお送りください。", "xpack.ml.overview.feedbackSectionTitle": "フィードバック", - "xpack.ml.overview.gettingStartedSectionCreateJob": "新規ジョブを作成中", "xpack.ml.overview.gettingStartedSectionDocs": "ドキュメンテーション", - "xpack.ml.overview.gettingStartedSectionText": "機械学習へようこそ。はじめに{docs}や{createJob}をご参照ください。{transforms}を使用して、分析ジョブの機能インデックスを作成することをお勧めします。", "xpack.ml.overview.gettingStartedSectionTitle": "はじめて使う", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearchの変換", "xpack.ml.overview.overviewLabel": "概要", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 898897548b7fc..316d3247d19d5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5009,7 +5009,6 @@ "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "我们在以前的集成中发现 APM 应用中不再使用的旧版 Machine Learning 作业", "xpack.apm.settings.anomaly_detection.legacy_jobs.button": "复查作业", "xpack.apm.settings.anomaly_detection.legacy_jobs.title": "旧版 ML 作业不再用于 APM 应用", - "xpack.apm.settings.anomaly_detection.license.text": "要使用异常检测,必须订阅 Elastic 白金级许可。使用该许可,您将能够借助 Machine Learning 监测服务。", "xpack.apm.settings.anomalyDetection": "异常检测", "xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText": "取消", "xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText": "创建作业", @@ -11087,7 +11086,6 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "编辑", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误:", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。", - "xpack.ml.dataframe.analytics.create.enableJsonEditorHelpText": "您不能从 json 编辑器切回到此表单。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "创建数据帧分析作业时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "获取现有数据帧分析作业 ID 时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", @@ -11673,7 +11671,6 @@ "xpack.ml.jobMessages.timeLabel": "时间", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高级配置", "xpack.ml.jobsBreadcrumbs.categorizationLabel": "归类", - "xpack.ml.jobsBreadcrumbs.jobWizardLabel": "创建作业", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "多指标", "xpack.ml.jobsBreadcrumbs.populationLabel": "填充", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "创建作业", @@ -12367,9 +12364,7 @@ "xpack.ml.overview.feedbackSectionLink": "在线反馈", "xpack.ml.overview.feedbackSectionText": "如果您在体验方面有任何意见或建议,请提交{feedbackLink}。", "xpack.ml.overview.feedbackSectionTitle": "反馈", - "xpack.ml.overview.gettingStartedSectionCreateJob": "创建新作业", "xpack.ml.overview.gettingStartedSectionDocs": "文档", - "xpack.ml.overview.gettingStartedSectionText": "欢迎使用 Machine Learning。首先阅读我们的{docs}或{createJob}。建议使用 {transforms} 为分析作业创建功能索引。", "xpack.ml.overview.gettingStartedSectionTitle": "入门", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearch 的转换", "xpack.ml.overview.overviewLabel": "概览", diff --git a/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts new file mode 100644 index 0000000000000..873cdc5d71baa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/common_jobs.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; + +export const commonJobConfig = { + description: 'test_job_annotation', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'min', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, +}; + +export const createJobConfig = (jobId: string) => { + return { ...commonJobConfig, job_id: jobId }; +}; + +export const testSetupJobConfigs = [1, 2, 3, 4].map((num) => ({ + ...commonJobConfig, + job_id: `job_annotation_${num}_${Date.now()}`, + description: `Test annotation ${num}`, +})); +export const jobIds = testSetupJobConfigs.map((j) => j.job_id); + +export const createAnnotationRequestBody = (jobId: string): Partial => { + return { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Test annotation', + job_id: jobId, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'user', + detector_index: 1, + partition_field_name: 'airline', + partition_field_value: 'AAL', + }; +}; + +export const testSetupAnnotations = testSetupJobConfigs.map((job) => + createAnnotationRequestBody(job.job_id) +); diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts new file mode 100644 index 0000000000000..14ecf1bfe524e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { createJobConfig, createAnnotationRequestBody } from './common_jobs'; +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `job_annotation_${Date.now()}`; + const testJobConfig = createJobConfig(jobId); + const annotationRequestBody = createAnnotationRequestBody(jobId); + + describe('create_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createAnomalyDetectionJob(testJobConfig); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should successfully create annotations for anomaly job', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + const annotationId = body._id; + + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + + expect(fetchedAnnotation).to.not.be(undefined); + + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_POWERUSER); + }); + + it('should successfully create annotation for user with ML read permissions', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(200); + + const annotationId = body._id; + const fetchedAnnotation = await ml.api.getAnnotationById(annotationId); + expect(fetchedAnnotation).to.not.be(undefined); + if (fetchedAnnotation) { + Object.keys(annotationRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(fetchedAnnotation[field]).to.eql(annotationRequestBody[field]); + }); + } + expect(fetchedAnnotation?.create_username).to.eql(USER.ML_VIEWER); + }); + + it('should not allow to create annotation for unauthorized user', async () => { + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts new file mode 100644 index 0000000000000..4fbb26e9b5a3e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('delete_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should delete annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should delete annotation by id for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body._id).to.eql(annotationIdToDelete); + expect(body.result).to.eql('deleted'); + + await ml.api.waitForAnnotationNotToExist(annotationIdToDelete); + }); + + it('should not delete annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const annotationIdToDelete = annotationsForJob[0]._id; + + const { body } = await supertest + .delete(`/api/ml/annotations/delete/${annotationIdToDelete}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + await ml.api.waitForAnnotationToExist(annotationIdToDelete); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts new file mode 100644 index 0000000000000..710473eed6901 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { omit } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('get_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should fetch all annotations for jobId', async () => { + const requestBody = { + jobIds: [jobIds[0]], + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + [jobIds[0]].forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for multiple jobs', async () => { + const requestBody = { + jobIds, + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should fetch all annotations for user with ML read permissions', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + expect(body.success).to.eql(true); + expect(body.annotations).not.to.be(undefined); + jobIds.forEach((jobId, idx) => { + expect(body.annotations).to.have.property(jobId); + expect(body.annotations[jobId]).to.have.length(1); + + const indexedAnnotation = omit(body.annotations[jobId][0], '_id'); + expect(indexedAnnotation).to.eql(testSetupAnnotations[idx]); + }); + }); + + it('should not allow to fetch annotation for unauthorized user', async () => { + const requestBody = { + jobIds: testSetupJobConfigs.map((j) => j.job_id), + earliestMs: 1454804100000, + latestMs: Date.now(), + maxAnnotations: 500, + }; + const { body } = await supertest + .post('/api/ml/annotations') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/annotations/index.ts b/x-pack/test/api_integration/apis/ml/annotations/index.ts new file mode 100644 index 0000000000000..7d73ee43d4d99 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('annotations', function () { + loadTestFile(require.resolve('./create_annotations')); + loadTestFile(require.resolve('./get_annotations')); + loadTestFile(require.resolve('./delete_annotations')); + loadTestFile(require.resolve('./update_annotations')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts new file mode 100644 index 0000000000000..ba73617151120 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { ANNOTATION_TYPE } from '../../../../../plugins/ml/common/constants/annotations'; +import { Annotation } from '../../../../../plugins/ml/common/types/annotations'; +import { testSetupJobConfigs, jobIds, testSetupAnnotations } from './common_jobs'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const commonAnnotationUpdateRequestBody: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + partition_field_name: 'airline', + partition_field_value: 'ANA', + }; + + describe('update_annotations', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // generate one annotation for each job + for (let i = 0; i < testSetupJobConfigs.length; i++) { + const job = testSetupJobConfigs[i]; + const annotationToIndex = testSetupAnnotations[i]; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.indexAnnotation(annotationToIndex); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should correctly update annotation by id', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[0]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } + }); + + it('should correctly update annotation for user with viewer permission', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[1]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(200); + + expect(body._id).to.eql(originalAnnotation._id); + expect(body.result).to.eql('updated'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(commonAnnotationUpdateRequestBody).forEach((key) => { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql(annotationUpdateRequestBody[field]); + }); + } + }); + + it('should not update annotation for unauthorized user', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[2]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + + const annotationUpdateRequestBody = { + ...commonAnnotationUpdateRequestBody, + job_id: originalAnnotation._source.job_id, + _id: originalAnnotation._id, + }; + + const { body } = await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + expect(updatedAnnotation).to.eql(originalAnnotation._source); + }); + + it('should override fields correctly', async () => { + const annotationsForJob = await ml.api.getAnnotations(jobIds[3]); + expect(annotationsForJob).to.have.length(1); + + const originalAnnotation = annotationsForJob[0]; + const annotationUpdateRequestBodyWithMissingFields: Partial = { + timestamp: Date.now(), + end_timestamp: Date.now(), + annotation: 'Updated annotation', + job_id: originalAnnotation._source.job_id, + type: ANNOTATION_TYPE.ANNOTATION, + event: 'model_change', + detector_index: 2, + _id: originalAnnotation._id, + }; + await supertest + .put('/api/ml/annotations/index') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(annotationUpdateRequestBodyWithMissingFields) + .expect(200); + + const updatedAnnotation = await ml.api.getAnnotationById(originalAnnotation._id); + if (updatedAnnotation) { + Object.keys(annotationUpdateRequestBodyWithMissingFields).forEach((key) => { + if (key !== '_id') { + const field = key as keyof Annotation; + expect(updatedAnnotation[field]).to.eql( + annotationUpdateRequestBodyWithMissingFields[field] + ); + } + }); + } + // validate missing fields in the annotationUpdateRequestBody + expect(updatedAnnotation?.partition_field_name).to.be(undefined); + expect(updatedAnnotation?.partition_field_value).to.be(undefined); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index b29bc47b50394..969f291b0d8b3 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -60,5 +60,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./data_frame_analytics')); loadTestFile(require.resolve('./filters')); loadTestFile(require.resolve('./calendars')); + loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts index a9bbf09a9e6f9..f99dd4c65fc83 100644 --- a/x-pack/test/api_integration/apis/security_solution/sources.ts +++ b/x-pack/test/api_integration/apis/security_solution/sources.ts @@ -18,22 +18,97 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/default')); after(() => esArchiver.unload('auditbeat/default')); - it('Make sure that we get source information when auditbeat indices is there', () => { - return client - .query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - docValueFields: [], - }, - }) - .then((resp) => { - const sourceStatus = resp.data.source.status; - // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz - expect(sourceStatus.indexFields.length).to.be(397); - expect(sourceStatus.indicesExist).to.be(true); - }); + it('Make sure that we get source information when auditbeat indices is there', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz + expect(sourceStatus.indexFields.length).to.be(397); + expect(sourceStatus.indicesExist).to.be(true); + }); + + it('should find indexes as being available when they exist', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(true); + }); + + it('should not find indexes as existing when there is an empty array of them', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there is a _all within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['_all'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there are empty strings within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [''], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should not find indexes as existing when there are blank spaces within it', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: [' '], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(false); + }); + + it('should find indexes when one is an empty index but the others are valid', async () => { + const resp = await client.query({ + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: ['', 'auditbeat-*'], + docValueFields: [], + }, + }); + const sourceStatus = resp.data.source.status; + expect(sourceStatus.indicesExist).to.be(true); }); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index b6a32ace5db56..33c00105e74f1 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -25,6 +25,12 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont describe('Settings', function () { loadTestFile(require.resolve('./settings/custom_link')); loadTestFile(require.resolve('./settings/agent_configuration')); + + describe('Anomaly detection', function () { + loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); + loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); + loadTestFile(require.resolve('./settings/anomaly_detection/write_user')); + }); }); describe('Traces', function () { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts new file mode 100644 index 0000000000000..d868a2a0e71cc --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const noAccessUser = getService('supertestAsNoAccessUser'); + + function getAnomalyDetectionJobs() { + return noAccessUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + } + + function createAnomalyDetectionJobs(environments: string[]) { + return noAccessUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + describe('when user does not have read access to ML', () => { + describe('when calling the endpoint for listing jobs', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await getAnomalyDetectionJobs(); + expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + }); + }); + + describe('when calling create endpoint', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await createAnomalyDetectionJobs(['production', 'staging']); + expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts new file mode 100644 index 0000000000000..070762a1d9446 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const apmReadUser = getService('supertestAsApmReadUser'); + + function getAnomalyDetectionJobs() { + return apmReadUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + } + + function createAnomalyDetectionJobs(environments: string[]) { + return apmReadUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + describe('when user has read access to ML', () => { + describe('when calling the endpoint for listing jobs', () => { + it('returns an error because the user is on basic license', async () => { + const { body } = await getAnomalyDetectionJobs(); + expect(body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: + "To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.", + }); + }); + }); + + describe('when calling create endpoint', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await createAnomalyDetectionJobs(['production', 'staging']); + expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts new file mode 100644 index 0000000000000..c7bd7f0c96fa4 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const apmWriteUser = getService('supertestAsApmWriteUser'); + + function getAnomalyDetectionJobs() { + return apmWriteUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + } + + function createAnomalyDetectionJobs(environments: string[]) { + return apmWriteUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + describe('when user has write access to ML', () => { + describe('when calling the endpoint for listing jobs', () => { + it('returns an error because the user is on basic license', async () => { + const { body } = await getAnomalyDetectionJobs(); + expect(body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: + "To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.", + }); + }); + }); + + describe('when calling create endpoint', () => { + it('returns an error because the user is on basic license', async () => { + const { body } = await createAnomalyDetectionJobs(['production', 'staging']); + expect(body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: + "To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.", + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 9c34b4791114a..5db456ee26107 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/test/apm_api_integration/common/authentication.ts @@ -10,42 +10,28 @@ import { SecurityServiceProvider } from '../../../../test/common/services/securi type SecurityService = PromiseReturnType; export enum ApmUser { + noAccessUser = 'no_access_user', apmReadUser = 'apm_read_user', apmWriteUser = 'apm_write_user', apmAnnotationsWriteUser = 'apm_annotations_write_user', } const roles = { + [ApmUser.noAccessUser]: {}, [ApmUser.apmReadUser]: { - elasticsearch: { - cluster: [], - indices: [ - { names: ['observability-annotations'], privileges: ['read', 'view_index_metadata'] }, - ], - }, kibana: [ { base: [], - feature: { - apm: ['read'], - }, + feature: { apm: ['read'], ml: ['read'] }, spaces: ['*'], }, ], }, [ApmUser.apmWriteUser]: { - elasticsearch: { - cluster: [], - indices: [ - { names: ['observability-annotations'], privileges: ['read', 'view_index_metadata'] }, - ], - }, kibana: [ { base: [], - feature: { - apm: ['all'], - }, + feature: { apm: ['all'], ml: ['all'] }, spaces: ['*'], }, ], @@ -71,6 +57,9 @@ const roles = { }; const users = { + [ApmUser.noAccessUser]: { + roles: [], + }, [ApmUser.apmReadUser]: { roles: ['apm_user', ApmUser.apmReadUser], }, diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index e4dc2a78ae018..110f42115397e 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -53,6 +53,7 @@ export function createTestConfig(settings: Settings) { ...services, supertest: supertestAsApmReadUser, supertestAsApmReadUser, + supertestAsNoAccessUser: supertestAsApmUser(servers.kibana, ApmUser.noAccessUser), supertestAsApmWriteUser: supertestAsApmUser(servers.kibana, ApmUser.apmWriteUser), supertestAsApmAnnotationsWriteUser: supertestAsApmUser( servers.kibana, diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index fb942f755a063..1b3b5602445ed 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -15,6 +15,14 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./services/rum_services.ts')); }); + describe('Settings', function () { + describe('Anomaly detection', function () { + loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); + loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); + loadTestFile(require.resolve('./settings/anomaly_detection/write_user')); + }); + }); + describe('Service Maps', function () { loadTestFile(require.resolve('./service_maps/service_maps')); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts new file mode 100644 index 0000000000000..39cd578917ba2 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const noAccessUser = getService('supertestAsNoAccessUser'); + + function getJobs() { + return noAccessUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + } + + function createJobs(environments: string[]) { + return noAccessUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + describe('when user does not have read access to ML', () => { + describe('when calling the endpoint for listing jobs', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await getJobs(); + expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + }); + }); + + describe('when calling create endpoint', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await createJobs(['production', 'staging']); + expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts new file mode 100644 index 0000000000000..6ea0124e5ee8e --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const apmReadUser = getService('supertestAsApmReadUser'); + + function getJobs() { + return apmReadUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + } + + function createJobs(environments: string[]) { + return apmReadUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + describe('when user has read access to ML', () => { + describe('when calling the endpoint for listing jobs', () => { + it('returns a list of jobs', async () => { + const { body } = await getJobs(); + expect(body).to.eql({ + jobs: [], + hasLegacyJobs: false, + }); + }); + }); + + describe('when calling create endpoint', () => { + it('returns an error because the user does not have access', async () => { + const { body } = await createJobs(['production', 'staging']); + expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts new file mode 100644 index 0000000000000..56a2d5dc0f662 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const apmWriteUser = getService('supertestAsApmWriteUser'); + + function getJobs() { + return apmWriteUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + } + + function createJobs(environments: string[]) { + return apmWriteUser + .post(`/api/apm/settings/anomaly-detection/jobs`) + .send({ environments }) + .set('kbn-xsrf', 'foo'); + } + + function deleteJobs(jobIds: string[]) { + return apmWriteUser.post(`/api/ml/jobs/delete_jobs`).send({ jobIds }).set('kbn-xsrf', 'foo'); + } + + describe('when user has write access to ML', () => { + after(async () => { + const res = await getJobs(); + const jobIds = res.body.jobs.map((job: any) => job.job_id); + await deleteJobs(jobIds); + }); + + describe('when calling the endpoint for listing jobs', () => { + it('returns a list of jobs', async () => { + const { body } = await getJobs(); + expect(body).to.eql({ jobs: [], hasLegacyJobs: false }); + }); + }); + + describe('when calling create endpoint', () => { + it('creates two jobs', async () => { + await createJobs(['production', 'staging']); + + const { body } = await getJobs(); + expect(body.hasLegacyJobs).to.be(false); + expect(body.jobs.map((job: any) => job.environment)).to.eql(['production', 'staging']); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index c8adb3ce67d55..f1da94febc631 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -14,8 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const config = getService('config'); - // FLAKY: https://github.com/elastic/kibana/issues/57413 - describe.skip('spaces feature controls', () => { + describe('spaces feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index c2500dca78444..68e5045c1f36c 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -129,17 +129,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show venn when clicking a line', async function () { await buildGraph(); - const { edges } = await PageObjects.graph.getGraphObjects(); await PageObjects.graph.isolateEdge('test', '/test/wp-admin/'); await PageObjects.graph.stopLayout(); await PageObjects.common.sleep(1000); - const testTestWpAdminBlogEdge = edges.find( - ({ sourceNode, targetNode }) => - targetNode.label === '/test/wp-admin/' && sourceNode.label === 'test' - )!; - await testTestWpAdminBlogEdge.element.click(); + await browser.execute(() => { + const event = document.createEvent('SVGEvents'); + event.initEvent('click', true, true); + return document.getElementsByClassName('gphEdge')[0].dispatchEvent(event); + }); await PageObjects.common.sleep(1000); await PageObjects.graph.startLayout(); diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index 64c07273c9ccf..c8e8db84df96f 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -25,10 +25,30 @@ export default function ({ getPageObjects }) { await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "ios"'); await PageObjects.maps.waitForMapPanAndZoom(origView); - const { lat, lon, zoom } = await PageObjects.maps.getView(); + const { lat, lon } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(43); expect(Math.round(lon)).to.equal(-102); - expect(Math.round(zoom)).to.equal(5); + }); + }); + + describe('with joins', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('join example'); + await PageObjects.maps.enableAutoFitToBounds(); + }); + + it('should automatically fit to bounds when query is applied', async () => { + // Set view to other side of world so no matching results + await PageObjects.maps.setView(0, 0, 6); + + // Setting query should trigger fit to bounds and move map + const origView = await PageObjects.maps.getView(); + await PageObjects.maps.setAndSubmitQuery('prop1 >= 11'); + await PageObjects.maps.waitForMapPanAndZoom(origView); + + const { lat, lon } = await PageObjects.maps.getView(); + expect(Math.round(lat)).to.equal(0); + expect(Math.round(lon)).to.equal(60); }); }); }); diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index ccf35a1e63e37..7e9a2cd85935e 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -14,8 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - // Failing: See https://github.com/elastic/kibana/issues/70493 - describe.skip('certificates', function () { + describe('certificates', function () { before(async () => { await makeCheck({ es, tls: true }); await uptime.goToRoot(true); @@ -58,6 +57,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const certId = getSha256(); const { monitorId } = await makeCheck({ es, + monitorId: 'cert-test-check-id', + fields: { + monitor: { + name: 'Cert Test Check', + }, + url: { + full: 'https://site-to-check.com/', + }, + }, tls: { sha256: certId, }, diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 028ab3ff8803a..6b2b61cba2b64 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -55,6 +55,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./certificates')); }); + describe('with real-world data', () => { before(async () => { await esArchiver.unload(ARCHIVE); diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 9dfec3a17dec0..401a96c5c11bd 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,13 +5,29 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { IndexDocumentParams } from 'elasticsearch'; import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index'; +import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; +import { + ML_ANNOTATIONS_INDEX_ALIAS_READ, + ML_ANNOTATIONS_INDEX_ALIAS_WRITE, +} from '../../../../plugins/ml/common/constants/index_patterns'; + +interface EsIndexResult { + _index: string; + _id: string; + _version: number; + result: string; + _shards: any; + _seq_no: number; + _primary_term: number; +} export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); @@ -634,5 +650,77 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } }); }, + + async getAnnotations(jobId: string) { + log.debug(`Fetching annotations for job '${jobId}'...`); + + const results = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + query: { + match: { + job_id: jobId, + }, + }, + }, + }); + expect(results).to.not.be(undefined); + expect(results).to.have.property('hits'); + return results.hits.hits; + }, + + async getAnnotationById(annotationId: string): Promise { + log.debug(`Fetching annotation '${annotationId}'...`); + + const result = await es.search({ + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + body: { + size: 1, + query: { + match: { + _id: annotationId, + }, + }, + }, + }); + // @ts-ignore due to outdated type for hits.total + if (result.hits.total.value === 1) { + return result?.hits?.hits[0]?._source as Annotation; + } + return undefined; + }, + + async indexAnnotation(annotationRequestBody: Partial) { + log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`); + // @ts-ignore due to outdated type for IndexDocumentParams.type + const params: IndexDocumentParams> = { + index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, + body: annotationRequestBody, + refresh: 'wait_for', + }; + const results: EsIndexResult = await es.index(params); + await this.waitForAnnotationToExist(results._id); + return results; + }, + + async waitForAnnotationToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) !== undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should exist`); + } + }); + }, + + async waitForAnnotationNotToExist(annotationId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await this.getAnnotationById(annotationId)) === undefined) { + return true; + } else { + throw new Error(errorMsg ?? `annotation '${annotationId}' should not exist`); + } + }); + }, }; } diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts index 2ceab1ca89e54..06de9be5af7e9 100644 --- a/x-pack/test/functional/services/uptime/certificates.ts +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -7,10 +7,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export function UptimeCertProvider({ getService }: FtrProviderContext) { +export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'timePicker', 'header']); + const changeSearchField = async (text: string) => { const input = await testSubjects.find('uptimeCertSearch'); await input.clearValueWithKeyboard(); @@ -61,6 +63,7 @@ export function UptimeCertProvider({ getService }: FtrProviderContext) { const self = this; return retry.tryForTime(60 * 1000, async () => { await changeSearchField(monId); + await PageObjects.header.waitUntilLoadingHasFinished(); await self.hasCertificates(1); }); }, diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index ab511abf130a5..710923c886cbe 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -17,7 +17,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv if (await testSubjects.exists('uptimeSettingsToOverviewLink', { timeout: 0 })) { await testSubjects.click('uptimeSettingsToOverviewLink'); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); - } else if (!(await testSubjects.exists('uptimeOverviewPage', { timeout: 0 }))) { + } else { await PageObjects.common.navigateToApp('uptime'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 6cb74aff95be2..a6de87d6f7b1a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -8,8 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - // FLAKY: https://github.com/elastic/kibana/issues/65948 - describe.skip('uptime alerts', () => { + describe('uptime alerts', () => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); @@ -105,7 +104,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { alertTypeId, consumer, id, - params: { numTimes, timerange, locations, filters }, + params: { numTimes, timerangeUnit, timerangeCount, filters }, schedule: { interval }, tags, } = alert; @@ -119,14 +118,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(interval).to.eql('11m'); expect(tags).to.eql(['uptime', 'another']); expect(numTimes).to.be(3); - expect(timerange.from).to.be('now-1h'); - expect(timerange.to).to.be('now'); - expect(locations).to.eql(['mpls']); - expect(filters).to.eql( - '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + - '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' + expect(timerangeUnit).to.be('h'); + expect(timerangeCount).to.be(1); + expect(JSON.stringify(filters)).to.eql( + `{"url.port":["5678"],"observer.geo.name":["mpls"],"monitor.type":["http"],"tags":[]}` ); } finally { await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts index 733b8d4fd9bd6..3f99f91394d2c 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts @@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -61,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -73,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .get('/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); diff --git a/x-pack/test/ingest_manager_api_integration/config.ts b/x-pack/test/ingest_manager_api_integration/config.ts index ddb49a09a7afa..85d1c20c7f155 100644 --- a/x-pack/test/ingest_manager_api_integration/config.ts +++ b/x-pack/test/ingest_manager_api_integration/config.ts @@ -10,9 +10,9 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { defineDockerServersConfig } from '@kbn/test'; // Docker image to use for Ingest Manager API integration tests. -// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit/48f3935a72b0c5aacc6fec8ef36d559b089a238b +// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit export const dockerImage = - 'docker.elastic.co/package-registry/distribution:48f3935a72b0c5aacc6fec8ef36d559b089a238b'; + 'docker.elastic.co/package-registry/distribution:80e93ade87f65e18d487b1c407406825915daba8'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/plugin_api_integration/config.ts b/x-pack/test/plugin_api_integration/config.ts index 6a86bcb7176f7..b89ed6ad550a3 100644 --- a/x-pack/test/plugin_api_integration/config.ts +++ b/x-pack/test/plugin_api_integration/config.ts @@ -20,6 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { testFiles: [ + require.resolve('./test_suites/platform'), require.resolve('./test_suites/task_manager'), require.resolve('./test_suites/event_log'), require.resolve('./test_suites/licensed_feature_usage'), diff --git a/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/kibana.json b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/kibana.json new file mode 100644 index 0000000000000..37ec33c168e76 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "elasticsearch_client_xpack", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/package.json b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/package.json new file mode 100644 index 0000000000000..ed31989472134 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/package.json @@ -0,0 +1,14 @@ +{ + "name": "elasticsearch_client_xpack", + "version": "1.0.0", + "kibana": { + "version": "kibana" + }, + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} diff --git a/x-pack/plugins/canvas/public/components/navbar/index.js b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/index.ts similarity index 66% rename from x-pack/plugins/canvas/public/components/navbar/index.js rename to x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/index.ts index 6948ada93155d..19b54a9b0326d 100644 --- a/x-pack/plugins/canvas/public/components/navbar/index.js +++ b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Navbar as Component } from './navbar'; +import { ElasticsearchClientXPack } from './plugin'; -export const Navbar = pure(Component); +export const plugin = () => new ElasticsearchClientXPack(); diff --git a/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/plugin.ts new file mode 100644 index 0000000000000..921c8f9c50860 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/plugin.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup } from 'kibana/server'; + +export class ElasticsearchClientXPack implements Plugin { + constructor() {} + + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + router.get( + { path: '/api/elasticsearch_client_xpack/context/user', validate: false }, + async (context, req, res) => { + const { body } = await context.core.elasticsearch.client.asCurrentUser.security.getUser(); + return res.ok({ body }); + } + ); + + router.get( + { path: '/api/elasticsearch_client_xpack/contract/user', validate: false }, + async (context, req, res) => { + const [coreStart] = await core.getStartServices(); + const { body } = await coreStart.elasticsearch.client + .asScoped(req) + .asCurrentUser.security.getUser(); + return res.ok({ body }); + } + ); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/test/plugin_api_integration/test_suites/platform/elasticsearch_client.ts b/x-pack/test/plugin_api_integration/test_suites/platform/elasticsearch_client.ts new file mode 100644 index 0000000000000..c2f6e93de83c1 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/platform/elasticsearch_client.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('elasticsearch client', () => { + it('scopes the elasticsearch client provided via request context to user credentials', async () => { + const { body } = await supertest + .get('/api/elasticsearch_client_xpack/context/user') + .expect(200); + expect(body).not.to.be.empty(); + }); + it('scopes the elasticsearch client provided via request context to user credentials', async () => { + const { body } = await supertest + .get('/api/elasticsearch_client_xpack/contract/user') + .expect(200); + expect(body).not.to.be.empty(); + }); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/platform/index.ts b/x-pack/test/plugin_api_integration/test_suites/platform/index.ts new file mode 100644 index 0000000000000..3375e9ca839a5 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/platform/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('platform', function taskManagerSuite() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./elasticsearch_client')); + }); +} diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 7962ec60ff57e..ad1980cd7218b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_REGISTRY_URL } from '../../../../plugins/ingest_manager/common'; +import { getRegistryUrl as getRegistryUrlFromIngest } from '../../../../plugins/ingest_manager/server'; import { FtrProviderContext } from '../../ftr_provider_context'; import { isRegistryEnabled, - getRegistryUrl, + getRegistryUrlFromTestEnv, } from '../../../security_solution_endpoint_api_int/registry'; export default function (providerContext: FtrProviderContext) { @@ -22,7 +22,7 @@ export default function (providerContext: FtrProviderContext) { log.warning('These tests are being run with an external package registry'); } - const registryUrl = getRegistryUrl() ?? DEFAULT_REGISTRY_URL; + const registryUrl = getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); log.info(`Package registry URL for tests: ${registryUrl}`); before(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml index 4d93386b4d4e1..00e01fe9ea0fc 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml +++ b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml @@ -1,2 +1,4 @@ package_paths: - /packages/production + - /packages/staging + - /packages/snapshot diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 56adc2382e234..9cdef1c938890 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { FtrProviderContext } from '../ftr_provider_context'; -import { isRegistryEnabled, getRegistryUrl } from '../registry'; -import { DEFAULT_REGISTRY_URL } from '../../../plugins/ingest_manager/common'; +import { isRegistryEnabled, getRegistryUrlFromTestEnv } from '../registry'; +import { getRegistryUrl as getRegistryUrlFromIngest } from '../../../plugins/ingest_manager/server'; export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { const { loadTestFile, getService } = providerContext; @@ -20,7 +20,7 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider log.warning('These tests are being run with an external package registry'); } - const registryUrl = getRegistryUrl() ?? DEFAULT_REGISTRY_URL; + const registryUrl = getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); log.info(`Package registry URL for tests: ${registryUrl}`); before(async () => { @@ -31,5 +31,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); + loadTestFile(require.resolve('./package')); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts new file mode 100644 index 0000000000000..3b5873d1fe0cd --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { eventsIndexPattern } from '../../../plugins/security_solution/common/endpoint/constants'; +import { + EndpointDocGenerator, + Event, +} from '../../../plugins/security_solution/common/endpoint/generate_data'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { InsertedEvents, processEventsIndex } from '../services/resolver'; + +interface EventIngested { + event: { + ingested: number; + }; +} + +interface NetworkEvent { + source: { + geo?: { + country_name: string; + }; + }; + destination: { + geo?: { + country_name: string; + }; + }; +} + +const networkIndex = 'logs-endpoint.events.network-default'; + +export default function ({ getService }: FtrProviderContext) { + const resolver = getService('resolverGenerator'); + const es = getService('es'); + const generator = new EndpointDocGenerator('data'); + + const searchForID = async (id: string) => { + return es.search>({ + index: eventsIndexPattern, + body: { + query: { + bool: { + filter: [ + { + ids: { + values: id, + }, + }, + ], + }, + }, + }, + }); + }; + + describe('Endpoint package', () => { + describe('ingested processor', () => { + let event: Event; + let genData: InsertedEvents; + + before(async () => { + event = generator.generateEvent(); + genData = await resolver.insertEvents([event]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('sets the event.ingested field', async () => { + const resp = await searchForID(genData.eventsInfo[0]._id); + expect(resp.body.hits.hits[0]._source.event.ingested).to.not.be(undefined); + }); + }); + + describe('geoip processor', () => { + let processIndexData: InsertedEvents; + let networkIndexData: InsertedEvents; + + before(async () => { + // 46.239.193.5 should be in Iceland + // 8.8.8.8 should be in the US + const eventWithBothIPs = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + }); + + const eventWithSourceOnly = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' } }, + }); + networkIndexData = await resolver.insertEvents( + [eventWithBothIPs, eventWithSourceOnly], + networkIndex + ); + + processIndexData = await resolver.insertEvents([eventWithBothIPs], processEventsIndex); + }); + + after(async () => { + await resolver.deleteData(networkIndexData); + await resolver.deleteData(processIndexData); + }); + + it('sets the geoip fields', async () => { + const eventWithBothIPs = await searchForID( + networkIndexData.eventsInfo[0]._id + ); + // Should be 'United States' + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + undefined + ); + // should be 'Iceland' + expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo?.country_name).to.not.be( + undefined + ); + + const eventWithSourceOnly = await searchForID( + networkIndexData.eventsInfo[1]._id + ); + // Should be 'United States' + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + undefined + ); + expect(eventWithSourceOnly.body.hits.hits[0]._source.destination?.geo).to.be(undefined); + }); + + it('does not set geoip fields for events in indices other than the network index', async () => { + const eventWithBothIPs = await searchForID( + processIndexData.eventsInfo[0]._id + ); + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo).to.be(undefined); + expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo).to.be(undefined); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index 4f2a801377204..231871fae3d39 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { SearchResponse } from 'elasticsearch'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { ResolverTree, @@ -20,7 +19,6 @@ import { InsertedEvents } from '../../services/resolver'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); - const es = getService('es'); const generator = new EndpointDocGenerator('resolver'); describe('Resolver handling of entity ids', () => { @@ -38,26 +36,10 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC }); it('excludes events that have an empty entity_id field', async () => { - // first lets get the _id of the document using the parent.process.entity_id - // then we'll use the API to search for that specific document - const res = await es.search>({ - index: genData.indices[0], - body: { - query: { - bool: { - filter: [ - { - term: { 'process.parent.entity_id': origin.process.parent!.entity_id }, - }, - ], - }, - }, - }, - }); const { body }: { body: ResolverEntityIndex } = await supertest.get( // using the same indices value here twice to force the query parameter to be an array // for some reason using supertest's query() function doesn't construct a parsable array - `/api/endpoint/resolver/entity?_id=${res.body.hits.hits[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` + `/api/endpoint/resolver/entity?_id=${genData.eventsInfo[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` ); expect(body).to.be.empty(); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/registry.ts b/x-pack/test/security_solution_endpoint_api_int/registry.ts index cc474cbf29aaf..9a9d184b9c297 100644 --- a/x-pack/test/security_solution_endpoint_api_int/registry.ts +++ b/x-pack/test/security_solution_endpoint_api_int/registry.ts @@ -57,7 +57,7 @@ export function createEndpointDockerConfig( }); } -export function getRegistryUrl(): string | undefined { +export function getRegistryUrlFromTestEnv(): string | undefined { let registryUrl: string | undefined; if (dockerRegistryPort !== undefined) { registryUrl = `--xpack.ingestManager.registryUrl=http://localhost:${dockerRegistryPort}`; @@ -68,10 +68,10 @@ export function getRegistryUrl(): string | undefined { } export function getRegistryUrlAsArray(): string[] { - const registryUrl: string | undefined = getRegistryUrl(); + const registryUrl: string | undefined = getRegistryUrlFromTestEnv(); return registryUrl !== undefined ? [registryUrl] : []; } export function isRegistryEnabled() { - return getRegistryUrl() !== undefined; + return getRegistryUrlFromTestEnv() !== undefined; } diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 335689b804d5b..7e4d4177affac 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -11,7 +11,7 @@ import { } from '../../../plugins/security_solution/common/endpoint/generate_data'; import { FtrProviderContext } from '../ftr_provider_context'; -const processIndex = 'logs-endpoint.events.process-default'; +export const processEventsIndex = 'logs-endpoint.events.process-default'; /** * Options for build a resolver tree @@ -36,7 +36,7 @@ export interface GeneratedTrees { * Structure containing the events inserted into ES and the index they live in */ export interface InsertedEvents { - events: Event[]; + eventsInfo: Array<{ _id: string; event: Event }>; indices: string[]; } @@ -46,24 +46,37 @@ interface BulkCreateHeader { }; } +interface BulkResponse { + items: Array<{ + create: { + _id: string; + }; + }>; +} + export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const client = getService('es'); return { async insertEvents( events: Event[], - eventsIndex: string = processIndex + eventsIndex: string = processEventsIndex ): Promise { const body = events.reduce((array: Array, doc) => { array.push({ create: { _index: eventsIndex } }, doc); return array; }, []); - await client.bulk({ body, refresh: true }); - return { events, indices: [eventsIndex] }; + const bulkResp = await client.bulk({ body, refresh: true }); + + const eventsInfo = events.map((event: Event, i: number) => { + return { event, _id: bulkResp.body.items[i].create._id }; + }); + + return { eventsInfo, indices: [eventsIndex] }; }, async createTrees( options: Options, - eventsIndex: string = processIndex, + eventsIndex: string = processEventsIndex, alertsIndex: string = 'logs-endpoint.alerts-default' ): Promise { const seed = options.seed || 'resolver-seed'; diff --git a/yarn.lock b/yarn.lock index 60d073330b35d..a70f04e030447 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4092,7 +4092,14 @@ "@testing-library/dom" "^6.3.0" "@types/testing-library__react" "^9.1.0" -"@turf/bbox@6.x": +"@turf/bbox-polygon@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/bbox-polygon/-/bbox-polygon-6.0.1.tgz#ae0fbb14558831fb34538aae089a23d3336c6379" + integrity sha512-f6BK6GOzUNjmJeOYHklk/5LNcQMQbo51gvAM10dTM5IqzKP01KM5bgV88uOKfSZB0HRQVpaRV1tgXk2bg5cPRg== + dependencies: + "@turf/helpers" "6.x" + +"@turf/bbox@6.0.1", "@turf/bbox@6.x": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.0.1.tgz#b966075771475940ee1c16be2a12cf389e6e923a" integrity sha512-EGgaRLettBG25Iyx7VyUINsPpVj1x3nFQFiGS3ER8KCI1MximzNLsam3eXRabqQDjyAKyAE1bJ4EZEpGvspQxw== @@ -4143,6 +4150,19 @@ "@turf/helpers" "6.x" "@turf/invariant" "6.x" +"@turf/distance@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.0.1.tgz#0761f28784286e7865a427c4e7e3593569c2dea8" + integrity sha512-q7t7rWIWfkg7MP1Vt4uLjSEhe5rPfCO2JjpKmk7JC+QZKEQkuvHEqy3ejW1iC7Kw5ZcZNR3qdMGGz+6HnVwqvg== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + +"@turf/helpers@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.0.1.tgz#625112616159e519033dc5d24c094ccbce7a457f" + integrity sha512-EQtcbwiNbkBnvxvlxcTcrwTzaq2eR4fnDVlzx/hsw5tYxDiMJfuL9goy1rDCmXxcyDnk8gCyZapYfEQWYLl1pQ== + "@turf/helpers@6.x": version "6.1.4" resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.1.4.tgz#d6fd7ebe6782dd9c87dca5559bda5c48ae4c3836" @@ -4788,7 +4808,7 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest-specific-snapshot@^0.5.3": +"@types/jest-specific-snapshot@^0.5.3", "@types/jest-specific-snapshot@^0.5.4": version "0.5.4" resolved "https://registry.yarnpkg.com/@types/jest-specific-snapshot/-/jest-specific-snapshot-0.5.4.tgz#997364c39a59ddeff0ee790a19415e79dd061d1e" integrity sha512-1qISn4fH8wkOOPFEx+uWRRjw6m/pP/It3OHLm8Ee1KQpO7Z9ZGYDtWPU5AgK05UXsNTAgOK+dPQvJKGdy9E/1g== @@ -5706,7 +5726,7 @@ "@types/node" "*" chokidar "^2.1.2" -"@types/webpack-env@^1.15.0": +"@types/webpack-env@^1.15.0", "@types/webpack-env@^1.15.2": version "1.15.2" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== @@ -6229,13 +6249,6 @@ adm-zip@0.4.11: resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.11.tgz#2aa54c84c4b01a9d0fb89bb11982a51f13e3d62a" integrity sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA== -affine-hull@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/affine-hull/-/affine-hull-1.0.0.tgz#763ff1d38d063ceb7e272f17ee4d7bbcaf905c5d" - integrity sha1-dj/x040GPOt+Jy8X7k17vK+QXF0= - dependencies: - robust-orientation "^1.1.3" - after-all-results@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/after-all-results/-/after-all-results-2.0.0.tgz#6ac2fc202b500f88da8f4f5530cfa100f4c6a2d0" @@ -8216,11 +8229,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bit-twiddle@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e" - integrity sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4= - bl@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" @@ -10374,15 +10382,6 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= -convex-hull@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/convex-hull/-/convex-hull-1.0.3.tgz#20a3aa6ce87f4adea2ff7d17971c9fc1c67e1fff" - integrity sha1-IKOqbOh/St6i/30XlxyfwcZ+H/8= - dependencies: - affine-hull "^1.0.0" - incremental-convex-hull "^1.0.1" - monotone-convex-hull-2d "^1.0.1" - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -11934,6 +11933,11 @@ diagnostics@^1.1.1: enabled "1.0.x" kuler "1.0.x" +diff-match-patch@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + diff-match-patch@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" @@ -12300,11 +12304,6 @@ eachr@^3.2.0: editions "^1.1.1" typechecker "^4.3.0" -earcut@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.1.3.tgz#ca579545f351941af7c3d0df49c9f7d34af99b0c" - integrity sha512-AxdCdWUk1zzK/NuZ7e1ljj6IGC+VAdC3Qb7QQDsXpfNrc5IM8tL9nNXUmEGE6jRHTfZ10zhzRhtDmWVsR5pd3A== - earcut@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.2.tgz#41b0bc35f63e0fe80da7cddff28511e7e2e80d11" @@ -14897,13 +14896,6 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -geojson-area@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/geojson-area/-/geojson-area-0.2.1.tgz#2537b0982db86309f21d2c428a4257c7a6282cc6" - integrity sha1-JTewmC24YwnyHSxCikJXx6YoLMY= - dependencies: - wgs84 "0.0.0" - geojson-flatten@~0.2.1: version "0.2.4" resolved "https://registry.yarnpkg.com/geojson-flatten/-/geojson-flatten-0.2.4.tgz#8f3396f31a0f5b747e39c9e6a14088f43ba4ecfb" @@ -14912,16 +14904,6 @@ geojson-flatten@~0.2.1: get-stdin "^6.0.0" minimist "1.2.0" -geojson-normalize@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/geojson-normalize/-/geojson-normalize-0.0.0.tgz#2dbc3678cd1b31b8179e876bda70cd120dde35c0" - integrity sha1-Lbw2eM0bMbgXnodr2nDNEg3eNcA= - -geojson-random@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/geojson-random/-/geojson-random-0.2.2.tgz#ab4838f126adc5e16f8f94e655def820f9119dbc" - integrity sha1-q0g48SatxeFvj5TmVd74IPkRnbw= - geojson-vt@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" @@ -17083,14 +17065,6 @@ in-publish@^2.0.0: resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= -incremental-convex-hull@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/incremental-convex-hull/-/incremental-convex-hull-1.0.1.tgz#51428c14cb9d9a6144bfe69b2851fb377334be1e" - integrity sha1-UUKMFMudmmFEv+abKFH7N3M0vh4= - dependencies: - robust-orientation "^1.1.2" - simplicial-complex "^1.0.0" - indent-string@3.2.0, indent-string@^3.0.0, indent-string@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" @@ -19298,6 +19272,14 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsondiffpatch@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/jsondiffpatch/-/jsondiffpatch-0.4.1.tgz#9fb085036767f03534ebd46dcd841df6070c5773" + integrity sha512-t0etAxTUk1w5MYdNOkZBZ8rvYYN5iL+2dHCCx/DpkFm/bW28M6y5nUS83D4XdZiHy35Fpaw6LBb+F88fHZnVCw== + dependencies: + chalk "^2.3.0" + diff-match-patch "^1.0.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -19410,11 +19392,6 @@ jstransformer@1.0.0, jstransformer@^1.0.0: is-promise "^2.0.0" promise "^7.0.1" -jsts@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/jsts/-/jsts-1.1.2.tgz#d205d2cc8393081d9e484ae36282110695edc230" - integrity sha1-0gXSzIOTCB2eSErjYoIRBpXtwjA= - jsts@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/jsts/-/jsts-1.6.2.tgz#c0efc885edae06ae84f78cbf2a0110ba929c5925" @@ -21560,13 +21537,6 @@ monocle-ts@^1.0.0: resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.1.tgz#03a615938aa90983a4fa29749969d30f72d80ba1" integrity sha512-X9OzpOyd/R83sYex8NYpJjUzi/MLQMvGNVfxDYiIvs+QMXMEUDwR61MQoARFN10Cqz5h/mbFSPnIQNUIGhYd2Q== -monotone-convex-hull-2d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz#47f5daeadf3c4afd37764baa1aa8787a40eee08c" - integrity sha1-R/Xa6t88Sv03dkuqGqh4ekDu4Iw= - dependencies: - robust-orientation "^1.1.3" - moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" @@ -26869,34 +26839,6 @@ rison-node@1.0.2: resolved "https://registry.yarnpkg.com/rison-node/-/rison-node-1.0.2.tgz#b7b5f37f39f5ae2a51a973a33c9aa17239a33e4b" integrity sha1-t7Xzfzn1ripRqXOjPJqhcjmjPks= -robust-orientation@^1.1.2, robust-orientation@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/robust-orientation/-/robust-orientation-1.1.3.tgz#daff5b00d3be4e60722f0e9c0156ef967f1c2049" - integrity sha1-2v9bANO+TmByLw6cAVbvln8cIEk= - dependencies: - robust-scale "^1.0.2" - robust-subtract "^1.0.0" - robust-sum "^1.0.0" - two-product "^1.0.2" - -robust-scale@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/robust-scale/-/robust-scale-1.0.2.tgz#775132ed09542d028e58b2cc79c06290bcf78c32" - integrity sha1-d1Ey7QlULQKOWLLMecBikLz3jDI= - dependencies: - two-product "^1.0.2" - two-sum "^1.0.0" - -robust-subtract@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/robust-subtract/-/robust-subtract-1.0.0.tgz#e0b164e1ed8ba4e3a5dda45a12038348dbed3e9a" - integrity sha1-4LFk4e2LpOOl3aRaEgODSNvtPpo= - -robust-sum@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/robust-sum/-/robust-sum-1.0.0.tgz#16646e525292b4d25d82757a286955e0bbfa53d9" - integrity sha1-FmRuUlKStNJdgnV6KGlV4Lv6U9k= - rollup@^0.25.8: version "0.25.8" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.25.8.tgz#bf6ce83b87510d163446eeaa577ed6a6fc5835e0" @@ -27757,19 +27699,6 @@ simplebar@^4.2.0: lodash.throttle "^4.1.1" resize-observer-polyfill "^1.5.1" -simplicial-complex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/simplicial-complex/-/simplicial-complex-1.0.0.tgz#6c33a4ed69fcd4d91b7bcadd3b30b63683eae241" - integrity sha1-bDOk7Wn81Nkbe8rdOzC2NoPq4kE= - dependencies: - bit-twiddle "^1.0.0" - union-find "^1.0.0" - -simplify-js@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/simplify-js/-/simplify-js-1.2.3.tgz#a3422c1b9884d60421345eb44d2b872662df27f5" - integrity sha512-0IkEqs+5c5vROkHaifGfbqHf5tYDcsTBy6oJPRbFCSwp2uzEr+PpH3dNP7wD8O3d7zdUCjLVq1/xHkwA/JjlFA== - sinon@^7.4.2: version "7.5.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec" @@ -30027,440 +29956,6 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== -turf-along@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-along/-/turf-along-3.0.12.tgz#e622bde7a4bd138c09647d4b14aa0ea700485de6" - integrity sha1-5iK956S9E4wJZH1LFKoOpwBIXeY= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-area@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-area/-/turf-area-3.0.12.tgz#9b7e469ef9fb558fd147bb0c214823263bdbf13c" - integrity sha1-m35Gnvn7VY/RR7sMIUgjJjvb8Tw= - dependencies: - geojson-area "^0.2.1" - -turf-bbox-polygon@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bbox-polygon/-/turf-bbox-polygon-3.0.12.tgz#330dc0bb38322d61545df966ce6c80f685acf4f2" - integrity sha1-Mw3AuzgyLWFUXflmzmyA9oWs9PI= - dependencies: - turf-helpers "^3.0.12" - -turf-bbox@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bbox/-/turf-bbox-3.0.12.tgz#3fa06117c8443860ec80ac60fd5d2f1320bfb1be" - integrity sha1-P6BhF8hEOGDsgKxg/V0vEyC/sb4= - dependencies: - turf-meta "^3.0.12" - -turf-bearing@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bearing/-/turf-bearing-3.0.12.tgz#65f609dd850e7364c7771aa6ded87b0e1917fd20" - integrity sha1-ZfYJ3YUOc2THdxqm3th7DhkX/SA= - dependencies: - turf-invariant "^3.0.12" - -turf-bezier@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bezier/-/turf-bezier-3.0.12.tgz#102efdd4a63b265ee9c8c1727631920b36f4dd02" - integrity sha1-EC791KY7Jl7pyMFydjGSCzb03QI= - dependencies: - turf-helpers "^3.0.12" - -turf-buffer@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-buffer/-/turf-buffer-3.0.12.tgz#20840fe7c6aa67b24be1cab7ffcc5a82fd6bd971" - integrity sha1-IIQP58aqZ7JL4cq3/8xagv1r2XE= - dependencies: - geojson-normalize "0.0.0" - jsts "1.1.2" - turf-combine "^3.0.12" - turf-helpers "^3.0.12" - -turf-center@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-center/-/turf-center-3.0.12.tgz#45dd6c1729bb867291e3e002e9c7506f8c440196" - integrity sha1-Rd1sFym7hnKR4+AC6cdQb4xEAZY= - dependencies: - turf-bbox "^3.0.12" - turf-helpers "^3.0.12" - -turf-centroid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-centroid/-/turf-centroid-3.0.12.tgz#eaee0d698204b57fc33994bb1bc867b8da293f8f" - integrity sha1-6u4NaYIEtX/DOZS7G8hnuNopP48= - dependencies: - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-circle@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-circle/-/turf-circle-3.0.12.tgz#140b21cb4950f2d3cbc70d2df012936867f58930" - integrity sha1-FAshy0lQ8tPLxw0t8BKTaGf1iTA= - dependencies: - turf-destination "^3.0.12" - turf-helpers "^3.0.12" - -turf-collect@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-collect/-/turf-collect-3.0.12.tgz#6e986d1a707da319cc83e7238d0bcdf19aa3c7f2" - integrity sha1-bphtGnB9oxnMg+cjjQvN8Zqjx/I= - dependencies: - turf-inside "^3.0.12" - -turf-combine@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-combine/-/turf-combine-3.0.12.tgz#1670746f0fdce0d1ea8aa6a29ffe5438d446cf73" - integrity sha1-FnB0bw/c4NHqiqain/5UONRGz3M= - dependencies: - turf-meta "^3.0.12" - -turf-concave@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-concave/-/turf-concave-3.0.12.tgz#fcab6056965b0a8319f6cd802601095f2fd3a8eb" - integrity sha1-/KtgVpZbCoMZ9s2AJgEJXy/TqOs= - dependencies: - turf-distance "^3.0.12" - turf-meta "^3.0.12" - turf-tin "^3.0.12" - turf-union "^3.0.12" - -turf-convex@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-convex/-/turf-convex-3.0.12.tgz#a88ddc3e22d1cb658796a9c85d3ada3bd3eca357" - integrity sha1-qI3cPiLRy2WHlqnIXTraO9Pso1c= - dependencies: - convex-hull "^1.0.3" - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-destination@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-destination/-/turf-destination-3.0.12.tgz#7dd6fbf97e86f831a26c83ef2d5a2f8d1d8a6de2" - integrity sha1-fdb7+X6G+DGibIPvLVovjR2KbeI= - dependencies: - turf-helpers "^3.0.12" - turf-invariant "^3.0.12" - -turf-difference@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-difference/-/turf-difference-3.0.12.tgz#9c3d0d7630421005b8b25b7f068ed9efb4bc6ea7" - integrity sha1-nD0NdjBCEAW4slt/Bo7Z77S8bqc= - dependencies: - jsts "1.1.2" - turf-helpers "^3.0.12" - -turf-distance@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-distance/-/turf-distance-3.0.12.tgz#fb97b8705facd993b145e014b41862610eeca449" - integrity sha1-+5e4cF+s2ZOxReAUtBhiYQ7spEk= - dependencies: - turf-helpers "^3.0.12" - turf-invariant "^3.0.12" - -turf-envelope@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-envelope/-/turf-envelope-3.0.12.tgz#96921d278cc8c664692e320e2543b914080d786b" - integrity sha1-lpIdJ4zIxmRpLjIOJUO5FAgNeGs= - dependencies: - turf-bbox "^3.0.12" - turf-bbox-polygon "^3.0.12" - -turf-explode@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-explode/-/turf-explode-3.0.12.tgz#c5ae28c284cd006c56511ec7d408c48a5414ecfe" - integrity sha1-xa4owoTNAGxWUR7H1AjEilQU7P4= - dependencies: - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-flip@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-flip/-/turf-flip-3.0.12.tgz#deb868177b9ff3bb310c5d41aaac61a9156a3cbb" - integrity sha1-3rhoF3uf87sxDF1BqqxhqRVqPLs= - dependencies: - turf-meta "^3.0.12" - -turf-grid@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/turf-grid/-/turf-grid-1.0.1.tgz#b904abc564b939b627a66ac15eb16e053829b80f" - integrity sha1-uQSrxWS5ObYnpmrBXrFuBTgpuA8= - dependencies: - turf-point "^2.0.0" - -turf-helpers@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-helpers/-/turf-helpers-3.0.12.tgz#dd4272e74b3ad7c96eecb7ae5c57fe8eca544b7b" - integrity sha1-3UJy50s618lu7LeuXFf+jspUS3s= - -turf-hex-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-hex-grid/-/turf-hex-grid-3.0.12.tgz#0698ef669020bb31d8e9cc2056d0abfcafc84e8f" - integrity sha1-BpjvZpAguzHY6cwgVtCr/K/ITo8= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-inside@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-inside/-/turf-inside-3.0.12.tgz#9ba40fa6eed63bec7e7d88aa6427622c4df07066" - integrity sha1-m6QPpu7WO+x+fYiqZCdiLE3wcGY= - dependencies: - turf-invariant "^3.0.12" - -turf-intersect@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-intersect/-/turf-intersect-3.0.12.tgz#c0d7fb305843a19275670057a39d268b17830d83" - integrity sha1-wNf7MFhDoZJ1ZwBXo50mixeDDYM= - dependencies: - jsts "1.1.2" - -turf-invariant@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-invariant/-/turf-invariant-3.0.12.tgz#3b95253953991ebd962dd35d4f6704c287de8ebe" - integrity sha1-O5UlOVOZHr2WLdNdT2cEwofejr4= - -turf-isolines@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-isolines/-/turf-isolines-3.0.12.tgz#00b233dfe2eebd4ecb47a94fc923c6ecec89c7ab" - integrity sha1-ALIz3+LuvU7LR6lPySPG7OyJx6s= - dependencies: - turf-bbox "^3.0.12" - turf-grid "1.0.1" - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - turf-planepoint "^3.0.12" - turf-square "^3.0.12" - turf-tin "^3.0.12" - -turf-kinks@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-kinks/-/turf-kinks-3.0.12.tgz#e9c9a8dba5724d98f2350fc5bdeba069ec333755" - integrity sha1-6cmo26VyTZjyNQ/FveugaewzN1U= - dependencies: - turf-helpers "^3.0.12" - -turf-line-distance@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-line-distance/-/turf-line-distance-3.0.12.tgz#7108f5b26907f7b8c2dd1b3997866dd3a60e8f5f" - integrity sha1-cQj1smkH97jC3Rs5l4Zt06YOj18= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-line-slice@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-line-slice/-/turf-line-slice-3.0.12.tgz#f5f1accc92adae69ea1ac0b29f07529a28dde916" - integrity sha1-9fGszJKtrmnqGsCynwdSmijd6RY= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - turf-point-on-line "^3.0.12" - -turf-meta@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-meta/-/turf-meta-3.0.12.tgz#0aa9a1caf82b2a5a08d54e0830b5b5a3fa0e8a38" - integrity sha1-CqmhyvgrKloI1U4IMLW1o/oOijg= - -turf-midpoint@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-midpoint/-/turf-midpoint-3.0.12.tgz#b12765ae89acdee8556fd5e26c9c5fa041a02cbe" - integrity sha1-sSdlroms3uhVb9XibJxfoEGgLL4= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-invariant "^3.0.12" - -turf-nearest@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-nearest/-/turf-nearest-3.0.12.tgz#700207f4443f05096f86cd246f929f170dfaf46d" - integrity sha1-cAIH9EQ/BQlvhs0kb5KfFw369G0= - dependencies: - turf-distance "^3.0.12" - -turf-planepoint@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-planepoint/-/turf-planepoint-3.0.12.tgz#2c37ae0f17fcb30db6e38f0d59ee6c0dd6caa9af" - integrity sha1-LDeuDxf8sw22448NWe5sDdbKqa8= - -turf-point-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-grid/-/turf-point-grid-3.0.12.tgz#d604978be10bc9e53306ae02cef7098431db4971" - integrity sha1-1gSXi+ELyeUzBq4CzvcJhDHbSXE= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-point-on-line@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-on-line/-/turf-point-on-line-3.0.12.tgz#1d8663354e70372db1863e6253e9040c47127b0f" - integrity sha1-HYZjNU5wNy2xhj5iU+kEDEcSew8= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-point-on-surface@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-on-surface/-/turf-point-on-surface-3.0.12.tgz#9be505b6b0ba78e98565001de3b3a4267115240a" - integrity sha1-m+UFtrC6eOmFZQAd47OkJnEVJAo= - dependencies: - turf-center "^3.0.12" - turf-distance "^3.0.12" - turf-explode "^3.0.12" - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - -turf-point@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/turf-point/-/turf-point-2.0.1.tgz#a2dcc30a2d20f44cf5c6271df7bae2c0e2146069" - integrity sha1-otzDCi0g9Ez1xicd97riwOIUYGk= - dependencies: - minimist "^1.1.0" - -turf-random@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-random/-/turf-random-3.0.12.tgz#34dbb141c3f1eaeae1424fd6c5eaba1f6fb9b1e8" - integrity sha1-NNuxQcPx6urhQk/Wxeq6H2+5seg= - dependencies: - geojson-random "^0.2.2" - -turf-sample@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-sample/-/turf-sample-3.0.12.tgz#7949f8620612047e1314c1ced87e99c142463cd2" - integrity sha1-eUn4YgYSBH4TFMHO2H6ZwUJGPNI= - dependencies: - turf-helpers "^3.0.12" - -turf-simplify@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-simplify/-/turf-simplify-3.0.12.tgz#85e443c8b46aa2b7526389444c7381daa2ad19e7" - integrity sha1-heRDyLRqordSY4lETHOB2qKtGec= - dependencies: - simplify-js "^1.2.1" - -turf-square-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-square-grid/-/turf-square-grid-3.0.12.tgz#3c1d80ac14556c6813b478bda012512ed4b93ec8" - integrity sha1-PB2ArBRVbGgTtHi9oBJRLtS5Psg= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-square@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-square/-/turf-square-3.0.12.tgz#1a38b1e0fb05ffe0fcaa43188e2f37942a515b64" - integrity sha1-Gjix4PsF/+D8qkMYji83lCpRW2Q= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-tag@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tag/-/turf-tag-3.0.12.tgz#2284fff0e8a1e92a27d4ac7fd7471b3c48ddd1a8" - integrity sha1-IoT/8Oih6Son1Kx/10cbPEjd0ag= - dependencies: - turf-inside "^3.0.12" - -turf-tesselate@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tesselate/-/turf-tesselate-3.0.12.tgz#41474b7b5b3820bcf273fb71e1894d8c3cd40d35" - integrity sha1-QUdLe1s4ILzyc/tx4YlNjDzUDTU= - dependencies: - earcut "^2.0.0" - turf-helpers "^3.0.12" - -turf-tin@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tin/-/turf-tin-3.0.12.tgz#b6534644763ace1c9df241c958d2384855257385" - integrity sha1-tlNGRHY6zhyd8kHJWNI4SFUlc4U= - dependencies: - turf-helpers "^3.0.12" - -turf-triangle-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-triangle-grid/-/turf-triangle-grid-3.0.12.tgz#80647e57dafe09346879a29a18a0e6294acf1159" - integrity sha1-gGR+V9r+CTRoeaKaGKDmKUrPEVk= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-union@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-union/-/turf-union-3.0.12.tgz#dfed0e5540b8c2855e4994c14621e3a60c829c8e" - integrity sha1-3+0OVUC4woVeSZTBRiHjpgyCnI4= - dependencies: - jsts "1.1.2" - -turf-within@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-within/-/turf-within-3.0.12.tgz#f77eeaf377238561b7fb1338e76e9d1298741f94" - integrity sha1-937q83cjhWG3+xM4526dEph0H5Q= - dependencies: - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - -turf@3.0.14: - version "3.0.14" - resolved "https://registry.yarnpkg.com/turf/-/turf-3.0.14.tgz#eb2f4a80a2d583b8c6486bc7b5c7190466866c27" - integrity sha1-6y9KgKLVg7jGSGvHtccZBGaGbCc= - dependencies: - turf-along "^3.0.12" - turf-area "^3.0.12" - turf-bbox "^3.0.12" - turf-bbox-polygon "^3.0.12" - turf-bearing "^3.0.12" - turf-bezier "^3.0.12" - turf-buffer "^3.0.12" - turf-center "^3.0.12" - turf-centroid "^3.0.12" - turf-circle "^3.0.12" - turf-collect "^3.0.12" - turf-combine "^3.0.12" - turf-concave "^3.0.12" - turf-convex "^3.0.12" - turf-destination "^3.0.12" - turf-difference "^3.0.12" - turf-distance "^3.0.12" - turf-envelope "^3.0.12" - turf-explode "^3.0.12" - turf-flip "^3.0.12" - turf-helpers "^3.0.12" - turf-hex-grid "^3.0.12" - turf-inside "^3.0.12" - turf-intersect "^3.0.12" - turf-isolines "^3.0.12" - turf-kinks "^3.0.12" - turf-line-distance "^3.0.12" - turf-line-slice "^3.0.12" - turf-meta "^3.0.12" - turf-midpoint "^3.0.12" - turf-nearest "^3.0.12" - turf-planepoint "^3.0.12" - turf-point-grid "^3.0.12" - turf-point-on-line "^3.0.12" - turf-point-on-surface "^3.0.12" - turf-random "^3.0.12" - turf-sample "^3.0.12" - turf-simplify "^3.0.12" - turf-square "^3.0.12" - turf-square-grid "^3.0.12" - turf-tag "^3.0.12" - turf-tesselate "^3.0.12" - turf-tin "^3.0.12" - turf-triangle-grid "^3.0.12" - turf-union "^3.0.12" - turf-within "^3.0.12" - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -30475,16 +29970,6 @@ twig@^1.10.5: minimatch "3.0.x" walk "2.3.x" -two-product@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/two-product/-/two-product-1.0.2.tgz#67d95d4b257a921e2cb4bd7af9511f9088522eaa" - integrity sha1-Z9ldSyV6kh4stL16+VEfkIhSLqo= - -two-sum@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/two-sum/-/two-sum-1.0.0.tgz#31d3f32239e4f731eca9df9155e2b297f008ab64" - integrity sha1-MdPzIjnk9zHsqd+RVeKyl/AIq2Q= - type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -30839,11 +30324,6 @@ unified@^7.1.0: vfile "^3.0.0" x-is-string "^0.1.0" -union-find@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/union-find/-/union-find-1.0.2.tgz#292bac415e6ad3a89535d237010db4a536284e58" - integrity sha1-KSusQV5q06iVNdI3AQ20pTYoTlg= - union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"