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/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.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/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/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; + completed$: Observable; } // @public 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/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/plugins/dashboard/config.ts b/src/plugins/dashboard/config.ts new file mode 100644 index 0000000000000..ff968a51679e0 --- /dev/null +++ b/src/plugins/dashboard/config.ts @@ -0,0 +1,26 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +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/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/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/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts index 8bffc5d012a74..ad19def160200 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -75,7 +75,7 @@ describe('get_data_telemetry', () => { { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name // New Indexing strategy: everything can be inferred from the constant_keyword values { - name: 'logs-nginx.access-default-000001', + name: '.ds-logs-nginx.access-default-000001', datasetName: 'nginx.access', datasetType: 'logs', shipper: 'filebeat', @@ -84,7 +84,7 @@ describe('get_data_telemetry', () => { sizeInBytes: 1000, }, { - name: 'logs-nginx.access-default-000002', + name: '.ds-logs-nginx.access-default-000002', datasetName: 'nginx.access', datasetType: 'logs', shipper: 'filebeat', @@ -92,6 +92,42 @@ describe('get_data_telemetry', () => { docCount: 1000, sizeInBytes: 60, }, + { + name: '.ds-traces-something-default-000002', + datasetName: 'something', + datasetType: 'traces', + packageName: 'some-package', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + { + name: '.ds-metrics-something.else-default-000002', + datasetName: 'something.else', + datasetType: 'metrics', + managedBy: 'ingest-manager', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + // Filter out if it has datasetName and datasetType but none of the shipper, packageName or managedBy === 'ingest-manager' + { + name: 'some-index-that-should-not-show', + datasetName: 'should-not-show', + datasetType: 'logs', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + { + name: 'other-index-that-should-not-show', + datasetName: 'should-not-show-either', + datasetType: 'metrics', + managedBy: 'me', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, ]) ).toStrictEqual([ { @@ -138,6 +174,21 @@ describe('get_data_telemetry', () => { doc_count: 2000, size_in_bytes: 1060, }, + { + dataset: { name: 'something', type: 'traces' }, + package: { name: 'some-package' }, + index_count: 1, + ecs_index_count: 1, + doc_count: 1000, + size_in_bytes: 60, + }, + { + dataset: { name: 'something.else', type: 'metrics' }, + index_count: 1, + ecs_index_count: 1, + doc_count: 1000, + size_in_bytes: 60, + }, ]); }); }); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts index cf906bc5c86cf..079f510bb256a 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -36,6 +36,9 @@ export interface DataTelemetryDocument extends DataTelemetryBasePayload { name?: string; type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s }; + package?: { + name: string; + }; shipper?: string; pattern_name?: DataPatternName; } @@ -44,6 +47,8 @@ export type DataTelemetryPayload = DataTelemetryDocument[]; export interface DataTelemetryIndex { name: string; + packageName?: string; // Populated by Ingest Manager at `_meta.package.name` + managedBy?: string; // Populated by Ingest Manager at `_meta.managed_by` datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword shipper?: string; // To be obtained from `_meta.beat` if it's set @@ -58,6 +63,7 @@ export interface DataTelemetryIndex { type AtLeastOne }> = Partial & U[keyof U]; type DataDescriptor = AtLeastOne<{ + packageName: string; datasetName: string; datasetType: string; shipper: string; @@ -67,17 +73,28 @@ type DataDescriptor = AtLeastOne<{ function findMatchingDescriptors({ name, shipper, + packageName, + managedBy, datasetName, datasetType, }: DataTelemetryIndex): DataDescriptor[] { // If we already have the data from the indices' mappings... - if ([shipper, datasetName, datasetType].some(Boolean)) { + if ( + [shipper, packageName].some(Boolean) || + (managedBy === 'ingest-manager' && [datasetType, datasetName].some(Boolean)) + ) { return [ { ...(shipper && { shipper }), + ...(packageName && { packageName }), ...(datasetName && { datasetName }), ...(datasetType && { datasetType }), - } as AtLeastOne<{ datasetName: string; datasetType: string; shipper: string }>, // Using casting here because TS doesn't infer at least one exists from the if clause + } as AtLeastOne<{ + packageName: string; + datasetName: string; + datasetType: string; + shipper: string; + }>, // Using casting here because TS doesn't infer at least one exists from the if clause ]; } @@ -122,6 +139,7 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe ({ name }) => !( name.startsWith('.') && + !name.startsWith('.ds-') && // data_stream-related indices can be included !startingDotPatternsUntilTheFirstAsterisk.find((pattern) => name.startsWith(pattern)) ) ); @@ -130,10 +148,17 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe for (const indexCandidate of indexCandidates) { const matchingDescriptors = findMatchingDescriptors(indexCandidate); - for (const { datasetName, datasetType, shipper, patternName } of matchingDescriptors) { - const key = `${datasetName}-${datasetType}-${shipper}-${patternName}`; + for (const { + datasetName, + datasetType, + packageName, + shipper, + patternName, + } of matchingDescriptors) { + const key = `${datasetName}-${datasetType}-${packageName}-${shipper}-${patternName}`; acc.set(key, { ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...(packageName && { package: { name: packageName } }), ...(shipper && { shipper }), ...(patternName && { pattern_name: patternName }), ...increaseCounters(acc.get(key), indexCandidate), @@ -165,6 +190,12 @@ interface IndexMappings { mappings: { _meta?: { beat?: string; + + // Ingest Manager provided metadata + package?: { + name?: string; + }; + managed_by?: string; // Typically "ingest-manager" }; properties: { dataset?: { @@ -195,7 +226,7 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { try { const index = [ ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern), - '*-*-*-*', // Include new indexing strategy indices {type}-{dataset}-{namespace}-{rollover_counter} + '*-*-*', // Include data-streams aliases `{type}-{dataset}-{namespace}` ]; const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([ // GET */_mapping?filter_path=*.mappings._meta.beat,*.mappings.properties.ecs.properties.version.type,*.mappings.properties.dataset.properties.type.value,*.mappings.properties.dataset.properties.name.value @@ -204,16 +235,17 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { filterPath: [ // _meta.beat tells the shipper '*.mappings._meta.beat', + // _meta.package.name tells the Ingest Manager's package + '*.mappings._meta.package.name', + // _meta.managed_by is usually populated by Ingest Manager for the UI to identify it + '*.mappings._meta.managed_by', // Does it have `ecs.version` in the mappings? => It follows the ECS conventions '*.mappings.properties.ecs.properties.version.type', - // Disable the fields below because they are still pending to be confirmed: - // https://github.com/elastic/ecs/pull/845 - // TODO: Re-enable when the final fields are confirmed - // // If `dataset.type` is a `constant_keyword`, it can be reported as a type - // '*.mappings.properties.dataset.properties.type.value', - // // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset - // '*.mappings.properties.dataset.properties.name.value', + // If `dataset.type` is a `constant_keyword`, it can be reported as a type + '*.mappings.properties.dataset.properties.type.value', + // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset + '*.mappings.properties.dataset.properties.name.value', ], }), // GET /_stats/docs,store?level=indices&filter_path=indices.*.total @@ -227,24 +259,25 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices }); const indices = indexNames.map((name) => { - const isECS = !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type; - const shipper = indexMappings[name]?.mappings?._meta?.beat; - const datasetName = indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value; - const datasetType = indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value; + const baseIndexInfo = { + name, + isECS: !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type, + shipper: indexMappings[name]?.mappings?._meta?.beat, + packageName: indexMappings[name]?.mappings?._meta?.package?.name, + managedBy: indexMappings[name]?.mappings?._meta?.managed_by, + datasetName: indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value, + datasetType: indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value, + }; const stats = (indexStats?.indices || {})[name]; if (stats) { return { - name, - datasetName, - datasetType, - shipper, - isECS, + ...baseIndexInfo, docCount: stats.total?.docs?.count, sizeInBytes: stats.total?.store?.size_in_bytes, }; } - return { name, datasetName, datasetType, shipper, isECS }; + return baseIndexInfo; }); return buildDataTelemetryPayload(indices); } catch (e) { 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/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/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/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 e3104aabbb02b..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,6 +120,7 @@ "@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", @@ -158,6 +160,7 @@ "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", @@ -394,4 +397,4 @@ "cypress-multi-reporters" ] } -} +} \ No newline at end of file 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 c6c0861c26a34..b2f15dbb11341 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -84,7 +84,7 @@ interface Props { export function Home({ tab }: Props) { const { config, core } = useApmPluginContext(); - const isMLEnabled = !!core.application.capabilities.ml; + const canAccessML = !!core.application.capabilities.ml?.canAccessML; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab @@ -106,7 +106,7 @@ export function Home({ tab }: Props) { - {isMLEnabled && ( + {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/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index 1471bc345d850..cb4726244e50c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -20,7 +20,7 @@ import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function Settings(props: { children: ReactNode }) { const plugin = useApmPluginContext(); - const isMLEnabled = !!plugin.core.application.capabilities.ml; + const canAccessML = !!plugin.core.application.capabilities.ml?.canAccessML; const { search, pathname } = useLocation(); return ( <> @@ -51,7 +51,7 @@ export function Settings(props: { children: ReactNode }) { '/settings/agent-configuration' ), }, - ...(isMLEnabled + ...(canAccessML ? [ { name: i18n.translate( 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/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 36e33fba89fbb..2434d898389d8 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -64,21 +64,20 @@ describe('DatePicker', () => { }); beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); - it('should set default query params in the URL', () => { + it('sets default query params in the URL', () => { mountDatePicker(); expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledWith( expect.objectContaining({ - search: - 'rangeFrom=now-15m&rangeTo=now&refreshPaused=false&refreshInterval=10000', + search: 'rangeFrom=now-15m&rangeTo=now', }) ); }); - it('should add missing default value', () => { + it('adds missing default value', () => { mountDatePicker({ rangeTo: 'now', refreshInterval: 5000, @@ -86,13 +85,12 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledWith( expect.objectContaining({ - search: - 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000&refreshPaused=false', + search: 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000', }) ); }); - it('should not set default query params in the URL when values already defined', () => { + it('does not set default query params in the URL when values already defined', () => { mountDatePicker({ rangeFrom: 'now-1d', rangeTo: 'now', @@ -102,7 +100,7 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(0); }); - it('should update the URL when the date range changes', () => { + it('updates the URL when the date range changes', () => { const datePicker = mountDatePicker(); datePicker.find(EuiSuperDatePicker).props().onTimeChange({ start: 'updated-start', @@ -113,13 +111,12 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(2); expect(mockHistoryPush).toHaveBeenLastCalledWith( expect.objectContaining({ - search: - 'rangeFrom=updated-start&rangeTo=updated-end&refreshInterval=5000&refreshPaused=false', + search: 'rangeFrom=updated-start&rangeTo=updated-end', }) ); }); - it('should auto-refresh when refreshPaused is false', async () => { + it('enables auto-refresh when refreshPaused is false', async () => { jest.useFakeTimers(); const wrapper = mountDatePicker({ refreshPaused: false, @@ -132,7 +129,7 @@ describe('DatePicker', () => { wrapper.unmount(); }); - it('should NOT auto-refresh when refreshPaused is true', async () => { + it('disables auto-refresh when refreshPaused is true', async () => { jest.useFakeTimers(); mountDatePicker({ refreshPaused: true, refreshInterval: 1000 }); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index 5201d80de5a12..403a8cad854cd 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -14,11 +14,7 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { clearCache } from '../../../services/rest/callApi'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; -import { - TimePickerQuickRange, - TimePickerTimeDefaults, - TimePickerRefreshInterval, -} from './typings'; +import { TimePickerQuickRange, TimePickerTimeDefaults } from './typings'; function removeUndefinedAndEmptyProps(obj: T): Partial { return pickBy(obj, (value) => value !== undefined && !isEmpty(String(value))); @@ -36,19 +32,9 @@ export function DatePicker() { UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS ); - const timePickerRefreshIntervalDefaults = core.uiSettings.get< - TimePickerRefreshInterval - >(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); - const DEFAULT_VALUES = { rangeFrom: timePickerTimeDefaults.from, rangeTo: timePickerTimeDefaults.to, - refreshPaused: timePickerRefreshIntervalDefaults.pause, - /* - * Must be replaced by timePickerRefreshIntervalDefaults.value when this issue is fixed. - * https://github.com/elastic/kibana/issues/70562 - */ - refreshInterval: 10000, }; const commonlyUsedRanges = timePickerQuickRanges.map( 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/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/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/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_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index cd125f944f8a5..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) => { 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/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/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..71e3386d821f1 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -15,7 +15,7 @@ export const ComponentStrings = { }), getTitleText: () => i18n.translate('xpack.canvas.embedObject.titleText', { - defaultMessage: 'Add from Visualize library', + defaultMessage: 'Add from Kibana', }), }, AdvancedFilter: { @@ -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: () => @@ -1301,7 +1308,7 @@ export const ComponentStrings = { }), getEmbedObjectMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { - defaultMessage: 'Add from Visualize library', + defaultMessage: 'Add from Kibana', }), getFilterMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { 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/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/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/canvas/public/components/navbar/index.js b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts similarity index 66% rename from x-pack/plugins/canvas/public/components/navbar/index.js rename to x-pack/plugins/canvas/storybook/addon/src/components/index.ts index 6948ada93155d..5acb1acf3b459 100644 --- a/x-pack/plugins/canvas/public/components/navbar/index.js +++ b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts @@ -4,7 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Navbar as Component } from './navbar'; - -export const Navbar = pure(Component); +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 982185a731b14..1321ade30bbde 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -6,236 +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: { - config: { - path: require.resolve('@kbn/optimizer/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: { - config: { - path: require.resolve('@kbn/optimizer/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 81d19c035075f..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', 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/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index faa4f2ee499e5..4fd1e889ba1a5 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -113,4 +113,19 @@ describe('ES search strategy', () => { expect(method).toBe('POST'); expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); }); + + it('sets wait_for_completion_timeout and keep_alive in the request', async () => { + mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + + const params = { index: 'foo-*', body: {} }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('transport.request'); + const { query } = mockApiCaller.mock.calls[0][1]; + expect(query).toHaveProperty('wait_for_completion_timeout'); + expect(query).toHaveProperty('keep_alive'); + }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 358335a2a4d60..1f7b6a5f9aace 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -80,8 +80,15 @@ async function asyncSearch( const method = request.id ? 'GET' : 'POST'; const path = encodeURI(request.id ? `/_async_search/${request.id}` : `/${index}/_async_search`); - // Wait up to 1s for the response to return - const query = toSnakeCase({ waitForCompletionTimeout: '100ms', ...queryParams }); + // Only report partial results every 64 shards; this should be reduced when we actually display partial results + const batchedReduceSize = request.id ? undefined : 64; + + const query = toSnakeCase({ + waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return + keepAlive: '1m', // Extend the TTL for this search request by one minute + ...(batchedReduceSize && { batchedReduceSize }), + ...queryParams, + }); const { id, response, is_partial, is_running } = (await caller( 'transport.request', 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/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx index 04642a01c15b4..ce0911666f0db 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/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 { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item'; export const InventoryAlertDropdown = () => { 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/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/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 2e06ee55189d9..83fe233553351 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -176,7 +176,7 @@ export function SavedViewsToolbarControls(props: Props) { {currentView ? currentView.name : i18n.translate('xpack.infra.savedView.unknownView', { - defaultMessage: 'No view seleted', + defaultMessage: 'No view selected', })} 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/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/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx index fe11c4cb08d13..a77de9369277b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx @@ -32,7 +32,7 @@ export const ManualInstructions: React.FunctionComponent = ({ const macOsLinuxTarCommand = `./elastic-agent enroll ${enrollArgs} ./elastic-agent run`; - const linuxDebRpmCommand = `./elastic-agent enroll ${enrollArgs} + const linuxDebRpmCommand = `elastic-agent enroll ${enrollArgs} systemctl enable elastic-agent systemctl start elastic-agent`; @@ -44,7 +44,7 @@ systemctl start elastic-agent`; diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors.ts index ee03b3faf79d1..401211409ebf7 100644 --- a/x-pack/plugins/ingest_manager/server/errors.ts +++ b/x-pack/plugins/ingest_manager/server/errors.ts @@ -15,9 +15,17 @@ export class IngestManagerError extends Error { export const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { return 502; // Bad Gateway + } + if (error instanceof PackageNotFoundError) { + return 404; + } + if (error instanceof PackageOutdatedError) { + return 400; } else { return 400; // Bad Request } }; export class RegistryError extends IngestManagerError {} +export class PackageNotFoundError extends IngestManagerError {} +export class PackageOutdatedError extends IngestManagerError {} diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 6f8c4948559d3..e2f659f54d625 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -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/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index f54e61280b98a..f47234fb20118 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -32,6 +32,7 @@ import { getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; +import { IngestManagerError, getHTTPResponseCode } from '../../errors'; export const getCategoriesHandler: RequestHandler< undefined, @@ -165,23 +166,25 @@ export const installPackageHandler: RequestHandler { }); // 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/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 878cecf644032..44e4eddfbbe6a 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -31,25 +31,38 @@ export const installPipelines = async ( // it can be created pointing to the new template, without removing the old one and effecting data // so do not remove the currently installed pipelines here const datasets = registryPackage.datasets; + if (!datasets?.length) return []; const pipelinePaths = paths.filter((path) => isPipeline(path)); - if (datasets) { - const pipelines = datasets.reduce>>((acc, dataset) => { - if (dataset.ingest_pipeline) { - acc.push( - installPipelinesForDataset({ - dataset, - callCluster, - paths: pipelinePaths, - pkgVersion: registryPackage.version, - }) - ); - } - return acc; - }, []); - const pipelinesToSave = await Promise.all(pipelines).then((results) => results.flat()); - return saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelinesToSave); - } - return []; + // get and save pipeline refs before installing pipelines + const pipelineRefs = datasets.reduce((acc, dataset) => { + const filteredPaths = pipelinePaths.filter((path) => isDatasetPipeline(path, dataset.path)); + const pipelineObjectRefs = filteredPaths.map((path) => { + const { name } = getNameAndExtension(path); + const nameForInstallation = getPipelineNameForInstallation({ + pipelineName: name, + dataset, + packageVersion: registryPackage.version, + }); + return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; + }); + acc.push(...pipelineObjectRefs); + return acc; + }, []); + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelineRefs); + const pipelines = datasets.reduce>>((acc, dataset) => { + if (dataset.ingest_pipeline) { + acc.push( + installPipelinesForDataset({ + dataset, + callCluster, + paths: pipelinePaths, + pkgVersion: registryPackage.version, + }) + ); + } + return acc; + }, []); + return await Promise.all(pipelines).then((results) => results.flat()); }; export function rewriteIngestPipeline( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index 436a6a1bdc55d..2a3120f064904 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -41,6 +41,16 @@ export const installTemplates = async ( ); // build templates per dataset from yml files const datasets = registryPackage.datasets; + if (!datasets) return []; + // get template refs to save + const installedTemplateRefs = datasets.map((dataset) => ({ + id: generateTemplateName(dataset), + type: ElasticsearchAssetType.indexTemplate, + })); + + // add package installation's references to index templates + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); + if (datasets) { const installTemplatePromises = datasets.reduce>>((acc, dataset) => { acc.push( @@ -55,14 +65,6 @@ export const installTemplates = async ( const res = await Promise.all(installTemplatePromises); const installedTemplates = res.flat(); - // get template refs to save - const installedTemplateRefs = installedTemplates.map((template) => ({ - id: template.templateName, - type: ElasticsearchAssetType.indexTemplate, - })); - - // add package installation's references to index templates - await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); return installedTemplates; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index a3fe444b19b1a..5741764164b83 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -11,14 +11,8 @@ import { } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import * as Registry from '../../registry'; -import { - AssetType, - KibanaAssetType, - AssetReference, - KibanaAssetReference, -} from '../../../../types'; -import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; -import { getInstallationObject, savedObjectTypes } from '../../packages'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { savedObjectTypes } from '../../packages'; type SavedObjectToBe = Required & { type: AssetType }; export type ArchiveAsset = Pick< @@ -28,7 +22,7 @@ export type ArchiveAsset = Pick< type: AssetType; }; -export async function getKibanaAsset(key: string) { +export async function getKibanaAsset(key: string): Promise { const buffer = Registry.getAsset(key); // cache values are buffers. convert to string / JSON @@ -51,31 +45,18 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; - paths: string[]; + kibanaAssets: ArchiveAsset[]; isUpdate: boolean; -}): Promise { - const { savedObjectsClient, paths, pkgName, isUpdate } = options; - - if (isUpdate) { - // delete currently installed kibana saved objects and installation references - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedKibanaRefs = installedPkg?.attributes.installed_kibana; - - if (installedKibanaRefs?.length) { - await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedKibanaRefs); - await deleteKibanaInstalledRefs(savedObjectsClient, pkgName, installedKibanaRefs); - } - } +}): Promise { + const { savedObjectsClient, kibanaAssets } = options; - // install the new assets and save installation references + // install the assets const kibanaAssetTypes = Object.values(KibanaAssetType); const installedAssets = await Promise.all( kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + installKibanaSavedObjects({ savedObjectsClient, assetType, kibanaAssets }) ) ); - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array return installedAssets.flat(); } export const deleteKibanaInstalledRefs = async ( @@ -92,21 +73,25 @@ export const deleteKibanaInstalledRefs = async ( installed_kibana: installedAssetsToSave, }); }; - +export async function getKibanaAssets(paths: string[]) { + const isKibanaAssetType = (path: string) => Registry.pathParts(path).type in KibanaAssetType; + const filteredPaths = paths.filter(isKibanaAssetType); + const kibanaAssets = await Promise.all(filteredPaths.map((path) => getKibanaAsset(path))); + return kibanaAssets; +} async function installKibanaSavedObjects({ savedObjectsClient, assetType, - paths, + kibanaAssets, }: { savedObjectsClient: SavedObjectsClientContract; assetType: KibanaAssetType; - paths: string[]; + kibanaAssets: ArchiveAsset[]; }) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); + const isSameType = (asset: ArchiveAsset) => assetType === asset.type; + const filteredKibanaAssets = kibanaAssets.filter((asset) => isSameType(asset)); const toBeSavedObjects = await Promise.all( - kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + filteredKibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) ); if (toBeSavedObjects.length === 0) { @@ -115,13 +100,11 @@ async function installKibanaSavedObjects({ const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { overwrite: true, }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; + return createResults.saved_objects; } } -function toAssetReference({ id, type }: SavedObject) { +export function toAssetReference({ id, type }: SavedObject) { const reference: AssetReference = { id, type: type as KibanaAssetType }; return reference; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 91e47f29bc449..46b4a7baa451e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -5,7 +5,6 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; -import Boom from 'boom'; import semver from 'semver'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { @@ -25,8 +24,15 @@ import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { installKibanaAssets } from '../kibana/assets/install'; +import { + installKibanaAssets, + getKibanaAssets, + toAssetReference, + ArchiveAsset, +} from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; +import { deleteKibanaSavedObjectsAssets } from './remove'; +import { PackageOutdatedError } from '../../../errors'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -101,7 +107,7 @@ export async function installPackage(options: { // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets const latestPackage = await Registry.fetchFindLatestPackage(pkgName); if (semver.lt(pkgVersion, latestPackage.version)) - throw Boom.badRequest('Cannot install or update to an out-of-date package'); + throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); @@ -128,12 +134,23 @@ export async function installPackage(options: { toSaveESIndexPatterns, }); } - const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); + const kibanaAssets = await getKibanaAssets(paths); + if (installedPkg) + await deleteKibanaSavedObjectsAssets( + savedObjectsClient, + installedPkg.attributes.installed_kibana + ); + // save new kibana refs before installing the assets + const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssets + ); const installKibanaAssetsPromise = installKibanaAssets({ savedObjectsClient, pkgName, - paths, + kibanaAssets, isUpdate, }); @@ -173,21 +190,14 @@ export async function installPackage(options: { ); } - // get template refs to save const installedTemplateRefs = installedTemplates.map((template) => ({ id: template.templateName, type: ElasticsearchAssetType.indexTemplate, })); - - const [installedKibanaAssets] = await Promise.all([ - installKibanaAssetsPromise, - installIndexPatternPromise, - ]); - - await saveInstalledKibanaRefs(savedObjectsClient, pkgName, installedKibanaAssets); + await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); // update to newly installed version when all assets are successfully installed if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - return [...installedKibanaAssets, ...installedPipelines, ...installedTemplateRefs]; + return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs]; } const updateVersion = async ( savedObjectsClient: SavedObjectsClientContract, @@ -234,15 +244,16 @@ export async function createInstallation(options: { return [...installedKibana, ...installedEs]; } -export const saveInstalledKibanaRefs = async ( +export const saveKibanaAssetsRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - installedAssets: KibanaAssetReference[] + kibanaAssets: ArchiveAsset[] ) => { + const assetRefs = kibanaAssets.map(toAssetReference); await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_kibana: installedAssets, + installed_kibana: assetRefs, }); - return installedAssets; + return assetRefs; }; export const saveInstalledEsRefs = async ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 81bc5847e6c0e..1acf2131dcb01 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -102,10 +102,12 @@ async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): P export async function deleteKibanaSavedObjectsAssets( savedObjectsClient: SavedObjectsClientContract, - installedObjects: AssetReference[] + installedRefs: AssetReference[] ) { + if (!installedRefs.length) return; + const logger = appContextService.getLogger(); - const deletePromises = installedObjects.map(({ id, type }) => { + const deletePromises = installedRefs.map(({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 7c1165a690860..406e58ea75f08 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -22,6 +22,7 @@ import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; +import { PackageNotFoundError } from '../../../errors'; export { ArchiveEntry } from './extract'; @@ -78,7 +79,7 @@ export async function fetchFindLatestPackage(packageName: string): Promise { 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/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/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 4692bb1db3477..0000000000000 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js +++ /dev/null @@ -1,50 +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, - setSelectedLayer, - updateFlyout, -} from '../../../actions'; -import { FLYOUT_STATE } from '../../../reducers/ui'; -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; -import { getFlyoutDisplay } from '../../../selectors/ui_selectors'; -import { - getQuery, - getRefreshConfig, - getTimeFilters, - hasDirtyState, -} from '../../../selectors/map_selectors'; - -function mapStateToProps(state = {}) { - return { - isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, - inspectorAdapters: getInspectorAdapters(state), - isSaveDisabled: hasDirtyState(state), - query: getQuery(state), - refreshConfig: getRefreshConfig(state), - timeFilters: getTimeFilters(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - closeFlyout: () => { - dispatch(setSelectedLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(removePreviewLayers()); - }, - 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 d7c754c91b89a..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 @@ -13,6 +13,7 @@ import { getQueryableUniqueIndexPatternIds, getRefreshConfig, getTimeFilters, + hasDirtyState, hasUnsavedChanges, } from '../../../selectors/map_selectors'; import { @@ -26,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), @@ -68,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 d945aa9623b21..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 @@ -9,13 +9,17 @@ import { i18n } from '@kbn/i18n'; import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; -import { getData, getCoreChrome } from '../../../kibana_services'; +import { + getData, + getCoreChrome, + getMapsCapabilities, + getNavigation, +} from '../../../kibana_services'; import { copyPersistentState } from '../../../reducers/util'; 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, @@ -27,6 +31,7 @@ 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?', @@ -58,7 +63,10 @@ export class MapsAppView extends React.Component { this._updateFromGlobalState ); - this._updateStateFromSavedQuery(this._appStateManager.getAppState().savedQuery); + const initialSavedQuery = this._appStateManager.getAppState().savedQuery; + if (initialSavedQuery) { + this._updateStateFromSavedQuery(initialSavedQuery); + } this._initMap(); @@ -237,18 +245,10 @@ export class MapsAppView extends React.Component { ); } - _onTopNavRefreshConfig = ({ isPaused, refreshInterval }) => { - this._onRefreshConfigChange({ - isPaused, - interval: refreshInterval, - }); - }; + _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(); @@ -266,7 +266,7 @@ export class MapsAppView extends React.Component { query: savedQuery.attributes.query, time: savedQuery.attributes.timefilter, }); - } + }; _initMap() { this._initMapAndLayerSettings(); @@ -295,27 +295,65 @@ export class MapsAppView extends React.Component { } _renderTopNav() { - return !this.props.isFullScreen ? ( - { - this.setState({ savedQuery: query }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + filters={this.props.filters} + query={this.props.query} + onQuerySubmit={({ dateRange, query }) => { + this._onQueryChange({ + query, + time: dateRange, + refresh: true, + }); }} - onSavedQueryUpdated={(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, + }); + }} + 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(), + }); }} - setBreadcrumbs={this._setBreadcrumbs} /> - ) : null; + ); } render() { 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 72% 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 be474b43da81a..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,109 +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, - timeFilters, - refreshConfig, - onRefreshConfigChange, - indexPatterns, - onFiltersChange, + isOpenSettingsDisabled, isSaveDisabled, closeFlyout, enableFullScreen, openMapSettings, inspectorAdapters, 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, - }); - }; - - return ( - - ); -} - -function getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs -) { return [ { id: 'full-screen', @@ -180,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, @@ -218,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/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/refresh_analytics_list_button/refresh_analytics_list_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx index f54cc4621eccf..e988640d2eae5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx @@ -15,7 +15,7 @@ export const RefreshAnalyticsListButton: FC = () => { const { refresh } = useRefreshAnalyticsList({ isLoading: setIsLoading }); return ( 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/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js index ecb8626867887..7c55ed01f432a 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export const RefreshJobsListButton = ({ onRefreshClick, isRefreshing }) => ( 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/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts new file mode 100644 index 0000000000000..10f9ebb5623df --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -0,0 +1,174 @@ +/* + * 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 { newThresholdRule } from '../objects/rule'; + +import { + CUSTOM_RULES_BTN, + RISK_SCORE, + RULE_NAME, + RULES_ROW, + RULES_TABLE, + SEVERITY, +} from '../screens/alerts_detection_rules'; +import { + ABOUT_FALSE_POSITIVES, + ABOUT_INVESTIGATION_NOTES, + ABOUT_MITRE, + ABOUT_RISK, + ABOUT_RULE_DESCRIPTION, + ABOUT_SEVERITY, + ABOUT_STEP, + ABOUT_TAGS, + ABOUT_URLS, + DEFINITION_CUSTOM_QUERY, + DEFINITION_INDEX_PATTERNS, + DEFINITION_THRESHOLD, + DEFINITION_TIMELINE, + DEFINITION_STEP, + INVESTIGATION_NOTES_MARKDOWN, + INVESTIGATION_NOTES_TOGGLE, + RULE_ABOUT_DETAILS_HEADER_TOGGLE, + RULE_NAME_HEADER, + SCHEDULE_LOOPBACK, + SCHEDULE_RUNS, + SCHEDULE_STEP, +} from '../screens/rule_details'; + +import { + goToManageAlertsDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, +} from '../tasks/alerts'; +import { + changeToThreeHundredRowsPerPage, + filterByCustomRules, + goToCreateNewRule, + goToRuleDetails, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/alerts_detection_rules'; +import { + createAndActivateRule, + fillAboutRuleAndContinue, + fillDefineThresholdRuleAndContinue, + selectThresholdRuleType, +} from '../tasks/create_new_rule'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS_URL } from '../urls/navigation'; + +describe('Detection rules, threshold', () => { + before(() => { + esArchiverLoad('timeline'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Creates and activates a new threshold rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRuleAndContinue(newThresholdRule); + fillAboutRuleAndContinue(newThresholdRule); + createAndActivateRule(); + + cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 1; + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); + + filterByCustomRules(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); + }); + cy.get(RULE_NAME).invoke('text').should('eql', newThresholdRule.name); + cy.get(RISK_SCORE).invoke('text').should('eql', newThresholdRule.riskScore); + cy.get(SEVERITY).invoke('text').should('eql', newThresholdRule.severity); + cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + let expectedUrls = ''; + newThresholdRule.referenceUrls.forEach((url) => { + expectedUrls = expectedUrls + url; + }); + let expectedFalsePositives = ''; + newThresholdRule.falsePositivesExamples.forEach((falsePositive) => { + expectedFalsePositives = expectedFalsePositives + falsePositive; + }); + let expectedTags = ''; + newThresholdRule.tags.forEach((tag) => { + expectedTags = expectedTags + tag; + }); + let expectedMitre = ''; + newThresholdRule.mitre.forEach((mitre) => { + expectedMitre = expectedMitre + mitre.tactic; + mitre.techniques.forEach((technique) => { + expectedMitre = expectedMitre + technique; + }); + }); + const expectedIndexPatterns = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ]; + + cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newThresholdRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newThresholdRule.description); + cy.get(ABOUT_STEP).eq(ABOUT_SEVERITY).invoke('text').should('eql', newThresholdRule.severity); + cy.get(ABOUT_STEP).eq(ABOUT_RISK).invoke('text').should('eql', newThresholdRule.riskScore); + cy.get(ABOUT_STEP).eq(ABOUT_URLS).invoke('text').should('eql', expectedUrls); + cy.get(ABOUT_STEP) + .eq(ABOUT_FALSE_POSITIVES) + .invoke('text') + .should('eql', expectedFalsePositives); + cy.get(ABOUT_STEP).eq(ABOUT_MITRE).invoke('text').should('eql', expectedMitre); + cy.get(ABOUT_STEP).eq(ABOUT_TAGS).invoke('text').should('eql', expectedTags); + + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); + + cy.get(DEFINITION_INDEX_PATTERNS).then((patterns) => { + cy.wrap(patterns).each((pattern, index) => { + cy.wrap(pattern).invoke('text').should('eql', expectedIndexPatterns[index]); + }); + }); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_CUSTOM_QUERY) + .invoke('text') + .should('eql', `${newThresholdRule.customQuery} `); + cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_THRESHOLD) + .invoke('text') + .should( + 'eql', + `Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}` + ); + + cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); + cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index a30fddc3c3a69..aeadc34c6e49c 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -33,6 +33,11 @@ export interface CustomRule { timelineId: string; } +export interface ThresholdRule extends CustomRule { + thresholdField: string; + threshold: string; +} + export interface MachineLearningRule { machineLearningJob: string; anomalyScoreThreshold: string; @@ -72,6 +77,22 @@ export const newRule: CustomRule = { timelineId: '0162c130-78be-11ea-9718-118a926974a4', }; +export const newThresholdRule: ThresholdRule = { + customQuery: 'host.name:*', + name: 'New Rule Test', + description: 'The new rule description.', + severity: 'High', + riskScore: '17', + tags: ['test', 'newRule'], + referenceUrls: ['https://www.google.com/', 'https://elastic.co/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [mitre1, mitre2], + note: '# test markdown', + timelineId: '0162c130-78be-11ea-9718-118a926974a4', + thresholdField: 'host.name', + threshold: '10', +}; + export const machineLearningRule: MachineLearningRule = { machineLearningJob: 'linux_anomalous_network_service', anomalyScoreThreshold: '20', diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index bc0740554bc52..af4fe7257ae5b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -27,6 +27,8 @@ export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; +export const INPUT = '[data-test-subj="input"]'; + export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; @@ -64,3 +66,9 @@ export const SEVERITY_DROPDOWN = export const TAGS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxSearchInput"]'; + +export const THRESHOLD_FIELD_SELECTION = '.euiFilterSelectItem'; + +export const THRESHOLD_INPUT_AREA = '[data-test-subj="thresholdInput"]'; + +export const THRESHOLD_TYPE = '[data-test-subj="thresholdRuleType"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index ec57e142125da..1c0102382ab6b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -26,6 +26,8 @@ export const ANOMALY_SCORE = 1; export const DEFINITION_CUSTOM_QUERY = 1; +export const DEFINITION_THRESHOLD = 4; + export const DEFINITION_TIMELINE = 3; export const DEFINITION_INDEX_PATTERNS = diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 88ae582b58891..de9d343bc91f7 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -3,7 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CustomRule, MachineLearningRule, machineLearningRule } from '../objects/rule'; + +import { + CustomRule, + MachineLearningRule, + machineLearningRule, + ThresholdRule, +} from '../objects/rule'; import { ABOUT_CONTINUE_BTN, ANOMALY_THRESHOLD_INPUT, @@ -15,6 +21,7 @@ import { DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, + INPUT, INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, @@ -30,6 +37,9 @@ import { SCHEDULE_CONTINUE_BUTTON, SEVERITY_DROPDOWN, TAGS_INPUT, + THRESHOLD_FIELD_SELECTION, + THRESHOLD_INPUT_AREA, + THRESHOLD_TYPE, } from '../screens/create_new_rule'; import { TIMELINE } from '../screens/timeline'; @@ -39,7 +49,9 @@ export const createAndActivateRule = () => { cy.get(CREATE_AND_ACTIVATE_BTN).should('not.exist'); }; -export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) => { +export const fillAboutRuleAndContinue = ( + rule: CustomRule | MachineLearningRule | ThresholdRule +) => { cy.get(RULE_NAME_INPUT).type(rule.name, { force: true }); cy.get(RULE_DESCRIPTION_INPUT).type(rule.description, { force: true }); @@ -80,18 +92,28 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); }; -export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { - cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); +export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timelineId)).click(); cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; -export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { - cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); - cy.get(TIMELINE(rule.timelineId)).click(); +export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { + const thresholdField = 0; + const threshold = 1; + + cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); + cy.get(THRESHOLD_INPUT_AREA) + .find(INPUT) + .then((inputs) => { + cy.wrap(inputs[thresholdField]).type(rule.thresholdField); + cy.get(THRESHOLD_FIELD_SELECTION).click({ force: true }); + cy.wrap(inputs[threshold]).clear().type(rule.threshold); + }); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); @@ -111,3 +133,7 @@ export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRu export const selectMachineLearningRuleType = () => { cy.get(MACHINE_LEARNING_TYPE).click({ force: true }); }; + +export const selectThresholdRuleType = () => { + cy.get(THRESHOLD_TYPE).click({ force: true }); +}; 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/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/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 84096e242cbbd..7b20873bf63cc 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -229,7 +229,11 @@ { "name": "pageInfo", "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", "ofType": null } + }, "defaultValue": null }, { @@ -10905,13 +10909,21 @@ { "name": "pageIndex", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "defaultValue": null }, { "name": "pageSize", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "defaultValue": null } ], @@ -13142,24 +13154,6 @@ "interfaces": null, "enumValues": null, "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "TemplateTimelineType", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "elastic", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null } ], "directives": [ diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 90d1b8bd54df5..f7d2c81f536be 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -102,9 +102,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex?: Maybe; + pageIndex: number; - pageSize?: Maybe; + pageSize: number; } export interface SortTimeline { @@ -423,11 +423,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2324,7 +2319,7 @@ export interface GetOneTimelineQueryArgs { id: string; } export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; 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/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 58442ab417b60..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 @@ -263,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); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx new file mode 100644 index 0000000000000..99902a31975d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx @@ -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 React from 'react'; +import { mount } from 'enzyme'; + +import { useKibana } from '../../../../common/lib/kibana'; +import '../../../../common/mock/match_media'; +import { createUseKibanaMock, TestProviders } from '../../../../common/mock'; +import { NoCases } from '.'; + +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mock; + +let navigateToApp: jest.Mock; + +describe('RecentCases', () => { + beforeEach(() => { + jest.resetAllMocks(); + navigateToApp = jest.fn(); + const kibanaMock = createUseKibanaMock()(); + useKibanaMock.mockReturnValue({ + ...kibanaMock, + services: { + application: { + navigateToApp, + getUrlForApp: jest.fn(), + }, + }, + }); + }); + + it('if no cases, you should be able to create a case by clicking on the link "start a new case"', () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="no-cases-create-case"]`).first().simulate('click'); + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { + path: + "/create?timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx index 40969a6e1df4a..875a678f32226 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx @@ -21,7 +21,7 @@ const NoCasesComponent = () => { const goToCreateCase = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { path: getCreateCaseUrl(search), }); }, @@ -30,6 +30,7 @@ const NoCasesComponent = () => { const newCaseLink = useMemo( () => ( {` ${i18n.START_A_NEW_CASE}`} diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 7a61427c56a3b..2a2354921a3d4 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -251,6 +251,20 @@ export class Simulator { return this.findInDOM('[data-test-subj="resolver:graph"]'); } + /** + * The outer panel container. + */ + public panelElement(): ReactWrapper { + return this.findInDOM('[data-test-subj="resolver:panel"]'); + } + + /** + * The panel content element (which may include tables, lists, other data depending on the view). + */ + public panelContentElement(): ReactWrapper { + return this.findInDOM('[data-test-subj^="resolver:panel:"]'); + } + /** * Like `this.wrapper.find` but only returns DOM nodes. */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 9cb900736677e..f339d128944cc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -63,6 +63,16 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( expect(simulator.processNodeElements().length).toBe(3); }); + it(`should have the default "process list" panel present`, async () => { + 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/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 084f892369b51..161a31e2ec934 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -21,6 +21,7 @@ import { EndpointAppContext } from '../../types'; import { AgentService } from '../../../../../ingest_manager/server'; import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models'; import { findAllUnenrolledAgentIds } from './support/unenroll'; +import { findAgentIDsByStatus } from './support/agent_status'; interface HitSource { _source: HostMetadata; @@ -52,6 +53,21 @@ const getLogger = (endpointAppContext: EndpointAppContext): Logger => { return endpointAppContext.logFactory.get('metadata'); }; +/* Filters that can be applied to the endpoint fetch route */ +export const endpointFilters = schema.object({ + kql: schema.nullable(schema.string()), + host_status: schema.nullable( + schema.arrayOf( + schema.oneOf([ + schema.literal(HostStatus.ONLINE.toString()), + schema.literal(HostStatus.OFFLINE.toString()), + schema.literal(HostStatus.UNENROLLING.toString()), + schema.literal(HostStatus.ERROR.toString()), + ]) + ) + ), +}); + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const logger = getLogger(endpointAppContext); router.post( @@ -76,10 +92,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp ]) ) ), - /** - * filter to be applied, it could be a kql expression or discrete filter to be implemented - */ - filter: schema.nullable(schema.oneOf([schema.string()])), + filters: endpointFilters, }) ), }, @@ -103,12 +116,21 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp context.core.savedObjects.client ); + const statusIDs = req.body?.filters?.host_status?.length + ? await findAgentIDsByStatus( + agentService, + context.core.savedObjects.client, + req.body?.filters?.host_status + ) + : undefined; + const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, metadataIndexPattern, { unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), + statusAgentIDs: statusIDs, } ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index f3b832de9a786..29624b35d5c9e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -27,7 +27,7 @@ import { HostStatus, } from '../../../../common/endpoint/types'; import { SearchResponse } from 'elasticsearch'; -import { registerEndpointRoutes } from './index'; +import { registerEndpointRoutes, endpointFilters } from './index'; import { createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -170,7 +170,7 @@ describe('test endpoint route', () => { }, ], - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); @@ -395,6 +395,53 @@ describe('test endpoint route', () => { }); }); +describe('Filters Schema Test', () => { + it('accepts a single host status', () => { + expect( + endpointFilters.validate({ + host_status: ['error'], + }) + ).toBeTruthy(); + }); + + it('accepts multiple host status filters', () => { + expect( + endpointFilters.validate({ + host_status: ['offline', 'unenrolling'], + }) + ).toBeTruthy(); + }); + + it('rejects invalid statuses', () => { + expect(() => + endpointFilters.validate({ + host_status: ['foobar'], + }) + ).toThrowError(); + }); + + it('accepts a KQL string', () => { + expect( + endpointFilters.validate({ + kql: 'whatever.field', + }) + ).toBeTruthy(); + }); + + it('accepts KQL + status', () => { + expect( + endpointFilters.validate({ + kql: 'thing.var', + host_status: ['online'], + }) + ).toBeTruthy(); + }); + + it('accepts no filters', () => { + expect(endpointFilters.validate({})).toBeTruthy(); + }); +}); + function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse { return ({ took: 15, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index 266d522e8a41d..e9eb7093a7631 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -127,7 +127,7 @@ describe('query builder', () => { it('test default query params for all endpoints metadata when body filter is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( @@ -201,7 +201,7 @@ describe('query builder', () => { const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: { - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index f6385d2710047..ba9be96201dbe 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -9,6 +9,7 @@ import { EndpointAppContext } from '../../types'; export interface QueryBuilderOptions { unenrolledAgentIds?: string[]; + statusAgentIDs?: string[]; } export async function kibanaRequestToMetadataListESQuery( @@ -22,7 +23,11 @@ export async function kibanaRequestToMetadataListESQuery( const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { - query: buildQueryBody(request, queryBuilderOptions?.unenrolledAgentIds!), + query: buildQueryBody( + request, + queryBuilderOptions?.unenrolledAgentIds!, + queryBuilderOptions?.statusAgentIDs! + ), collapse: { field: 'host.id', inner_hits: { @@ -76,47 +81,52 @@ async function getPagingProperties( function buildQueryBody( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, - unerolledAgentIds: string[] | undefined + unerolledAgentIds: string[] | undefined, + statusAgentIDs: string[] | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { - const filterUnenrolledAgents = unerolledAgentIds && unerolledAgentIds.length > 0; - if (typeof request?.body?.filter === 'string') { - const kqlQuery = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); - return { - bool: { - must: filterUnenrolledAgents - ? [ - { - bool: { - must_not: { - terms: { - 'elastic.agent.id': unerolledAgentIds, - }, - }, - }, - }, - { - ...kqlQuery, - }, - ] - : [ - { - ...kqlQuery, - }, - ], - }, - }; - } - return filterUnenrolledAgents - ? { - bool: { + const filterUnenrolledAgents = + unerolledAgentIds && unerolledAgentIds.length > 0 + ? { must_not: { terms: { 'elastic.agent.id': unerolledAgentIds, }, }, + } + : null; + const filterStatusAgents = statusAgentIDs + ? { + must: { + terms: { + 'elastic.agent.id': statusAgentIDs, + }, }, } + : null; + + const idFilter = { + bool: { + ...filterUnenrolledAgents, + ...filterStatusAgents, + }, + }; + + if (request?.body?.filters?.kql) { + const kqlQuery = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(request.body.filters.kql) + ); + const q = []; + if (filterUnenrolledAgents || filterStatusAgents) { + q.push(idFilter); + } + q.push({ ...kqlQuery }); + return { + bool: { must: q }, + }; + } + return filterUnenrolledAgents || filterStatusAgents + ? idFilter : { match_all: {}, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts new file mode 100644 index 0000000000000..a4b6b0750ec10 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { findAgentIDsByStatus } from './agent_status'; +import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { AgentService } from '../../../../../../ingest_manager/server/services'; +import { createMockAgentService } from '../../../mocks'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; +import { AgentStatusKueryHelper } from '../../../../../../ingest_manager/common/services'; + +describe('test filtering endpoint hosts by agent status', () => { + let mockSavedObjectClient: jest.Mocked; + let mockAgentService: jest.Mocked; + beforeEach(() => { + mockSavedObjectClient = savedObjectsClientMock.create(); + mockAgentService = createMockAgentService(); + }); + + it('will accept a valid status condition', async () => { + mockAgentService.listAgents.mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 0, + page: 1, + perPage: 10, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['online']); + expect(result).toBeDefined(); + }); + + it('will filter for offline hosts', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => + Promise.resolve({ + agents: [({ id: 'id1' } as unknown) as Agent, ({ id: 'id2' } as unknown) as Agent], + total: 2, + page: 1, + perPage: 2, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 2, + perPage: 2, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['offline']); + const offlineKuery = AgentStatusKueryHelper.buildKueryForOfflineAgents(); + expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect.stringContaining(offlineKuery) + ); + expect(result).toBeDefined(); + expect(result).toEqual(['id1', 'id2']); + }); + + it('will filter for multiple statuses', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => + Promise.resolve({ + agents: [({ id: 'A' } as unknown) as Agent, ({ id: 'B' } as unknown) as Agent], + total: 2, + page: 1, + perPage: 2, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 2, + perPage: 2, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, [ + 'unenrolling', + 'error', + ]); + const unenrollKuery = AgentStatusKueryHelper.buildKueryForUnenrollingAgents(); + const errorKuery = AgentStatusKueryHelper.buildKueryForErrorAgents(); + expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect.stringContaining(`${unenrollKuery} OR ${errorKuery}`) + ); + expect(result).toBeDefined(); + expect(result).toEqual(['A', 'B']); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts new file mode 100644 index 0000000000000..86f6d1a9a65e2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.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 { SavedObjectsClientContract } from 'kibana/server'; +import { AgentService } from '../../../../../../ingest_manager/server'; +import { AgentStatusKueryHelper } from '../../../../../../ingest_manager/common/services'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; +import { HostStatus } from '../../../../../common/endpoint/types'; + +const STATUS_QUERY_MAP = new Map([ + [HostStatus.ONLINE.toString(), AgentStatusKueryHelper.buildKueryForOnlineAgents()], + [HostStatus.OFFLINE.toString(), AgentStatusKueryHelper.buildKueryForOfflineAgents()], + [HostStatus.ERROR.toString(), AgentStatusKueryHelper.buildKueryForErrorAgents()], + [HostStatus.UNENROLLING.toString(), AgentStatusKueryHelper.buildKueryForUnenrollingAgents()], +]); + +export async function findAgentIDsByStatus( + agentService: AgentService, + soClient: SavedObjectsClientContract, + status: string[], + pageSize: number = 1000 +): Promise { + const helpers = status.map((s) => STATUS_QUERY_MAP.get(s)); + const searchOptions = (pageNum: number) => { + return { + page: pageNum, + perPage: pageSize, + showInactive: true, + kuery: `(fleet-agents.packages : "endpoint" AND (${helpers.join(' OR ')}))`, + }; + }; + + let page = 1; + + const result: string[] = []; + let hasMore = true; + + while (hasMore) { + const agents = await agentService.listAgents(soClient, searchOptions(page++)); + result.push(...agents.agents.map((agent: Agent) => agent.id)); + hasMore = agents.agents.length > 0; + } + return result; +} 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/graphql/timeline/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts index f4a18a40f7d4b..9bd544f6942cf 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts @@ -50,7 +50,7 @@ export const createTimelineResolvers = ( return libs.timeline.getAllTimeline( req, args.onlyUserFavorite || null, - args.pageInfo || null, + args.pageInfo, args.search || null, args.sort || null, args.status || null, diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 58a13a7115b72..573539e1bb54f 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -178,8 +178,8 @@ export const timelineSchema = gql` } input PageInfoTimeline { - pageIndex: Float - pageSize: Float + pageIndex: Float! + pageSize: Float! } enum SortFieldTimeline { @@ -316,7 +316,7 @@ export const timelineSchema = gql` extend type Query { getOneTimeline(id: ID!): TimelineResult! - getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! + getAllTimeline(pageInfo: PageInfoTimeline!, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! } extend type Mutation { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index ca0732816aa4d..fa55af351651e 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -104,9 +104,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex?: Maybe; + pageIndex: number; - pageSize?: Maybe; + pageSize: number; } export interface SortTimeline { @@ -425,11 +425,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2326,7 +2321,7 @@ export interface GetOneTimelineQueryArgs { id: string; } export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; @@ -2802,7 +2797,7 @@ export namespace QueryResolvers { TContext = SiemContext > = Resolver; export interface GetAllTimelineArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh index 3dd8e7f1097f4..f3b8a81f4086a 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh @@ -14,13 +14,13 @@ STATUS=${1:-active} TIMELINE_TYPE=${2:-default} # Example get all timelines: -# ./timelines/find_timeline_by_filter.sh active +# sh ./timelines/find_timeline_by_filter.sh active # Example get all prepackaged timeline templates: # ./timelines/find_timeline_by_filter.sh immutable template # Example get all custom timeline templates: -# ./timelines/find_timeline_by_filter.sh active template +# sh ./timelines/find_timeline_by_filter.sh active template curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh index 335d1b8c86696..05a9e0bd1ac97 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh @@ -9,28 +9,11 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_all_timelines.sh +# Example: sh ./timelines/get_all_timelines.sh + curl -s -k \ -H "Content-Type: application/json" \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST "${KIBANA_URL}${SPACE_URL}/api/solutions/security/graphql" \ - -d '{ - "operationName": "GetAllTimeline", - "variables": { - "onlyUserFavorite": false, - "pageInfo": { - "pageIndex": null, - "pageSize": null - }, - "search": "", - "sort": { - "sortField": "updated", - "sortOrder": "desc" - }, - "status": "active", - "timelineType": null - }, - "query": "query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n" -}' | jq . - + -X GET "${KIBANA_URL}${SPACE_URL}/api/timeline" \ + | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh index 0c0694c0591f9..13184ac6c6d56 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_timeline_by_id.sh {timeline_id} +# Example: sh ./timelines/get_timeline_by_id.sh {timeline_id} curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh index 36862b519130b..87eddfbe6b9d4 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} +# Example: sh ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} curl -s -k \ -H "Content-Type: application/json" \ 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/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 0b10018de5bba..245146dda183f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -1198,3 +1198,21 @@ export const mockCheckTimelinesStatusAfterInstallResult = { }, ], }; + +export const mockSavedObject = { + type: 'siem-ui-timeline', + id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + attributes: { + savedQueryId: null, + + status: 'immutable', + + excludedRowRendererIds: [], + ...mockGetTemplateTimelineValue, + }, + references: [], + updated_at: '2020-07-21T12:03:08.901Z', + version: 'WzAsMV0=', + namespaces: ['default'], + score: 0.9444616, +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index e3aeff280678f..c5d69398b7f0c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -175,11 +175,11 @@ export const cleanDraftTimelinesRequest = (timelineType: TimelineType) => }, }); -export const getTimelineByIdRequest = (query: GetTimelineByIdSchemaQuery) => +export const getTimelineRequest = (query?: GetTimelineByIdSchemaQuery) => requestMock.create({ method: 'get', path: TIMELINE_URL, - query, + query: query ?? {}, }); export const installPrepackedTimelinesRequest = () => diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts similarity index 66% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts index 30528f8563ab8..6f99739ae2e2b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts @@ -10,19 +10,24 @@ import { requestContextMock, createMockConfig, } from '../../detection_engine/routes/__mocks__'; +import { getAllTimeline } from '../saved_object'; import { mockGetCurrentUser } from './__mocks__/import_timelines'; -import { getTimelineByIdRequest } from './__mocks__/request_responses'; +import { getTimelineRequest } from './__mocks__/request_responses'; import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; -import { getTimelineByIdRoute } from './get_timeline_by_id_route'; +import { getTimelineRoute } from './get_timeline_route'; jest.mock('./utils/create_timelines', () => ({ getTimeline: jest.fn(), getTemplateTimeline: jest.fn(), })); -describe('get timeline by id', () => { +jest.mock('../saved_object', () => ({ + getAllTimeline: jest.fn(), +})); + +describe('get timeline', () => { let server: ReturnType; let securitySetup: SecurityPluginSetup; let { context } = requestContextMock.createTools(); @@ -41,15 +46,12 @@ describe('get timeline by id', () => { authz: {}, } as unknown) as SecurityPluginSetup; - getTimelineByIdRoute(server.router, createMockConfig(), securitySetup); + getTimelineRoute(server.router, createMockConfig(), securitySetup); }); test('should call getTemplateTimeline if templateTimelineId is given', async () => { const templateTimelineId = '123'; - await server.inject( - getTimelineByIdRequest({ template_timeline_id: templateTimelineId }), - context - ); + await server.inject(getTimelineRequest({ template_timeline_id: templateTimelineId }), context); expect((getTemplateTimeline as jest.Mock).mock.calls[0][1]).toEqual(templateTimelineId); }); @@ -57,8 +59,16 @@ describe('get timeline by id', () => { test('should call getTimeline if id is given', async () => { const id = '456'; - await server.inject(getTimelineByIdRequest({ id }), context); + await server.inject(getTimelineRequest({ id }), context); expect((getTimeline as jest.Mock).mock.calls[0][1]).toEqual(id); }); + + test('should call getAllTimeline if nither templateTimelineId nor id is given', async () => { + (getAllTimeline as jest.Mock).mockResolvedValue({ totalCount: 3 }); + + await server.inject(getTimelineRequest(), context); + + expect(getAllTimeline as jest.Mock).toHaveBeenCalledTimes(2); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts similarity index 67% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts index c4957b9d4b9e2..f36adb648cc03 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts @@ -17,8 +17,10 @@ import { buildSiemResponse, transformError } from '../../detection_engine/routes import { buildFrameworkRequest } from './utils/common'; import { getTimelineByIdSchemaQuery } from './schemas/get_timeline_by_id_schema'; import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { getAllTimeline } from '../saved_object'; +import { TimelineStatus } from '../../../../common/types/timeline'; -export const getTimelineByIdRoute = ( +export const getTimelineRoute = ( router: IRouter, config: ConfigType, security: SetupPlugins['security'] @@ -34,12 +36,33 @@ export const getTimelineByIdRoute = ( async (context, request, response) => { try { const frameworkRequest = await buildFrameworkRequest(context, security, request); - const { template_timeline_id: templateTimelineId, id } = request.query; + const query = request.query ?? {}; + const { template_timeline_id: templateTimelineId, id } = query; let res = null; - if (templateTimelineId != null) { + if (templateTimelineId != null && id == null) { res = await getTemplateTimeline(frameworkRequest, templateTimelineId); - } else if (id != null) { + } else if (templateTimelineId == null && id != null) { res = await getTimeline(frameworkRequest, id); + } else if (templateTimelineId == null && id == null) { + const tempResult = await getAllTimeline( + frameworkRequest, + false, + { pageSize: 1, pageIndex: 1 }, + null, + null, + TimelineStatus.active, + null + ); + + res = await getAllTimeline( + frameworkRequest, + false, + { pageSize: tempResult?.totalCount ?? 0, pageIndex: 1 }, + null, + null, + TimelineStatus.active, + null + ); } return response.ok({ body: res ?? {} }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts index 2c6098bc75500..65c956ed60440 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import * as rt from 'io-ts'; +import { unionWithNullType } from '../../../../../common/utility_types'; -export const getTimelineByIdSchemaQuery = rt.partial({ - template_timeline_id: rt.string, - id: rt.string, -}); +export const getTimelineByIdSchemaQuery = unionWithNullType( + rt.partial({ + template_timeline_id: rt.string, + id: rt.string, + }) +); export type GetTimelineByIdSchemaQuery = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts index 2ce2c37d4fa31..b5aa24336b2d7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts @@ -35,7 +35,7 @@ export const checkTimelinesStatus = async ( try { readStream = await getReadables(dataPath); - timeline = await getExistingPrepackagedTimelines(frameworkRequest, false); + timeline = await getExistingPrepackagedTimelines(frameworkRequest); } catch (err) { return { timelinesToInstall: [], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts index 6f194c3b8538e..79ebf6280a19e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts @@ -20,7 +20,7 @@ import { FrameworkRequest } from '../../../framework'; import * as noteLib from '../../../note/saved_object'; import * as pinnedEventLib from '../../../pinned_event/saved_object'; -import { getTimelines } from '../../saved_object'; +import { getSelectedTimelines } from '../../saved_object'; const getGlobalEventNotesByTimelineId = (currentNotes: NoteSavedObject[]): ExportedNotes => { const initialNotes: ExportedNotes = { @@ -55,7 +55,7 @@ const getTimelinesFromObjects = async ( request: FrameworkRequest, ids?: string[] | null ): Promise> => { - const { timelines, errors } = await getTimelines(request, ids); + const { timelines, errors } = await getSelectedTimelines(request, ids); const exportedIds = timelines.map((t) => t.savedObjectId); const [notes, pinnedEvents] = await Promise.all([ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts deleted file mode 100644 index 1dac773ad6fde..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts +++ /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 { FrameworkRequest } from '../../../framework'; -import { getTimelines as getSelectedTimelines } from '../../saved_object'; -import { TimelineSavedObject } from '../../../../../common/types/timeline'; - -export const getTimelines = async ( - frameworkRequest: FrameworkRequest, - ids: string[] -): Promise<{ timeline: TimelineSavedObject[] | null; error: string | null }> => { - try { - const timelines = await getSelectedTimelines(frameworkRequest, ids); - const existingTimelineIds = timelines.timelines.map((timeline) => timeline.savedObjectId); - const errorMsg = timelines.errors.reduce( - (acc, curr) => (acc ? `${acc}, ${curr.message}` : curr.message), - '' - ); - if (existingTimelineIds.length > 0) { - const message = existingTimelineIds.join(', '); - return { - timeline: timelines.timelines, - error: errorMsg ? `${message} found, ${errorMsg}` : null, - }; - } else { - return { timeline: null, error: errorMsg }; - } - } catch (e) { - return e.message; - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts index 996dc5823691d..1fea11f01bcc5 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts @@ -77,6 +77,24 @@ export const timelineSavedObjectOmittedFields = [ 'version', ]; +export const setTimeline = ( + parsedTimelineObject: Partial, + parsedTimeline: ImportedTimeline, + isTemplateTimeline: boolean +) => { + return { + ...parsedTimelineObject, + status: + parsedTimeline.status === TimelineStatus.draft + ? TimelineStatus.active + : parsedTimeline.status ?? TimelineStatus.active, + templateTimelineVersion: isTemplateTimeline + ? parsedTimeline.templateTimelineVersion ?? 1 + : null, + templateTimelineId: isTemplateTimeline ? parsedTimeline.templateTimelineId ?? uuid.v4() : null, + }; +}; + const CHUNK_PARSED_OBJECT_SIZE = 10; const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; @@ -151,15 +169,7 @@ export const importTimelines = async ( // create timeline / timeline template newTimeline = await createTimelines({ frameworkRequest, - timeline: { - ...parsedTimelineObject, - status: - status === TimelineStatus.draft - ? TimelineStatus.active - : status ?? TimelineStatus.active, - templateTimelineVersion: isTemplateTimeline ? templateTimelineVersion : null, - templateTimelineId: isTemplateTimeline ? templateTimelineId ?? uuid.v4() : null, - }, + timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline), pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], isImmutable, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts index 3c4343b642891..0ef83bb84c4c3 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts @@ -3,8 +3,30 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { FrameworkRequest } from '../framework'; +import { mockGetTimelineValue, mockSavedObject } from './routes/__mocks__/import_timelines'; -import { convertStringToBase64 } from './saved_object'; +import { + convertStringToBase64, + getExistingPrepackagedTimelines, + getAllTimeline, + AllTimelinesResponse, +} from './saved_object'; +import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; +import { getNotesByTimelineId } from '../note/saved_object'; +import { getAllPinnedEventsByTimelineId } from '../pinned_event/saved_object'; + +jest.mock('./convert_saved_object_to_savedtimeline', () => ({ + convertSavedObjectToSavedTimeline: jest.fn(), +})); + +jest.mock('../note/saved_object', () => ({ + getNotesByTimelineId: jest.fn().mockResolvedValue([]), +})); + +jest.mock('../pinned_event/saved_object', () => ({ + getAllPinnedEventsByTimelineId: jest.fn().mockResolvedValue([]), +})); describe('saved_object', () => { describe('convertStringToBase64', () => { @@ -22,4 +44,210 @@ describe('saved_object', () => { expect(convertStringToBase64('')).toBe(''); }); }); + + describe('getExistingPrepackagedTimelines', () => { + let mockFindSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + + beforeEach(() => { + mockFindSavedObject = jest.fn().mockResolvedValue({ saved_objects: [], total: 0 }); + mockRequest = ({ + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + find: mockFindSavedObject, + }, + }, + }, + }, + } as unknown) as FrameworkRequest; + }); + + afterEach(() => { + mockFindSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should send correct options if countsOnly is true', async () => { + const contsOnly = true; + await getExistingPrepackagedTimelines(mockRequest, contsOnly); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options if countsOnly is false', async () => { + const contsOnly = false; + await getExistingPrepackagedTimelines(mockRequest, contsOnly); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options if pageInfo is given', async () => { + const contsOnly = false; + const pageInfo = { + pageSize: 10, + pageIndex: 1, + }; + await getExistingPrepackagedTimelines(mockRequest, contsOnly, pageInfo); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 10, + type: 'siem-ui-timeline', + }); + }); + }); + + describe('getAllTimeline', () => { + let mockFindSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + const pageInfo = { + pageSize: 10, + pageIndex: 1, + }; + let result = (null as unknown) as AllTimelinesResponse; + beforeEach(async () => { + (convertSavedObjectToSavedTimeline as jest.Mock).mockReturnValue(mockGetTimelineValue); + mockFindSavedObject = jest + .fn() + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValueOnce({ saved_objects: [], total: 0 }) + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValue({ saved_objects: [], total: 0 }); + mockRequest = ({ + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + find: mockFindSavedObject, + }, + }, + }, + }, + } as unknown) as FrameworkRequest; + + result = await getAllTimeline(mockRequest, false, pageInfo, null, null, null, null); + }); + + afterEach(() => { + mockFindSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should send correct options if no filters applys', async () => { + expect(mockFindSavedObject.mock.calls[0][0]).toEqual({ + filter: 'not siem-ui-timeline.attributes.status: draft', + page: pageInfo.pageIndex, + perPage: pageInfo.pageSize, + type: 'siem-ui-timeline', + sortField: undefined, + sortOrder: undefined, + search: undefined, + searchFields: ['title', 'description'], + }); + }); + + test('should send correct options for counts of default timelines', async () => { + expect(mockFindSavedObject.mock.calls[1][0]).toEqual({ + filter: + 'not siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of timeline templates', async () => { + expect(mockFindSavedObject.mock.calls[2][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of Elastic prebuilt templates', async () => { + expect(mockFindSavedObject.mock.calls[3][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of custom templates', async () => { + expect(mockFindSavedObject.mock.calls[4][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of favorite timeline', async () => { + expect(mockFindSavedObject.mock.calls[5][0]).toEqual({ + filter: + 'not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + search: ' dXNlcm5hbWU=', + searchFields: ['title', 'description', 'favorite.keySearch'], + type: 'siem-ui-timeline', + }); + }); + + test('should call getNotesByTimelineId', async () => { + expect((getNotesByTimelineId as jest.Mock).mock.calls[0][1]).toEqual(mockSavedObject.id); + }); + + test('should call getAllPinnedEventsByTimelineId', async () => { + expect((getAllPinnedEventsByTimelineId as jest.Mock).mock.calls[0][1]).toEqual( + mockSavedObject.id + ); + }); + + test('should retuen correct result', async () => { + expect(result).toEqual({ + totalCount: 1, + customTemplateTimelineCount: 0, + defaultTimelineCount: 0, + elasticTemplateTimelineCount: 1, + favoriteCount: 0, + templateTimelineCount: 1, + timeline: [ + { + ...mockGetTimelineValue, + noteIds: [], + pinnedEventIds: [], + eventIdToNoteIds: [], + favorite: [], + notes: [], + pinnedEventsSaveObject: [], + }, + ], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts index 6bc0ca64ae33f..23ea3e6213469 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts @@ -41,7 +41,7 @@ interface ResponseTimelines { totalCount: number; } -interface AllTimelinesResponse extends ResponseTimelines { +export interface AllTimelinesResponse extends ResponseTimelines { defaultTimelineCount: number; templateTimelineCount: number; elasticTemplateTimelineCount: number; @@ -63,7 +63,7 @@ export interface Timeline { getAllTimeline: ( request: FrameworkRequest, onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, + pageInfo: PageInfoTimeline, search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, @@ -152,17 +152,18 @@ const getTimelineTypeFilter = ( export const getExistingPrepackagedTimelines = async ( request: FrameworkRequest, countsOnly?: boolean, - pageInfo?: PageInfoTimeline | null + pageInfo?: PageInfoTimeline ): Promise<{ totalCount: number; timeline: TimelineSavedObject[]; }> => { - const queryPageInfo = countsOnly - ? { - perPage: 1, - page: 1, - } - : pageInfo ?? {}; + const queryPageInfo = + countsOnly && pageInfo == null + ? { + perPage: 1, + page: 1, + } + : { perPage: pageInfo?.pageSize, page: pageInfo?.pageIndex } ?? {}; const elasticTemplateTimelineOptions = { type: timelineSavedObjectType, ...queryPageInfo, @@ -175,7 +176,7 @@ export const getExistingPrepackagedTimelines = async ( export const getAllTimeline = async ( request: FrameworkRequest, onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, + pageInfo: PageInfoTimeline, search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, @@ -183,13 +184,13 @@ export const getAllTimeline = async ( ): Promise => { const options: SavedObjectsFindOptions = { type: timelineSavedObjectType, - perPage: pageInfo?.pageSize ?? undefined, - page: pageInfo?.pageIndex ?? undefined, + perPage: pageInfo.pageSize, + page: pageInfo.pageIndex, search: search != null ? search : undefined, searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], - filter: getTimelineTypeFilter(timelineType, status), + filter: getTimelineTypeFilter(timelineType ?? null, status ?? null), sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; @@ -220,7 +221,7 @@ export const getAllTimeline = async ( searchFields: ['title', 'description', 'favorite.keySearch'], perPage: 1, page: 1, - filter: getTimelineTypeFilter(timelineType, TimelineStatus.active), + filter: getTimelineTypeFilter(timelineType ?? null, TimelineStatus.active), }; const result = await Promise.all([ @@ -496,7 +497,6 @@ const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObje ]); }) ); - return { totalCount: savedObjects.total, timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => @@ -532,14 +532,20 @@ export const timelineWithReduxProperties = ( pinnedEventsSaveObject: pinnedEvents, }); -export const getTimelines = async (request: FrameworkRequest, timelineIds?: string[] | null) => { +export const getSelectedTimelines = async ( + request: FrameworkRequest, + timelineIds?: string[] | null +) => { const savedObjectsClient = request.context.core.savedObjects.client; let exportedIds = timelineIds; if (timelineIds == null || timelineIds.length === 0) { const { timeline: savedAllTimelines } = await getAllTimeline( request, false, - null, + { + pageIndex: 1, + pageSize: timelineIds?.length ?? 0, + }, null, null, TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 37a97c03ad332..000bd875930f9 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -37,7 +37,7 @@ import { cleanDraftTimelinesRoute } from '../lib/timeline/routes/clean_draft_tim import { SetupPlugins } from '../plugin'; import { ConfigType } from '../config'; import { installPrepackedTimelinesRoute } from '../lib/timeline/routes/install_prepacked_timelines_route'; -import { getTimelineByIdRoute } from '../lib/timeline/routes/get_timeline_by_id_route'; +import { getTimelineRoute } from '../lib/timeline/routes/get_timeline_route'; export const initRoutes = ( router: IRouter, @@ -70,7 +70,7 @@ export const initRoutes = ( importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security); getDraftTimelinesRoute(router, config, security); - getTimelineByIdRoute(router, config, security); + getTimelineRoute(router, config, security); cleanDraftTimelinesRoute(router, config, security); installPrepackedTimelinesRoute(router, config, security); 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/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx index f8a1f84937326..f886b0b461482 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx @@ -20,7 +20,7 @@ export const RefreshTransformListButton: FC = ({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ff8c3186d5cf8..f13aa374e549c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11084,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": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", @@ -11669,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": "ジョブを作成", @@ -12363,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": "概要", @@ -19308,8 +19304,7 @@ "xpack.watcher.models.baseAction.simulateMessage": "アクション {id} のシミュレーションが完了しました", "xpack.watcher.models.baseAction.typeName": "アクション", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "不明なアクションタイプ {type} を作成しようとしました。", - "xpack.watcher.models.baseWatch.displayName": "新規ウォッチ", - "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", + "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", "xpack.watcher.models.baseWatch.selectMessageText": "新規ウォッチをセットアップします。", "xpack.watcher.models.baseWatch.typeName": "ウォッチ", "xpack.watcher.models.baseWatch.watchJsonPropertyMissingBadRequestMessage": "json 引数には {watchJson} プロパティが含まれている必要があります", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bcf2f6707e7bf..83e290b8c241a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11086,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": "获取现有索引模式标题时发生错误:", @@ -11672,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": "创建作业", @@ -12366,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": "概览", @@ -19315,7 +19311,6 @@ "xpack.watcher.models.baseAction.simulateMessage": "已成功模拟操作 {id}", "xpack.watcher.models.baseAction.typeName": "操作", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "尝试创建的操作类型 {type} 未知。", - "xpack.watcher.models.baseWatch.displayName": "新建监视", "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 参数必须包含 {id} 属性", "xpack.watcher.models.baseWatch.selectMessageText": "设置新监视。", "xpack.watcher.models.baseWatch.typeName": "监视", diff --git a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js index eaced3e27c8a0..6b7d693bb308e 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js +++ b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js @@ -79,11 +79,7 @@ export class BaseWatch { }; get displayName() { - if (this.isNew) { - return i18n.translate('xpack.watcher.models.baseWatch.displayName', { - defaultMessage: 'New Watch', - }); - } else if (this.name) { + if (this.name) { return this.name; } else { return this.id; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts b/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts index 2d62bca75c1a1..36dfdb55b4ab6 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts @@ -66,12 +66,19 @@ export async function saveWatch(watch: BaseWatch, toasts: ToastsSetup): Promise< try { await createWatch(watch); toasts.addSuccess( - i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { - defaultMessage: "Saved '{watchDisplayName}'", - values: { - watchDisplayName: watch.displayName, - }, - }) + watch.isNew + ? i18n.translate('xpack.watcher.sections.watchEdit.json.createSuccessNotificationText', { + defaultMessage: "Created '{watchDisplayName}'", + values: { + watchDisplayName: watch.displayName, + }, + }) + : i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { + defaultMessage: "Saved '{watchDisplayName}'", + values: { + watchDisplayName: watch.displayName, + }, + }) ); goToWatchList(); } catch (error) { 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/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/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index f452c9cce7a1a..d315f9eb77210 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -62,8 +62,15 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F return rows; } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~mlAnalyticsRefreshListButton', { timeout: 10 * 1000 }); + await testSubjects.existOrFail('mlAnalyticsRefreshListButton loaded', { timeout: 30 * 1000 }); + } + public async refreshAnalyticsTable() { - await testSubjects.click('mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForAnalyticsToLoad(); } diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index a72d9c204060b..58a1afad88e11 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -141,8 +141,15 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte }); } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~mlRefreshJobListButton', { timeout: 10 * 1000 }); + await testSubjects.existOrFail('mlRefreshJobListButton loaded', { timeout: 30 * 1000 }); + } + public async refreshJobList() { - await testSubjects.click('mlRefreshJobListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlRefreshJobListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForJobsToLoad(); } diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 453dca904b605..37d8b6e51072f 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -95,8 +95,19 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~transformRefreshTransformListButton', { + timeout: 10 * 1000, + }); + await testSubjects.existOrFail('transformRefreshTransformListButton loaded', { + timeout: 30 * 1000, + }); + } + public async refreshTransformList() { - await testSubjects.click('transformRefreshTransformListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~transformRefreshTransformListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForTransformsToLoad(); } 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/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js new file mode 100644 index 0000000000000..92b41ca4102ee --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function loadTests({ loadTestFile }) { + describe('EPM Endpoints', () => { + loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./file')); + //loadTestFile(require.resolve('./template')); + loadTestFile(require.resolve('./ilm')); + loadTestFile(require.resolve('./install_overrides')); + loadTestFile(require.resolve('./install_remove_assets')); + loadTestFile(require.resolve('./install_errors')); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts new file mode 100644 index 0000000000000..8acb11b00b57d --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + + describe('package error handling', async () => { + skipIfNoDockerRegistry(providerContext); + it('should return 404 if package does not exist', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(404); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'nonexistent', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 400 if trying to update/install to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/update-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'update', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 9ca8ebf136078..35058de0684b2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -108,6 +108,54 @@ export default function (providerContext: FtrProviderContext) { }); expect(resSearch.id).equal('sample_search'); }); + it('should have created the correct saved object', async function () { + const res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + expect(res.attributes).eql({ + installed_kibana: [ + { + id: 'sample_dashboard', + type: 'dashboard', + }, + { + id: 'sample_dashboard2', + type: 'dashboard', + }, + { + id: 'sample_search', + type: 'search', + }, + { + id: 'sample_visualization', + type: 'visualization', + }, + ], + installed_es: [ + { + id: 'logs-all_assets.test_logs-0.1.0', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs', + type: 'index_template', + }, + { + id: 'metrics-all_assets.test_metrics', + type: 'index_template', + }, + ], + es_index_patterns: { + test_logs: 'logs-all_assets.test_logs-*', + test_metrics: 'metrics-all_assets.test_metrics-*', + }, + name: 'all_assets', + version: '0.1.0', + internal: false, + removable: true, + }); + }); }); describe('uninstalls all assets when uninstalling a package', async () => { @@ -192,6 +240,18 @@ export default function (providerContext: FtrProviderContext) { } expect(resSearch.response.data.statusCode).equal(404); }); + it('should have removed the saved object', async function () { + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts index 98b26c1c04ebb..20414fcb90521 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; const listResponse = await fetchPackageList(); - expect(listResponse.response.length).to.be(13); + expect(listResponse.response.length).to.be(14); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml new file mode 100644 index 0000000000000..12a9a03c1337b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md new file mode 100644 index 0000000000000..13ef3f4fa9152 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml new file mode 100644 index 0000000000000..b12f1bfbd3b7e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: update +title: Package update test +description: This is a test package for updating a package +version: 0.1.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml new file mode 100644 index 0000000000000..12a9a03c1337b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md new file mode 100644 index 0000000000000..8e26522d86839 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml new file mode 100644 index 0000000000000..11dbdc102dce8 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: update +title: Package update test +description: This is a test package for updating a package +version: 0.2.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index d21b80bd6eed7..72121b2164bfd 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -12,12 +12,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./fleet/index')); // EPM - loadTestFile(require.resolve('./epm/list')); - loadTestFile(require.resolve('./epm/file')); - //loadTestFile(require.resolve('./epm/template')); - loadTestFile(require.resolve('./epm/ilm')); - loadTestFile(require.resolve('./epm/install_overrides')); - loadTestFile(require.resolve('./epm/install_remove_assets')); + loadTestFile(require.resolve('./epm/index')); // Package configs loadTestFile(require.resolve('./package_config/create')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 719327e5f9b79..3afa9f397a2ea 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -119,7 +119,11 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') - .send({ filter: 'not host.ip:10.46.229.234' }) + .send({ + filters: { + kql: 'not host.ip:10.46.229.234', + }, + }) .expect(200); expect(body.total).to.eql(2); expect(body.hosts.length).to.eql(2); @@ -141,7 +145,9 @@ export default function ({ getService }: FtrProviderContext) { page_index: 0, }, ], - filter: `not host.ip:${notIncludedIp}`, + filters: { + kql: `not host.ip:${notIncludedIp}`, + }, }) .expect(200); expect(body.total).to.eql(2); @@ -166,7 +172,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.os.Ext.variant:${variantValue}`, + filters: { + kql: `host.os.Ext.variant:${variantValue}`, + }, }) .expect(200); expect(body.total).to.eql(2); @@ -185,7 +193,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.ip:${targetEndpointIp}`, + filters: { + kql: `host.ip:${targetEndpointIp}`, + }, }) .expect(200); expect(body.total).to.eql(1); @@ -204,7 +214,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `not Endpoint.policy.applied.status:success`, + filters: { + kql: `not Endpoint.policy.applied.status:success`, + }, }) .expect(200); const statuses: Set = new Set( @@ -223,7 +235,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `elastic.agent.id:${targetElasticAgentId}`, + filters: { + kql: `elastic.agent.id:${targetElasticAgentId}`, + }, }) .expect(200); expect(body.total).to.eql(1); @@ -243,7 +257,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: '', + filters: { + kql: '', + }, }) .expect(200); expect(body.total).to.eql(numberOfHostsInFixture); diff --git a/yarn.lock b/yarn.lock index 4e9f732f1e0a0..a70f04e030447 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4808,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== @@ -5726,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== @@ -11933,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" @@ -19267,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"