diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy new file mode 100644 index 0000000000000..5cf6efe324ac3 --- /dev/null +++ b/.ci/end2end.groovy @@ -0,0 +1,125 @@ +#!/usr/bin/env groovy + +library identifier: 'apm@current', +retriever: modernSCM( + [$class: 'GitSCMSource', + credentialsId: 'f94e9298-83ae-417e-ba91-85c279771570', + id: '37cf2c00-2cc7-482e-8c62-7bbffef475e2', + remote: 'git@github.com:elastic/apm-pipeline-library.git']) + +pipeline { + agent { label 'linux && immutable' } + environment { + BASE_DIR = 'src/github.com/elastic/kibana' + HOME = "${env.WORKSPACE}" + APM_ITS = 'apm-integration-testing' + CYPRESS_DIR = 'x-pack/legacy/plugins/apm/cypress' + PIPELINE_LOG_LEVEL = 'DEBUG' + } + options { + timeout(time: 1, unit: 'HOURS') + buildDiscarder(logRotator(numToKeepStr: '40', artifactNumToKeepStr: '20', daysToKeepStr: '30')) + timestamps() + ansiColor('xterm') + disableResume() + durabilityHint('PERFORMANCE_OPTIMIZED') + } + triggers { + issueCommentTrigger('(?i).*jenkins\\W+run\\W+(?:the\\W+)?e2e(?:\\W+please)?.*') + } + parameters { + booleanParam(name: 'FORCE', defaultValue: false, description: 'Whether to force the run.') + } + stages { + stage('Checkout') { + options { skipDefaultCheckout() } + steps { + deleteDir() + gitCheckout(basedir: "${BASE_DIR}", githubNotifyFirstTimeContributor: false, + shallow: false, reference: "/var/lib/jenkins/.git-references/kibana.git") + script { + dir("${BASE_DIR}"){ + def regexps =[ "^x-pack/legacy/plugins/apm/.*" ] + env.APM_UPDATED = isGitRegionMatch(patterns: regexps) + } + } + dir("${APM_ITS}"){ + git changelog: false, + credentialsId: 'f6c7695a-671e-4f4f-a331-acdce44ff9ba', + poll: false, + url: "git@github.com:elastic/${APM_ITS}.git" + } + } + } + stage('Start services') { + options { skipDefaultCheckout() } + when { + anyOf { + expression { return params.FORCE } + expression { return env.APM_UPDATED != "false" } + } + } + steps { + dir("${APM_ITS}"){ + sh './scripts/compose.py start master --no-kibana --no-xpack-secure' + } + } + } + stage('Prepare Kibana') { + options { skipDefaultCheckout() } + when { + anyOf { + expression { return params.FORCE } + expression { return env.APM_UPDATED != "false" } + } + } + environment { + JENKINS_NODE_COOKIE = 'dontKillMe' + } + steps { + dir("${BASE_DIR}"){ + sh script: "${CYPRESS_DIR}/ci/prepare-kibana.sh" + } + } + } + stage('Smoke Tests'){ + options { skipDefaultCheckout() } + when { + anyOf { + expression { return params.FORCE } + expression { return env.APM_UPDATED != "false" } + } + } + steps{ + dir("${BASE_DIR}"){ + sh ''' + jobs -l + docker build --tag cypress ${CYPRESS_DIR}/ci + docker run --rm -t --user "$(id -u):$(id -g)" \ + -v `pwd`:/app --network="host" \ + --name cypress cypress''' + } + } + post { + always { + dir("${BASE_DIR}"){ + archiveArtifacts(allowEmptyArchive: false, artifacts: "${CYPRESS_DIR}/screenshots/**,${CYPRESS_DIR}/videos/**,${CYPRESS_DIR}/*e2e-tests.xml") + junit(allowEmptyResults: true, testResults: "${CYPRESS_DIR}/*e2e-tests.xml") + } + dir("${APM_ITS}"){ + sh 'docker-compose logs > apm-its.log || true' + sh 'docker-compose down -v || true' + archiveArtifacts(allowEmptyArchive: false, artifacts: 'apm-its.log') + } + } + } + } + } + post { + always { + dir("${BASE_DIR}"){ + archiveArtifacts(allowEmptyArchive: true, artifacts: "${CYPRESS_DIR}/ingest-data.log,kibana.log") + } + } + } +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 610681e83798e..c5e6768c17d46 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,7 @@ # App /x-pack/legacy/plugins/lens/ @elastic/kibana-app /x-pack/legacy/plugins/graph/ @elastic/kibana-app +/src/legacy/server/sample_data/ @elastic/kibana-app # App Architecture /src/plugins/data/ @elastic/kibana-app-arch @@ -66,14 +67,25 @@ /packages/kbn-es/ @elastic/kibana-operations /packages/kbn-pm/ @elastic/kibana-operations /packages/kbn-test/ @elastic/kibana-operations +/src/legacy/server/keystore/ @elastic/kibana-operations +/src/legacy/server/pid/ @elastic/kibana-operations +/src/legacy/server/sass/ @elastic/kibana-operations +/src/legacy/server/utils/ @elastic/kibana-operations +/src/legacy/server/warnings/ @elastic/kibana-operations # Platform /src/core/ @elastic/kibana-platform -/src/legacy/server/saved_objects/ @elastic/kibana-platform /config/kibana.yml @elastic/kibana-platform /x-pack/plugins/features/ @elastic/kibana-platform /x-pack/plugins/licensing/ @elastic/kibana-platform /packages/kbn-config-schema/ @elastic/kibana-platform +/src/legacy/server/config/ @elastic/kibana-platform +/src/legacy/server/csp/ @elastic/kibana-platform +/src/legacy/server/http/ @elastic/kibana-platform +/src/legacy/server/i18n/ @elastic/kibana-platform +/src/legacy/server/logging/ @elastic/kibana-platform +/src/legacy/server/saved_objects/ @elastic/kibana-platform +/src/legacy/server/status/ @elastic/kibana-platform # Security /x-pack/legacy/plugins/security/ @elastic/kibana-security diff --git a/.i18nrc.json b/.i18nrc.json index e5ba6762da154..fac9b9ce53184 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -19,6 +19,7 @@ "kbnVislibVisTypes": "src/legacy/core_plugins/kbn_vislib_vis_types", "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", + "kibana_utils": "src/plugins/kibana_utils", "navigation": "src/legacy/core_plugins/navigation", "newsfeed": "src/plugins/newsfeed", "regionMap": "src/legacy/core_plugins/region_map", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53e44fbede724..599cf26970030 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -387,6 +387,14 @@ To activate the APM agent, use the [`active`](https://www.elastic.co/guide/en/ap All config options can be set either via environment variables, or by creating an appropriate config file under `config/apm.dev.js`. For more information about configuring the APM agent, please refer to [the documentation](https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuring-the-agent.html). +Example `config/apm.dev.js` file: + +```js +module.exports = { + active: true, +}; +``` + Once the agent is active, it will trace all incoming HTTP requests to Kibana, monitor for errors, and collect process-level metrics. The collected data will be sent to the APM Server and is viewable in the APM UI in Kibana. @@ -405,7 +413,7 @@ The following table outlines possible test file locations and how to invoke them | Jest | `src/**/*.test.js`
`src/**/*.test.ts` | `node scripts/jest -t regexp [test path]` | | Jest (integration) | `**/integration_tests/**/*.test.js` | `node scripts/jest_integration -t regexp [test path]` | | Mocha | `src/**/__tests__/**/*.js`
`!src/**/public/__tests__/*.js`
`packages/kbn-datemath/test/**/*.js`
`packages/kbn-dev-utils/src/**/__tests__/**/*.js`
`tasks/**/__tests__/**/*.js` | `node scripts/mocha --grep=regexp [test path]` | -| Functional | `test/*integration/**/config.js`
`test/*functional/**/config.js` | `node scripts/functional_tests_server --config test/[directory]/config.js`
`node scripts/functional_test_runner --config test/[directory]/config.js --grep=regexp` | +| Functional | `test/*integration/**/config.js`
`test/*functional/**/config.js`
`test/accessibility/config.js` | `node scripts/functional_tests_server --config test/[directory]/config.js`
`node scripts/functional_test_runner --config test/[directory]/config.js --grep=regexp` | | Karma | `src/**/public/__tests__/*.js` | `npm run test:dev` | For X-Pack tests located in `x-pack/` see [X-Pack Testing](x-pack/README.md#testing) diff --git a/docs/developer/core/development-functional-tests.asciidoc b/docs/developer/core/development-functional-tests.asciidoc index 6d2c5a72f0532..350a3c2a997cf 100644 --- a/docs/developer/core/development-functional-tests.asciidoc +++ b/docs/developer/core/development-functional-tests.asciidoc @@ -98,6 +98,7 @@ When run without any arguments the `FunctionalTestRunner` automatically loads th * `--config test/functional/config.js` starts Elasticsearch and Kibana servers with the WebDriver tests configured to run in Chrome. * `--config test/functional/config.firefox.js` starts Elasticsearch and Kibana servers with the WebDriver tests configured to run in Firefox. * `--config test/api_integration/config.js` starts Elasticsearch and Kibana servers with the api integration tests configuration. +* `--config test/accessibility/config.ts` starts Elasticsearch and Kibana servers with the WebDriver tests configured to run an accessibility audit using https://www.deque.com/axe/[axe]. There are also command line flags for `--bail` and `--grep`, which behave just like their mocha counterparts. For instance, use `--grep=foo` to run only tests that match a regular expression. @@ -362,7 +363,7 @@ Full list of services that are used in functional tests can be found here: {blob ** Source: {blob}test/functional/services/remote/remote.ts[test/functional/services/remote/remote.ts] ** Instance of https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html[WebDriver] class ** Responsible for all communication with the browser -** To perform browser actions, use `remote` service +** To perform browser actions, use `remote` service ** For searching and manipulating with DOM elements, use `testSubjects` and `find` services ** See the https://seleniumhq.github.io/selenium/docs/api/javascript/[selenium-webdriver docs] for the full API. diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md index 960d610b589b8..e01a5e9da50d5 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md @@ -17,6 +17,7 @@ core: { i18n: I18nStart; notifications: NotificationsStart; overlays: OverlayStart; + savedObjects: SavedObjectsStart; uiSettings: IUiSettingsClient; injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.md index e12121e0e3ebb..68a1c27b11836 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.md @@ -16,5 +16,5 @@ export interface AppMountContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-public.appmountcontext.core.md) | {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
uiSettings: IUiSettingsClient;
injectedMetadata: {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
};
} | Core service APIs available to mounted applications. | +| [core](./kibana-plugin-public.appmountcontext.core.md) | {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
savedObjects: SavedObjectsStart;
uiSettings: IUiSettingsClient;
injectedMetadata: {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
};
} | Core service APIs available to mounted applications. | diff --git a/docs/development/core/public/kibana-plugin-public.chromestart.md b/docs/development/core/public/kibana-plugin-public.chromestart.md index 153e06d591404..d5d99f3d5be65 100644 --- a/docs/development/core/public/kibana-plugin-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-public.chromestart.md @@ -39,6 +39,7 @@ export interface ChromeStart | [setBrand(brand)](./kibana-plugin-public.chromestart.setbrand.md) | Set the brand configuration. | | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | | [setHelpExtension(helpExtension)](./kibana-plugin-public.chromestart.sethelpextension.md) | Override the current set of custom help content | +| [setHelpSupportUrl(url)](./kibana-plugin-public.chromestart.sethelpsupporturl.md) | Override the default support URL shown in the help menu | | [setIsCollapsed(isCollapsed)](./kibana-plugin-public.chromestart.setiscollapsed.md) | Set the collapsed state of the chrome navigation. | | [setIsVisible(isVisible)](./kibana-plugin-public.chromestart.setisvisible.md) | Set the temporary visibility for the chrome. This does nothing if the chrome is hidden by default and should be used to hide the chrome for things like full-screen modes with an exit button. | diff --git a/docs/development/core/public/kibana-plugin-public.chromestart.sethelpsupporturl.md b/docs/development/core/public/kibana-plugin-public.chromestart.sethelpsupporturl.md new file mode 100644 index 0000000000000..975283ce59cb7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromestart.sethelpsupporturl.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeStart](./kibana-plugin-public.chromestart.md) > [setHelpSupportUrl](./kibana-plugin-public.chromestart.sethelpsupporturl.md) + +## ChromeStart.setHelpSupportUrl() method + +Override the default support URL shown in the help menu + +Signature: + +```typescript +setHelpSupportUrl(url: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| url | string | | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index cecceb04240e6..1ce18834f5319 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index c4ceb47f66e1b..6033c667c1866 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index dba0ad8c8560c..25eebf1c06d01 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -23,6 +23,7 @@ export interface HttpServiceSetup | [registerAuth](./kibana-plugin-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | | [registerOnPostAuth](./kibana-plugin-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. | | [registerOnPreAuth](./kibana-plugin-server.httpservicesetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests. | +| [registerOnPreResponse](./kibana-plugin-server.httpservicesetup.registeronpreresponse.md) | (handler: OnPreResponseHandler) => void | To define custom logic to perform for the server response. | | [registerRouteHandlerContext](./kibana-plugin-server.httpservicesetup.registerroutehandlercontext.md) | <T extends keyof RequestHandlerContext>(contextName: T, provider: RequestHandlerContextProvider<T>) => RequestHandlerContextContainer | Register a context provider for a route handler. | ## Example diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md new file mode 100644 index 0000000000000..9f0eaae8830e1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [registerOnPreResponse](./kibana-plugin-server.httpservicesetup.registeronpreresponse.md) + +## HttpServiceSetup.registerOnPreResponse property + +To define custom logic to perform for the server response. + +Signature: + +```typescript +registerOnPreResponse: (handler: OnPreResponseHandler) => void; +``` + +## Remarks + +Doesn't provide the whole response object. Supports extending response with custom headers. See [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md). + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 9144742c9bb73..fceabd1237665 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -77,6 +77,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | | [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | +| [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) | Additional data to extend a response. | +| [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) | Response status code. | +| [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration schema and capabilities. | @@ -173,6 +176,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [MutatingOperationRefreshSetting](./kibana-plugin-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md). | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | +| [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | | [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | Dedicated type for plugin configuration schema. | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md new file mode 100644 index 0000000000000..8736020daf063 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) > [headers](./kibana-plugin-server.onpreresponseextensions.headers.md) + +## OnPreResponseExtensions.headers property + +additional headers to attach to the response + +Signature: + +```typescript +headers?: ResponseHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md new file mode 100644 index 0000000000000..e5aa624c39909 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) + +## OnPreResponseExtensions interface + +Additional data to extend a response. + +Signature: + +```typescript +export interface OnPreResponseExtensions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.onpreresponseextensions.headers.md) | ResponseHeaders | additional headers to attach to the response | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md b/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md new file mode 100644 index 0000000000000..082de0a9b4aeb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md) + +## OnPreResponseHandler type + +See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). + +Signature: + +```typescript +export declare type OnPreResponseHandler = (request: KibanaRequest, preResponse: OnPreResponseInfo, toolkit: OnPreResponseToolkit) => OnPreResponseResult | Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md new file mode 100644 index 0000000000000..736b4298037cf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) + +## OnPreResponseInfo interface + +Response status code. + +Signature: + +```typescript +export interface OnPreResponseInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [statusCode](./kibana-plugin-server.onpreresponseinfo.statuscode.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md new file mode 100644 index 0000000000000..4fd4529dc400f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) > [statusCode](./kibana-plugin-server.onpreresponseinfo.statuscode.md) + +## OnPreResponseInfo.statusCode property + +Signature: + +```typescript +statusCode: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md new file mode 100644 index 0000000000000..5525f5bf60284 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) + +## OnPreResponseToolkit interface + +A tool set defining an outcome of OnPreAuth interceptor for incoming request. + +Signature: + +```typescript +export interface OnPreResponseToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [next](./kibana-plugin-server.onpreresponsetoolkit.next.md) | (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult | To pass request to the next handler | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md new file mode 100644 index 0000000000000..bfb5827b16b2f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) > [next](./kibana-plugin-server.onpreresponsetoolkit.next.md) + +## OnPreResponseToolkit.next property + +To pass request to the next handler + +Signature: + +```typescript +next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md index 33a26eef8a7cb..56d064dcb290e 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md @@ -8,6 +8,9 @@ ```typescript config: { + legacy: { + globalConfig$: Observable; + }; create: () => Observable; createIfExists: () => Observable; }; diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md index e7aa32edaa293..c2fadfb779fc9 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md @@ -16,7 +16,7 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | -| [config](./kibana-plugin-server.plugininitializercontext.config.md) | {
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
} | | +| [config](./kibana-plugin-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
};
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
} | | | [env](./kibana-plugin-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
} | | | [logger](./kibana-plugin-server.plugininitializercontext.logger.md) | LoggerFactory | | | [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | diff --git a/docs/management/field-formatters/url-formatter.asciidoc b/docs/management/field-formatters/url-formatter.asciidoc index 2cbade4431202..41d4f75603dc6 100644 --- a/docs/management/field-formatters/url-formatter.asciidoc +++ b/docs/management/field-formatters/url-formatter.asciidoc @@ -4,6 +4,8 @@ The `Url` field formatter can take on the following types: * The *Image* type can be used to specify an image directory where a specified image is located. * The *Audio* type can be used to specify an audio directory where a specified audio file is located. +For an *Image* type you can specify width and height attributes. These will be used to set the max width / max height of the image, while keeping the aspect ratio. Image will not be upscaled if it's smaller than the provided size parameters. + You can customize either type of URL field formats with templates. A _URL template_ enables you to add specific values to a partial URL. Use the string `{{value}}` to add the contents of the field to a fixed URL. diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc index c983fa2cfe4ba..37fbc6d78c8e4 100644 --- a/docs/maps/connect-to-ems.asciidoc +++ b/docs/maps/connect-to-ems.asciidoc @@ -13,11 +13,20 @@ EMS requests are made to the following domains: * vector.maps.elastic.co **Elastic Maps** makes requests directly from the browser to EMS. -To proxy EMS requests through the Kibana server, set `map.proxyElasticMapsServiceInMaps` to `true` in your <> file. + +[float] +=== Connect to Elastic Maps Service from an internal network + +To connect to EMS when your Kibana server and browser are in an internal network: + +. Set `map.proxyElasticMapsServiceInMaps` to `true` in your <> file to proxy EMS requests through the Kibana server. +. Update your firewall rules to whitelist connections from your Kibana server to the EMS domains listed above. + +NOTE: Coordinate map and region map visualizations do not support `map.proxyElasticMapsServiceInMaps` and will not proxy EMS requests through the Kibana server. [float] -=== Disabling Elastic Maps Service +=== Disable Elastic Maps Service You might experience EMS connection issues if your Kibana server or browser are on a private network or behind a firewall. If this happens, you can disable the EMS connection to avoid unnecessary EMS requests. diff --git a/docs/user/security/images/role-index-privilege.png b/docs/user/security/images/role-index-privilege.png new file mode 100644 index 0000000000000..1dc1ae640e3ba Binary files /dev/null and b/docs/user/security/images/role-index-privilege.png differ diff --git a/docs/user/security/images/role-management.png b/docs/user/security/images/role-management.png new file mode 100644 index 0000000000000..2a78c69a5e352 Binary files /dev/null and b/docs/user/security/images/role-management.png differ diff --git a/docs/user/security/images/role-new-user.png b/docs/user/security/images/role-new-user.png new file mode 100644 index 0000000000000..0e8d75421cca3 Binary files /dev/null and b/docs/user/security/images/role-new-user.png differ diff --git a/docs/user/security/images/role-space-visualization.png b/docs/user/security/images/role-space-visualization.png new file mode 100644 index 0000000000000..746af89c66e85 Binary files /dev/null and b/docs/user/security/images/role-space-visualization.png differ diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index f57d1bcd3bc2a..eab3833b3f5ae 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -37,4 +37,4 @@ cause Kibana's authorization to behave unexpectedly. include::authorization/index.asciidoc[] include::authorization/kibana-privileges.asciidoc[] include::api-keys/index.asciidoc[] - +include::rbac_tutorial.asciidoc[] diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc new file mode 100644 index 0000000000000..e4dbdc2483f70 --- /dev/null +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -0,0 +1,104 @@ +[[space-rbac-tutorial]] +=== Tutorial: Use role-based access control to customize Kibana spaces + +With role-based access control (RBAC), you can provide users access to data, tools, +and Kibana spaces. In this tutorial, you will learn how to configure roles +that provide the right users with the right access to the data, tools, and +Kibana spaces. + +[float] +==== Scenario + +Our user is a web developer working on a bank's +online mortgage service. The web developer has these +three requirements: + +* Have access to the data for that service +* Build visualizations and dashboards +* Monitor the performance of the system + +You'll provide the web developer with the access and privileges to get the job done. + +[float] +==== Prerequisites + +To complete this tutorial, you'll need the following: + +* **Administrative privileges**: You must have a role that grants privileges to create a space, role, and user. This is any role which grants the `manage_security` cluster privilege. By default, the `superuser` role provides this access. See the {ref}/built-in-roles.html[built-in] roles. +* **A space**: In this tutorial, use `Dev Mortgage` as the space +name. See <> for +details on creating a space. +* **Data**: You can use <> or +live data. In the steps below, Filebeat and Metricbeat data are used. + +[float] +==== Steps + +With the requirements in mind, here are the steps that you will work +through in this tutorial: + +* Create a role named `mortgage-developer` +* Give the role permission to access the data in the relevant indices +* Give the role permission to create visualizations and dashboards +* Create the web developer's user account with the proper roles + +[float] +==== Create a role + +Go to **Management > Roles** +for an overview of your roles. This view provides actions +for you to create, edit, and delete roles. + +[role="screenshot"] +image::security/images/role-management.png["Role management"] + + +You can create as many roles as you like. Click *Create role* and +provide a name. Use `dev-mortgage` because this role is for a developer +working on the bank's mortgage application. + + +[float] +==== Give the role permission to access the data + +Access to data in indices is an index-level privilege, so in +*Index privileges*, add lines for the indices that contain the +data for this role. Two privileges are required: `read` and +`view_index_metadata`. All privileges are detailed in the +https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html[security privileges] documentation. + +In the screenshots, Filebeat and Metricbeat data is used, but you +should use the index patterns for your indices. + +[role="screenshot"] +image::security/images/role-index-privilege.png["Index privilege"] + +[float] +==== Give the role permission to create visualizations and dashboards + +By default, roles do not give Kibana privileges. Click **Add space +privilege** and associate this role with the `Dev Mortgage` space. + +To enable users with the `dev-mortgage` role to create visualizations +and dashboards, click *All* for *Visualize* and *Dashboard*. Also +assign *All* for *Discover* because it is common for developers +to create saved searches while designing visualizations. + +[role="screenshot"] +image::security/images/role-space-visualization.png["Associate space"] + +[float] +==== Create the developer's user account with the proper roles + +Go to **Management > Users** and click on **Create user** to create a +user. Give the user the `dev-mortgage` role +and the `monitoring-user` role, which is required for users of **Stack Monitoring**. + +[role="screenshot"] +image::security/images/role-new-user.png["Developer user"] + +Finally, have the developer log in and access the Dev Mortgage space +and create a new visualization. + +NOTE: If the user is assigned to only one space, they will automatically enter that space on login. + diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc index 7452f1c4c3d7e..e9085d18185ec 100644 --- a/docs/visualize/most-frequent.asciidoc +++ b/docs/visualize/most-frequent.asciidoc @@ -7,20 +7,19 @@ levels of {es} {ref}/search-aggregations-bucket.html[bucket] aggregations. The most frequently used visualizations include: -* Line, Area and Bar charts +* Line, area, and bar charts * Pie charts -* Data table -* Metric visualization -* Goal and Gauge visualization +* Data tables +* Metric, goals, and gauges * Heat maps -* Tag cloud +* Tag clouds [float] === Configure your visualization -You configure visualizations using the default editor, which is broken into *Metrics* and *Buckets*, and includes a default count +You configure visualizations using the default editor, which is broken into metrics and buckets, and includes a default count metric. Each visualization supports different configurations for what the metrics and buckets -represent. For example, a Bar chart allows you to add an X-axis: +represent. For example, a bar chart allows you to add an X-axis: [role="screenshot"] image::images/add-bucket.png["",height=478] diff --git a/package.json b/package.json index 1ff42384f95d5..cf36d4ce884ac 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ "**/image-diff/gm/debug": "^2.6.9", "**/react-dom": "^16.12.0", "**/react-test-renderer": "^16.12.0", - "**/deepmerge": "^4.2.2" + "**/deepmerge": "^4.2.2", + "**/serialize-javascript": "^2.1.1" }, "workspaces": { "packages": [ @@ -112,7 +113,7 @@ "@elastic/charts": "^14.0.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", - "@elastic/eui": "16.0.0", + "@elastic/eui": "17.0.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -165,6 +166,7 @@ "encode-uri-query": "1.0.1", "execa": "^3.2.0", "expiry-js": "0.1.7", + "fast-deep-equal": "^3.1.1", "file-loader": "4.2.0", "font-awesome": "4.7.0", "getos": "^3.1.0", @@ -228,6 +230,7 @@ "react-resize-detector": "^4.2.0", "react-router-dom": "^4.3.1", "react-sizeme": "^2.3.6", + "react-use": "^13.10.2", "reactcss": "1.2.3", "redux": "4.0.0", "redux-actions": "2.2.1", diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 8c5358c82208d..0cc54fa2a64c4 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -15,7 +15,8 @@ "@kbn/dev-utils": "1.0.0", "@types/parse-link-header": "^1.0.0", "@types/strip-ansi": "^5.2.1", - "@types/xml2js": "^0.4.5" + "@types/xml2js": "^0.4.5", + "diff": "^4.0.1" }, "dependencies": { "chalk": "^2.4.2", diff --git a/packages/kbn-test/src/failed_tests_reporter/README.md b/packages/kbn-test/src/failed_tests_reporter/README.md index 1af309ba525b6..20592ecd733b6 100644 --- a/packages/kbn-test/src/failed_tests_reporter/README.md +++ b/packages/kbn-test/src/failed_tests_reporter/README.md @@ -12,10 +12,10 @@ copy(`wget "${Array.from($$('a[href$=".xml"]')).filter(a => a.innerText === 'Dow This copies a script to download the reports, which you should execute in the `test/junit` directory. -Next, run the CLI in `--dry-run` mode so that it doesn't actually communicate with Github. +Next, run the CLI in `--no-github-update` mode so that it doesn't actually communicate with Github and `--no-report-update` to prevent the script from mutating the reports on disk and instead log the updated report. ```sh -node scripts/report_failed_tests.js --verbose --dry-run --build-url foo +node scripts/report_failed_tests.js --verbose --no-github-update --no-report-update ``` -If you specify the `GITHUB_TOKEN` environment variable then `--dry-run` will execute read operations but still won't execute write operations. \ No newline at end of file +Unless you specify the `GITHUB_TOKEN` environment variable requests to read existing issues will use anonymous access which is limited to 60 requests per hour. \ No newline at end of file diff --git a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts new file mode 100644 index 0000000000000..02b6b5f064218 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +const Fs = jest.requireActual('fs'); + +export const FTR_REPORT = Fs.readFileSync(require.resolve('./ftr_report.xml'), 'utf8'); +export const JEST_REPORT = Fs.readFileSync(require.resolve('./jest_report.xml'), 'utf8'); +export const KARMA_REPORT = Fs.readFileSync(require.resolve('./karma_report.xml'), 'utf8'); +export const MOCHA_REPORT = Fs.readFileSync(require.resolve('./mocha_report.xml'), 'utf8'); diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts new file mode 100644 index 0000000000000..9e800e88bc9ba --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts @@ -0,0 +1,350 @@ +/* + * 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 Path from 'path'; + +import { ToolingLog } from '@kbn/dev-utils'; +// @ts-ignore +import { createPatch } from 'diff'; + +// turns out Jest can't encode xml diffs in their JUnit reports... +expect.addSnapshotSerializer({ + test: v => typeof v === 'string' && (v.includes('<') || v.includes('>')), + print: v => + v + .replace(//g, '›') + .replace(/^\s+$/gm, ''), +}); + +jest.mock('fs', () => { + const realFs = jest.requireActual('fs'); + return { + readFile: realFs.read, + writeFile: (...args: any[]) => { + setTimeout(args[args.length - 1], 0); + }, + }; +}); + +import { FTR_REPORT, JEST_REPORT, MOCHA_REPORT, KARMA_REPORT } from './__fixtures__'; +import { parseTestReport } from './test_report'; +import { addMessagesToReport } from './add_messages_to_report'; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +const log = new ToolingLog(); + +it('rewrites ftr reports with minimal changes', async () => { + const xml = await addMessagesToReport({ + report: await parseTestReport(FTR_REPORT), + messages: [ + { + name: 'maps app maps loaded from sample data ecommerce "before all" hook', + classname: + 'Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps/sample_data·js', + message: 'foo bar', + }, + ], + log, + reportPath: Path.resolve(__dirname, './__fixtures__/ftr_report.xml'), + }); + + expect(createPatch('ftr.xml', FTR_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(` + Index: ftr.xml + =================================================================== + --- ftr.xml [object Object] + +++ ftr.xml + @@ -2,52 +2,56 @@ + ‹testsuites› + ‹testsuite timestamp="2019-06-05T23:37:10" time="903.670" tests="129" failures="5" skipped="71"› + ‹testcase name="maps app maps loaded from sample data ecommerce "before all" hook" classname="Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps/sample_data·js" time="154.378"› + ‹system-out› + - ‹![CDATA[[00:00:00] │ + + [00:00:00] │ + [00:07:04] └-: maps app + ... + [00:15:02] │ + -]]› + + + ‹/system-out› + ‹failure› + - ‹![CDATA[Error: retry.try timeout: TimeoutError: Waiting for element to be located By(css selector, [data-test-subj~="layerTocActionsPanelToggleButtonRoad_Map_-_Bright"]) + + Error: retry.try timeout: TimeoutError: Waiting for element to be located By(css selector, [data-test-subj~="layerTocActionsPanelToggleButtonRoad_Map_-_Bright"]) + Wait timed out after 10055ms + at /var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/webdriver.js:834:17 + at process._tickCallback (internal/process/next_tick.js:68:7) + at lastError (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:28:9) + - at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13)]]› + - ‹/failure› + + at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13) + + + + + +Failed Tests Reporter: + + - foo bar + +‹/failure› + ‹/testcase› + ‹testcase name="maps app "after all" hook" classname="Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps" time="0.179"› + ‹system-out› + - ‹![CDATA[[00:00:00] │ + + [00:00:00] │ + [00:07:04] └-: maps app + ... + -]]› + + + ‹/system-out› + ‹failure› + - ‹![CDATA[{ NoSuchSessionError: This driver instance does not have a valid session ID (did you call WebDriver.quit()?) and may no longer be used. + + { NoSuchSessionError: This driver instance does not have a valid session ID (did you call WebDriver.quit()?) and may no longer be used. + at promise.finally (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/webdriver.js:726:38) + at Object.thenFinally [as finally] (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/promise.js:124:12) + - at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' }]]› + + at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' } + ‹/failure› + ‹/testcase› + ‹testcase name="InfraOps app feature controls infrastructure security global infrastructure all privileges shows infrastructure navlink" classname="Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/infra/feature_controls/infrastructure_security·ts"› + ‹system-out› + - ‹![CDATA[[00:00:00] │ + + [00:00:00] │ + [00:05:13] └-: InfraOps app + ... + -]]› + + + ‹/system-out› + ‹skipped/› + ‹/testcase› + ‹testcase name="machine learning anomaly detection saved search with lucene query job creation opens the advanced section" classname="Firefox XPack UI Functional Tests.x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job·ts" time="6.040"› + - ‹system-out›‹![CDATA[[00:21:57] └-: machine learning...]]›‹/system-out› + - ‹failure›‹![CDATA[{ NoSuchSessionError: Tried to run command without establishing a connection + + ‹system-out›[00:21:57] └-: machine learning...‹/system-out› + + ‹failure›{ NoSuchSessionError: Tried to run command without establishing a connection + at Object.throwDecodedError (/dev/shm/workspace/kibana/node_modules/selenium-webdriver/lib/error.js:550:15) + at parseHttpResponse (/dev/shm/workspace/kibana/node_modules/selenium-webdriver/lib/http.js:563:13) + at Executor.execute (/dev/shm/workspace/kibana/node_modules/selenium-webdriver/lib/http.js:489:26) + - at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' }]]›‹/failure› + + at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' }‹/failure› + ‹/testcase› + ‹/testsuite› + -‹/testsuites› + +‹/testsuites› + \\ No newline at end of file + + `); +}); + +it('rewrites jest reports with minimal changes', async () => { + const xml = await addMessagesToReport({ + report: await parseTestReport(JEST_REPORT), + messages: [ + { + classname: 'X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp', + name: 'launcher can reconnect if process died', + message: 'foo bar', + }, + ], + log, + reportPath: Path.resolve(__dirname, './__fixtures__/jest_report.xml'), + }); + + expect(createPatch('jest.xml', JEST_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(` + Index: jest.xml + =================================================================== + --- jest.xml [object Object] + +++ jest.xml + @@ -3,13 +3,17 @@ + ‹testsuite name="x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts" timestamp="2019-06-07T03:42:21" time="14.504" tests="5" failures="1" skipped="0" file="/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts"› + ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="launcher can start and end a process" time="1.316"/› + ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="launcher can force kill the process if langServer can not exit" time="3.182"/› + ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="launcher can reconnect if process died" time="7.060"› + - ‹failure› + - ‹![CDATA[TypeError: Cannot read property '0' of undefined + - at Object.‹anonymous›.test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts:166:10)]]› + - ‹/failure› + + ‹failure›‹![CDATA[ + + TypeError: Cannot read property '0' of undefined + + at Object.‹anonymous›.test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts:166:10) + + + + + +Failed Tests Reporter: + + - foo bar + +]]›‹/failure› + ‹/testcase› + ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="passive launcher can start and end a process" time="0.435"/› + ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="passive launcher should restart a process if a process died before connected" time="1.502"/› + ‹/testsuite› + -‹/testsuites› + +‹/testsuites› + \\ No newline at end of file + + `); +}); + +it('rewrites mocha reports with minimal changes', async () => { + const xml = await addMessagesToReport({ + report: await parseTestReport(MOCHA_REPORT), + messages: [ + { + name: 'code in multiple nodes "before all" hook', + classname: 'X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts', + message: 'foo bar', + }, + ], + log, + reportPath: Path.resolve(__dirname, './__fixtures__/mocha_report.xml'), + }); + + expect(createPatch('mocha.xml', MOCHA_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(` + Index: mocha.xml + =================================================================== + --- mocha.xml [object Object] + +++ mocha.xml + @@ -2,12 +2,12 @@ + ‹testsuites› + ‹testsuite timestamp="2019-06-13T23:29:36" time="30.739" tests="1444" failures="2" skipped="3"› + ‹testcase name="code in multiple nodes "before all" hook" classname="X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts" time="0.121"› + ‹system-out› + - ‹![CDATA[]]› + + + ‹/system-out› + - ‹failure› + - ‹![CDATA[Error: Unable to read artifact info from https://artifacts-api.elastic.co/v1/versions/8.0.0-SNAPSHOT/builds/latest/projects/elasticsearch: Service Temporarily Unavailable + + ‹failure›‹![CDATA[ + + Error: Unable to read artifact info from https://artifacts-api.elastic.co/v1/versions/8.0.0-SNAPSHOT/builds/latest/projects/elasticsearch: Service Temporarily Unavailable + ‹html› + ‹head›‹title›503 Service Temporarily Unavailable‹/title›‹/head› + ‹body bgcolor="white"› + ‹center›‹h1›503 Service Temporarily Unavailable‹/h1›‹/center› + @@ -15,24 +15,28 @@ + ‹/body› + ‹/html› + + at Function.getSnapshot (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/packages/kbn-es/src/artifact.js:95:13) + - at process._tickCallback (internal/process/next_tick.js:68:7)]]› + - ‹/failure› + + at process._tickCallback (internal/process/next_tick.js:68:7) + + + + + +Failed Tests Reporter: + + - foo bar + +]]›‹/failure› + ‹/testcase› + ‹testcase name="code in multiple nodes "after all" hook" classname="X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts" time="0.003"› + ‹system-out› + - ‹![CDATA[]]› + + + ‹/system-out› + ‹failure› + - ‹![CDATA[TypeError: Cannot read property 'shutdown' of undefined + + TypeError: Cannot read property 'shutdown' of undefined + at Context.shutdown (plugins/code/server/__tests__/multi_node.ts:125:23) + - at process.topLevelDomainCallback (domain.js:120:23)]]› + + at process.topLevelDomainCallback (domain.js:120:23) + ‹/failure› + ‹/testcase› + ‹testcase name="repository service test can not clone a repo by ssh without a key" classname="X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/repository_service·ts" time="0.005"› + ‹system-out› + - ‹![CDATA[]]› + + + ‹/system-out› + ‹/testcase› + ‹/testsuite› + -‹/testsuites› + +‹/testsuites› + \\ No newline at end of file + + `); +}); + +it('rewrites karma reports with minimal changes', async () => { + const xml = await addMessagesToReport({ + report: await parseTestReport(KARMA_REPORT), + messages: [ + { + name: + 'CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should initialize OK', + classname: 'Browser Unit Tests.CoordinateMapsVisualizationTest', + message: 'foo bar', + }, + ], + log, + reportPath: Path.resolve(__dirname, './__fixtures__/karma_report.xml'), + }); + + expect(createPatch('karma.xml', KARMA_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(` + Index: karma.xml + =================================================================== + --- karma.xml [object Object] + +++ karma.xml + @@ -1,5 +1,5 @@ + -‹?xml version="1.0"?› + +‹?xml version="1.0" encoding="utf-8"?› + ‹testsuite name="Chrome 75.0.3770 (Mac OS X 10.14.5)" package="" timestamp="2019-07-02T19:53:21" id="0" hostname="spalger.lan" tests="648" errors="0" failures="4" time="1.759"› + ‹properties› + ‹property name="browser.fullName" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"/› + ‹/properties› + @@ -7,27 +7,31 @@ + ‹testcase name="Vis-Editor-Agg-Params plugin directive should hide custom label parameter" time="0" classname="Browser Unit Tests.Vis-Editor-Agg-Params plugin directive"› + ‹skipped/› + ‹/testcase› + ‹testcase name="CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should initialize OK" time="0.265" classname="Browser Unit Tests.CoordinateMapsVisualizationTest"› + - ‹failure type=""›Error: expected 7069 to be below 64 + - at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.assert (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13671:11) + - at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.lessThan.Assertion.below (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13891:8) + - at Function.lessThan (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:14078:15) + - at _callee3$ (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158985:60) + + ‹failure type=""›‹![CDATA[Error: expected 7069 to be below 64 + + at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.assert (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13671:11) + + at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.lessThan.Assertion.below (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13891:8) + + at Function.lessThan (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:14078:15) + + at _callee3$ (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158985:60) + at tryCatch (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:62:40) + at Generator.invoke [as _invoke] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:288:22) + - at Generator.prototype.<computed> [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21) + - at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) + - at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) + -‹/failure› + + at Generator.prototype.‹computed› [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21) + + at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) + + at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) + + + + + +Failed Tests Reporter: + + - foo bar + +]]›‹/failure› + ‹/testcase› + ‹testcase name="CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should toggle to Heatmap OK" time="0.055" classname="Browser Unit Tests.CoordinateMapsVisualizationTest"/› + ‹testcase name="VegaParser._parseSchema should warn on vega-lite version too new to be supported" time="0.001" classname="Browser Unit Tests.VegaParser·_parseSchema"/› + ‹system-out› + - ‹![CDATA[Chrome 75.0.3770 (Mac OS X 10.14.5) LOG: 'ready to load tests for shard 1 of 4' + + Chrome 75.0.3770 (Mac OS X 10.14.5) LOG: 'ready to load tests for shard 1 of 4' + ,Chrome 75.0.3770 (Mac OS X 10.14.5) WARN: 'Unmatched GET to http://localhost:9876/api/interpreter/fns' + ... + + -]]› + + + ‹/system-out› + ‹system-err/› + -‹/testsuite› + +‹/testsuite› + \\ No newline at end of file + + `); +}); diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts new file mode 100644 index 0000000000000..f82e1ef1fc19a --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts @@ -0,0 +1,90 @@ +/* + * 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 Fs from 'fs'; +import { promisify } from 'util'; + +import { ToolingLog } from '@kbn/dev-utils'; +import xml2js from 'xml2js'; + +import { TestReport, makeFailedTestCaseIter } from './test_report'; + +const writeAsync = promisify(Fs.writeFile); + +export interface Message { + classname: string; + name: string; + message: string; +} + +/** + * Mutate the report to include mentions of Github issues related to test failures, + * then write the updated report to disk + */ +export async function addMessagesToReport(options: { + log: ToolingLog; + report: TestReport; + messages: Message[]; + reportPath: string; + dryRun?: boolean; +}) { + const { log, report, messages, reportPath, dryRun } = options; + + for (const testCase of makeFailedTestCaseIter(report)) { + const { classname, name } = testCase.$; + const messageList = messages + .filter(u => u.classname === classname && u.name === name) + .reduce((acc, u) => `${acc}\n - ${u.message}`, ''); + + if (!messageList) { + continue; + } + + log.info(`${classname} - ${name}:${messageList}`); + const append = `\n\nFailed Tests Reporter:${messageList}\n`; + + if ( + testCase.failure[0] && + typeof testCase.failure[0] === 'object' && + typeof testCase.failure[0]._ === 'string' + ) { + testCase.failure[0]._ += append; + } else { + testCase.failure[0] = String(testCase.failure[0]) + append; + } + } + + const builder = new xml2js.Builder({ + cdata: true, + xmldec: { version: '1.0', encoding: 'utf-8' }, + }); + + const xml = builder + .buildObject(report) + .split('\n') + .map(line => (line.trim() === '' ? '' : line)) + .join('\n'); + + if (dryRun) { + log.info(`updated ${reportPath}\n${xml}`); + } else { + await writeAsync(reportPath, xml, 'utf8'); + } + return xml; +} diff --git a/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts b/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts index 1e0514a9b1cb0..fe6e0bbc796ee 100644 --- a/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts @@ -17,14 +17,12 @@ * under the License. */ -import { ToolingLog } from '@kbn/dev-utils'; - import { getFailures } from './get_failures'; - -const log = new ToolingLog(); +import { parseTestReport } from './test_report'; +import { FTR_REPORT, JEST_REPORT, KARMA_REPORT, MOCHA_REPORT } from './__fixtures__'; it('discovers failures in ftr report', async () => { - const failures = await getFailures(log, require.resolve('./__fixtures__/ftr_report.xml')); + const failures = getFailures(await parseTestReport(FTR_REPORT)); expect(failures).toMatchInlineSnapshot(` Array [ Object { @@ -37,15 +35,39 @@ it('discovers failures in ftr report', async () => { at lastError (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:28:9) at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13) ", + "likelyIrrelevant": false, "name": "maps app maps loaded from sample data ecommerce \\"before all\\" hook", "time": "154.378", }, + Object { + "classname": "Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps", + "failure": " + { NoSuchSessionError: This driver instance does not have a valid session ID (did you call WebDriver.quit()?) and may no longer be used. + at promise.finally (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/webdriver.js:726:38) + at Object.thenFinally [as finally] (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/promise.js:124:12) + at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' } + ", + "likelyIrrelevant": true, + "name": "maps app \\"after all\\" hook", + "time": "0.179", + }, + Object { + "classname": "Firefox XPack UI Functional Tests.x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job·ts", + "failure": "{ NoSuchSessionError: Tried to run command without establishing a connection + at Object.throwDecodedError (/dev/shm/workspace/kibana/node_modules/selenium-webdriver/lib/error.js:550:15) + at parseHttpResponse (/dev/shm/workspace/kibana/node_modules/selenium-webdriver/lib/http.js:563:13) + at Executor.execute (/dev/shm/workspace/kibana/node_modules/selenium-webdriver/lib/http.js:489:26) + at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' }", + "likelyIrrelevant": true, + "name": "machine learning anomaly detection saved search with lucene query job creation opens the advanced section", + "time": "6.040", + }, ] `); }); it('discovers failures in jest report', async () => { - const failures = await getFailures(log, require.resolve('./__fixtures__/jest_report.xml')); + const failures = getFailures(await parseTestReport(JEST_REPORT)); expect(failures).toMatchInlineSnapshot(` Array [ Object { @@ -54,6 +76,7 @@ it('discovers failures in jest report', async () => { TypeError: Cannot read property '0' of undefined at Object..test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts:166:10) ", + "likelyIrrelevant": false, "name": "launcher can reconnect if process died", "time": "7.060", }, @@ -62,7 +85,7 @@ it('discovers failures in jest report', async () => { }); it('discovers failures in karma report', async () => { - const failures = await getFailures(log, require.resolve('./__fixtures__/karma_report.xml')); + const failures = getFailures(await parseTestReport(KARMA_REPORT)); expect(failures).toMatchInlineSnapshot(` Array [ Object { @@ -78,6 +101,7 @@ it('discovers failures in karma report', async () => { at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) ", + "likelyIrrelevant": false, "name": "CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should initialize OK", "time": "0.265", }, @@ -86,6 +110,39 @@ it('discovers failures in karma report', async () => { }); it('discovers failures in mocha report', async () => { - const failures = await getFailures(log, require.resolve('./__fixtures__/mocha_report.xml')); - expect(failures).toMatchInlineSnapshot(`Array []`); + const failures = getFailures(await parseTestReport(MOCHA_REPORT)); + expect(failures).toMatchInlineSnapshot(` + Array [ + Object { + "classname": "X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts", + "failure": " + Error: Unable to read artifact info from https://artifacts-api.elastic.co/v1/versions/8.0.0-SNAPSHOT/builds/latest/projects/elasticsearch: Service Temporarily Unavailable + + 503 Service Temporarily Unavailable + +

503 Service Temporarily Unavailable

+
nginx/1.13.7
+ + + + at Function.getSnapshot (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/packages/kbn-es/src/artifact.js:95:13) + at process._tickCallback (internal/process/next_tick.js:68:7) + ", + "likelyIrrelevant": true, + "name": "code in multiple nodes \\"before all\\" hook", + "time": "0.121", + }, + Object { + "classname": "X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts", + "failure": " + TypeError: Cannot read property 'shutdown' of undefined + at Context.shutdown (plugins/code/server/__tests__/multi_node.ts:125:23) + at process.topLevelDomainCallback (domain.js:120:23) + ", + "likelyIrrelevant": true, + "name": "code in multiple nodes \\"after all\\" hook", + "time": "0.003", + }, + ] + `); }); diff --git a/packages/kbn-test/src/failed_tests_reporter/get_failures.ts b/packages/kbn-test/src/failed_tests_reporter/get_failures.ts index 85eff8eb07f5d..be058791f737a 100644 --- a/packages/kbn-test/src/failed_tests_reporter/get_failures.ts +++ b/packages/kbn-test/src/failed_tests_reporter/get_failures.ts @@ -17,69 +17,16 @@ * under the License. */ -import { promisify } from 'util'; -import Fs from 'fs'; - -import xml2js from 'xml2js'; import stripAnsi from 'strip-ansi'; -import { ToolingLog } from '@kbn/dev-utils'; - -type TestReport = - | { - testsuites: { - testsuite: TestSuite[]; - }; - } - | { - testsuite: TestSuite; - }; - -interface TestSuite { - $: { - /* ISO8601 timetamp when test suite ran */ - timestamp: string; - /* number of second this tests suite took */ - time: string; - /* number of tests as a string */ - tests: string; - /* number of failed tests as a string */ - failures: string; - /* number of skipped tests as a string */ - skipped: string; - }; - testcase: TestCase[]; -} -interface TestCase { - $: { - /* unique test name */ - name: string; - /* somewhat human readable combination of test name and file */ - classname: string; - /* number of seconds this test took */ - time: string; - }; - /* contents of system-out elements */ - 'system-out'?: string[]; - /* contents of failure elements */ - failure?: Array; - /* contents of skipped elements */ - skipped?: string[]; -} +import { FailedTestCase, TestReport, makeFailedTestCaseIter } from './test_report'; -export type TestFailure = TestCase['$'] & { +export type TestFailure = FailedTestCase['$'] & { failure: string; + likelyIrrelevant: boolean; }; -const readAsync = promisify(Fs.readFile); - -const indent = (text: string) => - ` ${text - .split('\n') - .map(l => ` ${l}`) - .join('\n')}`; - -const getFailureText = (failure: NonNullable) => { +const getFailureText = (failure: FailedTestCase['failure']) => { const [failureNode] = failure; if (failureNode && typeof failureNode === 'object' && typeof failureNode._ === 'string') { @@ -89,7 +36,7 @@ const getFailureText = (failure: NonNullable) => { return stripAnsi(String(failureNode)); }; -const isLikelyIrrelevant = ({ name, failure }: TestFailure) => { +const isLikelyIrrelevant = (name: string, failure: string) => { if ( failure.includes('NoSuchSessionError: This driver instance does not have a valid session ID') || failure.includes('NoSuchSessionError: Tried to run command without establishing a connection') @@ -118,47 +65,25 @@ const isLikelyIrrelevant = ({ name, failure }: TestFailure) => { if (failure.includes('Unable to fetch Kibana status API response from Kibana')) { return true; } -}; - -export async function getFailures(log: ToolingLog, testReportPath: string) { - const xml = await readAsync(testReportPath, 'utf8'); - - // Parses junit XML files - const report: TestReport = await xml2js.parseStringPromise(xml); - // Grab the failures. Reporters may report multiple testsuites in a single file. - const testSuites = 'testsuites' in report ? report.testsuites.testsuite : [report.testsuite]; + return false; +}; +export function getFailures(report: TestReport) { const failures: TestFailure[] = []; - for (const testSuite of testSuites) { - for (const testCase of testSuite.testcase) { - const { failure } = testCase; - if (!failure) { - continue; - } + for (const testCase of makeFailedTestCaseIter(report)) { + const failure = getFailureText(testCase.failure); + const likelyIrrelevant = isLikelyIrrelevant(testCase.$.name, failure); + failures.push({ // unwrap xml weirdness - const failureCase: TestFailure = { - ...testCase.$, - // Strip ANSI color characters - failure: getFailureText(failure), - }; - - if (isLikelyIrrelevant(failureCase)) { - log.warning( - `Ignoring likely irrelevant failure: ${failureCase.classname} - ${ - failureCase.name - }\n${indent(failureCase.failure)}` - ); - continue; - } - - failures.push(failureCase); - } + ...testCase.$, + // Strip ANSI color characters + failure, + likelyIrrelevant, + }); } - log.info(`Found ${failures.length} test failures`); - return failures; } diff --git a/packages/kbn-test/src/failed_tests_reporter/github_api.ts b/packages/kbn-test/src/failed_tests_reporter/github_api.ts index 46278044c840d..d8a952bee42e5 100644 --- a/packages/kbn-test/src/failed_tests_reporter/github_api.ts +++ b/packages/kbn-test/src/failed_tests_reporter/github_api.ts @@ -19,7 +19,7 @@ import Url from 'url'; -import Axios, { AxiosRequestConfig } from 'axios'; +import Axios, { AxiosRequestConfig, AxiosInstance } from 'axios'; import parseLinkHeader from 'parse-link-header'; import { ToolingLog, isAxiosResponseError, isAxiosRequestError } from '@kbn/dev-utils'; @@ -40,25 +40,34 @@ type RequestOptions = AxiosRequestConfig & { }; export class GithubApi { - private readonly x = Axios.create({ - headers: { - Authorization: `token ${this.token}`, - 'User-Agent': 'elastic/kibana#failed_test_reporter', - }, - }); + private readonly log: ToolingLog; + private readonly token: string | undefined; + private readonly dryRun: boolean; + private readonly x: AxiosInstance; /** * Create a GithubApi helper object, if token is undefined requests won't be * sent, but will instead be logged. */ - constructor( - private readonly log: ToolingLog, - private readonly token: string | undefined, - private readonly dryRun: boolean - ) { - if (!token && !dryRun) { + constructor(options: { + log: GithubApi['log']; + token: GithubApi['token']; + dryRun: GithubApi['dryRun']; + }) { + this.log = options.log; + this.token = options.token; + this.dryRun = options.dryRun; + + if (!this.token && !this.dryRun) { throw new TypeError('token parameter is required'); } + + this.x = Axios.create({ + headers: { + ...(this.token ? { Authorization: `token ${this.token}` } : {}), + 'User-Agent': 'elastic/kibana#failed_test_reporter', + }, + }); } private failedTestIssuesPageCache: { diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts index 0e9f8db587cb7..ef6ab3c51ab19 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts @@ -18,19 +18,14 @@ */ import dedent from 'dedent'; -import { ToolingLog, ToolingLogCollectingWriter } from '@kbn/dev-utils'; -import { createFailureIssue, updatedFailureIssue } from './report_failure'; +import { createFailureIssue, updateFailureIssue } from './report_failure'; jest.mock('./github_api'); const { GithubApi } = jest.requireMock('./github_api'); describe('createFailureIssue()', () => { it('creates new github issue with failure text, link to issue, and valid metadata', async () => { - const log = new ToolingLog(); - const writer = new ToolingLogCollectingWriter(); - log.setWriters([writer]); - const api = new GithubApi(); await createFailureIssue( @@ -40,8 +35,8 @@ describe('createFailureIssue()', () => { failure: 'this is the failure text', name: 'test name', time: '2018-01-01T01:00:00Z', + likelyIrrelevant: false, }, - log, api ); @@ -72,23 +67,14 @@ describe('createFailureIssue()', () => { ], } `); - expect(writer.messages).toMatchInlineSnapshot(` - Array [ - " info Created issue undefined", - ] - `); }); }); -describe('updatedFailureIssue()', () => { +describe('updateFailureIssue()', () => { it('increments failure count and adds new comment to issue', async () => { - const log = new ToolingLog(); - const writer = new ToolingLogCollectingWriter(); - log.setWriters([writer]); - const api = new GithubApi(); - await updatedFailureIssue( + await updateFailureIssue( 'https://build-url', { html_url: 'https://github.com/issues/1234', @@ -101,7 +87,6 @@ describe('updatedFailureIssue()', () => { " `, }, - log, api ); @@ -139,10 +124,5 @@ describe('updatedFailureIssue()', () => { ], } `); - expect(writer.messages).toMatchInlineSnapshot(` - Array [ - " info Updated issue https://github.com/issues/1234, failCount: 11", - ] - `); }); }); diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts index afb46c429c7fc..97e9d517576fc 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts @@ -17,18 +17,11 @@ * under the License. */ -import { ToolingLog } from '@kbn/dev-utils'; - import { TestFailure } from './get_failures'; import { GithubIssue, GithubApi } from './github_api'; import { getIssueMetadata, updateIssueMetadata } from './issue_metadata'; -export async function createFailureIssue( - buildUrl: string, - failure: TestFailure, - log: ToolingLog, - api: GithubApi -) { +export async function createFailureIssue(buildUrl: string, failure: TestFailure, api: GithubApi) { const title = `Failing test: ${failure.classname} - ${failure.name}`; const body = updateIssueMetadata( @@ -48,16 +41,10 @@ export async function createFailureIssue( } ); - const newIssueUrl = await api.createIssue(title, body, ['failed-test']); - log.info(`Created issue ${newIssueUrl}`); + return await api.createIssue(title, body, ['failed-test']); } -export async function updatedFailureIssue( - buildUrl: string, - issue: GithubIssue, - log: ToolingLog, - api: GithubApi -) { +export async function updateFailureIssue(buildUrl: string, issue: GithubIssue, api: GithubApi) { // Increment failCount const newCount = getIssueMetadata(issue.body, 'test.failCount', 0) + 1; const newBody = updateIssueMetadata(issue.body, { @@ -67,5 +54,5 @@ export async function updatedFailureIssue( await api.editIssueBodyAndEnsureOpen(issue.number, newBody); await api.addIssueComment(issue.number, `New failure: [Jenkins Build](${buildUrl})`); - log.info(`Updated issue ${issue.html_url}, failCount: ${newCount}`); + return newCount; } diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 0eea1d32e5c2e..b3c2a8dc338da 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -22,19 +22,22 @@ import globby from 'globby'; import { getFailures } from './get_failures'; import { GithubApi } from './github_api'; -import { updatedFailureIssue, createFailureIssue } from './report_failure'; +import { updateFailureIssue, createFailureIssue } from './report_failure'; import { getIssueMetadata } from './issue_metadata'; +import { readTestReport } from './test_report'; +import { addMessagesToReport, Message } from './add_messages_to_report'; export function runFailedTestsReporterCli() { run( async ({ log, flags }) => { - const buildUrl = flags['build-url']; - if (typeof buildUrl !== 'string' || !buildUrl) { - throw createFlagError('Missing --build-url or process.env.BUILD_URL'); + let updateGithub = flags['github-update']; + if (updateGithub && !process.env.GITHUB_TOKEN) { + throw createFailError( + 'GITHUB_TOKEN environment variable must be set, otherwise use --no-github-update flag' + ); } - const dryRun = !!flags['dry-run']; - if (!dryRun) { + if (updateGithub) { // JOB_NAME is formatted as `elastic+kibana+7.x` in some places and `elastic+kibana+7.x/JOB=kibana-intake,node=immutable` in others const jobNameSplit = (process.env.JOB_NAME || '').split(/\+|\//); const branch = jobNameSplit.length >= 3 ? jobNameSplit[2] : process.env.GIT_BRANCH; @@ -48,26 +51,43 @@ export function runFailedTestsReporterCli() { const isMasterOrVersion = branch.match(/^(origin\/){0,1}master$/) || branch.match(/^(origin\/){0,1}\d+\.(x|\d+)$/); if (!isMasterOrVersion || isPr) { - throw createFailError('Failure issues only created on master/version branch jobs', { - exitCode: 0, - }); + log.info('Failure issues only created on master/version branch jobs'); + updateGithub = false; } + } - if (!process.env.GITHUB_TOKEN) { - throw createFailError( - 'GITHUB_TOKEN environment variable must be set, otherwise use --dry-run flag' - ); - } + const githubApi = new GithubApi({ + log, + token: process.env.GITHUB_TOKEN, + dryRun: !updateGithub, + }); + + const buildUrl = flags['build-url'] || (updateGithub ? '' : 'http://buildUrl'); + if (typeof buildUrl !== 'string' || !buildUrl) { + throw createFlagError('Missing --build-url or process.env.BUILD_URL'); } - const githubApi = new GithubApi(log, process.env.GITHUB_TOKEN, dryRun); const reportPaths = await globby(['target/junit/**/*.xml'], { cwd: REPO_ROOT, absolute: true, }); for (const reportPath of reportPaths) { - for (const failure of await getFailures(log, reportPath)) { + const report = await readTestReport(reportPath); + const messages: Message[] = []; + + for (const failure of await getFailures(report)) { + if (failure.likelyIrrelevant) { + messages.push({ + classname: failure.classname, + name: failure.name, + message: + 'Failure is likely irrelevant' + + (updateGithub ? ', so an issue was not created or updated' : ''), + }); + continue; + } + const existingIssue = await githubApi.findFailedTestIssue( i => getIssueMetadata(i.body, 'test.class') === failure.classname && @@ -75,23 +95,57 @@ export function runFailedTestsReporterCli() { ); if (existingIssue) { - await updatedFailureIssue(buildUrl, existingIssue, log, githubApi); - } else { - await createFailureIssue(buildUrl, failure, log, githubApi); + const newFailureCount = await updateFailureIssue(buildUrl, existingIssue, githubApi); + const url = existingIssue.html_url; + const message = + `Test has failed ${newFailureCount - 1} times on tracked branches: ${url}` + + (updateGithub + ? `. Updated existing issue: ${url} (fail count: ${newFailureCount})` + : ''); + + messages.push({ + classname: failure.classname, + name: failure.name, + message, + }); + continue; } + + const newIssueUrl = await createFailureIssue(buildUrl, failure, githubApi); + const message = + `Test has not failed recently on tracked branches` + + (updateGithub ? `Created new issue: ${newIssueUrl}` : ''); + + messages.push({ + classname: failure.classname, + name: failure.name, + message, + }); } + + // mutates report to include messages and writes updated report to disk + await addMessagesToReport({ + report, + messages, + log, + reportPath, + dryRun: !flags['report-update'], + }); } }, { description: `a cli that opens issues or updates existing issues based on junit reports`, flags: { - boolean: ['dry-run'], + boolean: ['github-update', 'report-update'], string: ['build-url'], default: { + 'github-update': true, + 'report-update': true, 'build-url': process.env.BUILD_URL, }, help: ` - --dry-run Execute the CLI without contacting Github + --no-github-update Execute the CLI without writing to Github + --no-report-update Execute the CLI without writing to the JUnit reports --build-url URL of the failed build, defaults to process.env.BUILD_URL `, }, diff --git a/packages/kbn-test/src/failed_tests_reporter/test_report.ts b/packages/kbn-test/src/failed_tests_reporter/test_report.ts new file mode 100644 index 0000000000000..644a4cc9fd5a7 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/test_report.ts @@ -0,0 +1,100 @@ +/* + * 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 Fs from 'fs'; +import { promisify } from 'util'; + +import xml2js from 'xml2js'; + +const readAsync = promisify(Fs.readFile); + +export type TestReport = + | { + testsuites: { + testsuite: TestSuite[]; + }; + } + | { + testsuite: TestSuite; + }; + +export interface TestSuite { + $: { + /* ISO8601 timetamp when test suite ran */ + timestamp: string; + /* number of second this tests suite took */ + time: string; + /* number of tests as a string */ + tests: string; + /* number of failed tests as a string */ + failures: string; + /* number of skipped tests as a string */ + skipped: string; + }; + testcase: TestCase[]; +} + +export interface TestCase { + $: { + /* unique test name */ + name: string; + /* somewhat human readable combination of test name and file */ + classname: string; + /* number of seconds this test took */ + time: string; + }; + /* contents of system-out elements */ + 'system-out'?: string[]; + /* contents of failure elements */ + failure?: Array; + /* contents of skipped elements */ + skipped?: string[]; +} + +export interface FailedTestCase extends TestCase { + failure: Array; +} + +/** + * Parse JUnit XML Files + */ +export async function parseTestReport(xml: string): Promise { + return await xml2js.parseStringPromise(xml); +} + +export async function readTestReport(testReportPath: string) { + return await parseTestReport(await readAsync(testReportPath, 'utf8')); +} + +export function* makeFailedTestCaseIter(report: TestReport) { + // Grab the failures. Reporters may report multiple testsuites in a single file. + const testSuites = 'testsuites' in report ? report.testsuites.testsuite : [report.testsuite]; + + for (const testSuite of testSuites) { + for (const testCase of testSuite.testcase) { + const { failure } = testCase; + + if (!failure) { + continue; + } + + yield testCase as FailedTestCase; + } + } +} diff --git a/packages/kbn-ui-framework/dist/kui_dark.css b/packages/kbn-ui-framework/dist/kui_dark.css index dcbd65fbca520..aa16bcdb34d36 100644 --- a/packages/kbn-ui-framework/dist/kui_dark.css +++ b/packages/kbn-ui-framework/dist/kui_dark.css @@ -1,10 +1,24 @@ +/* 1 */ +/* 1 */ +/** + * 1. Extend beta badges to at least 40% of the container's width + * 2. Fix for IE to ensure badges are visible outside of a + + + ); +}; +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/redux.md b/src/plugins/kibana_utils/docs/state_containers/redux.md new file mode 100644 index 0000000000000..1a60d841a8b75 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/redux.md @@ -0,0 +1,40 @@ +# Redux + +State containers similar to Redux stores but without the boilerplate. + +State containers expose Redux-like API: + +```js +container.getState() +container.dispatch() +container.replaceReducer() +container.subscribe() +container.addMiddleware() +``` + +State containers have a reducer and every time you execute a state transition it +actually dispatches an "action". For example, this + +```js +container.transitions.increment(25); +``` + +is equivalent to + +```js +container.dispatch({ + type: 'increment', + args: [25], +}); +``` + +Because all transitions happen through `.dispatch()` interface, you can add middleware—similar how you +would do with Redux—to monitor or intercept transitions. + +For example, you can add `redux-logger` middleware to log in console all transitions happening with your store. + +```js +import logger from 'redux-logger'; + +container.addMiddleware(logger); +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/transitions.md b/src/plugins/kibana_utils/docs/state_containers/transitions.md new file mode 100644 index 0000000000000..51d52cdf3daaf --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/transitions.md @@ -0,0 +1,61 @@ +# State transitions + +*State transitions* describe possible state changes over time. Transitions are pure functions which +receive `state` object and other—optional—arguments and must return a new `state` object back. + +```ts +type Transition = (state: State) => (...args) => State; +``` + +Transitions must not mutate `state` object in-place, instead they must return a +shallow copy of it, e.g. `{ ...state }`. Example: + +```ts +const setUiMode: PureTransition = state => uiMode => ({ ...state, uiMode }); +``` + +You provide transitions as a second argument when you create your state container. + +```ts +import { createStateContainer } from 'src/plugins/kibana_utils'; + +const container = createStateContainer(0, { + increment: (cnt: number) => (by: number) => cnt + by, + double: (cnt: number) => () => cnt * 2, +}); +``` + +Now you can execute the transitions by calling them with only optional parameters (`state` is +provided to your transitions automatically). + +```ts +container.transitions.increment(25); +container.transitions.increment(5); +container.state; // 30 +``` + +Your transitions are bound to the container so you can treat each of them as a +standalone function for export. + +```ts +const defaultState = { + uiMode: 'light', +}; + +const container = createStateContainer(defaultState, { + setUiMode: state => uiMode => ({ ...state, uiMode }), + resetUiMode: state => () => ({ ...state, uiMode: defaultState.uiMode }), +}); + +export const { + setUiMode, + resetUiMode +} = container.transitions; +``` + +You can add TypeScript annotations for your transitions as the second generic argument +to `createStateContainer()` function. + +```ts +const container = createStateContainer(defaultState, pureTransitions); +``` diff --git a/src/plugins/kibana_utils/docs/store/README.md b/src/plugins/kibana_utils/docs/store/README.md deleted file mode 100644 index e1cb098fe04ce..0000000000000 --- a/src/plugins/kibana_utils/docs/store/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# State containers - -- State containers for holding serializable state. -- [Each plugin/app that needs runtime state will create a *store* using `store = createStore()`](./creation.md). -- [*Store* can be updated using mutators `mutators = store.createMutators({ ... })`](./mutators.md). -- [*Store* can be connected to React `{Provider, connect} = createContext(store)`](./react.md). -- [In no-React setting *store* is consumed using `store.get()` and `store.state$`](./getters.md). -- [Under-the-hood uses Redux `store.redux`](./redux.md) (but you should never need it explicitly). -- [See idea doc with samples and rationale](https://docs.google.com/document/d/18eitHkcyKSsEHUfUIqFKChc8Pp62Z4gcRxdu903hbA0/edit#heading=h.iaxc9whxifl5). diff --git a/src/plugins/kibana_utils/docs/store/mutators.md b/src/plugins/kibana_utils/docs/store/mutators.md deleted file mode 100644 index 9db1b1bb60b3c..0000000000000 --- a/src/plugins/kibana_utils/docs/store/mutators.md +++ /dev/null @@ -1,70 +0,0 @@ -# Mutators - -State *mutators* are pure functions which receive `state` object and other—optional—arguments -and must return a new `state` object back. - -```ts -type Mutator = (state: State) => (...args) => State; -``` - -Mutator must not mutate `state` object in-place, instead it should return a -shallow copy of it, e.g. `{ ...state }`. - -```ts -const setUiMode: Mutator = state => uiMode => ({ ...state, uiMode }); -``` - -You create mutators using `.createMutator(...)` method. - -```ts -const store = createStore({uiMode: 'light'}); -const mutators = store.createMutators({ - setUiMode: state => uiMode => ({ ...state, uiMode }), -}); -``` - -Now you can use your mutators by calling them with only optional parameters (`state` is -provided to your mutator automatically). - -```ts -mutators.setUiMode('dark'); -``` - -Your mutators are bound to the `store` so you can treat each of them as a -standalone function for export. - -```ts -const { setUiMode, resetUiMode } = store.createMutators({ - setUiMode: state => uiMode => ({ ...state, uiMode }), - resetUiMode: state => () => ({ ...state, uiMode: 'light' }), -}); - -export { - setUiMode, - resetUiMode, -}; -``` - -The mutators you create are also available on the `store` object. - -```ts -const store = createStore({ cnt: 0 }); -store.createMutators({ - add: state => value => ({ ...state, cnt: state.cnt + value }), -}); - -store.mutators.add(5); -store.get(); // { cnt: 5 } -``` - -You can add TypeScript annotations to your `.mutators` property of `store` object. - -```ts -const store = createStore<{ - cnt: number; -}, { - add: (value: number) => void; -}>({ - cnt: 0 -}); -``` diff --git a/src/plugins/kibana_utils/docs/store/react.md b/src/plugins/kibana_utils/docs/store/react.md deleted file mode 100644 index 68a016ed6d3ca..0000000000000 --- a/src/plugins/kibana_utils/docs/store/react.md +++ /dev/null @@ -1,101 +0,0 @@ -# React - -`createContext` factory allows you to easily use state containers with React. - -```ts -import { createStore, createContext } from 'kibana-utils'; - -const store = createStore({}); -const { - Provider, - Consumer, - connect, - context, - useStore, - useState, - useMutators, - useSelector, -} = createContext(store); -``` - -Wrap your app with ``. - -```tsx - - - -``` - -Use `connect()()` higer-order-component to inject props from state into your component. - -```tsx -interface Props { - name: string; - punctuation: '.' | ',' | '!', -} -const Demo: React.FC = ({ name, punctuation }) => -
Hello, {name}{punctuation}
; - -const store = createStore({ userName: 'John' }); -const { Provider, connect } = createContext(store); - -const mapStateToProps = ({ userName }) => ({ name: userName }); -const DemoConnected = connect(mapStateToProps)(Demo); - - - - -``` - -`useStore` React hook will fetch the `store` object from the context. - -```tsx -const Demo = () => { - const store = useStore(); - return
{store.get().isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -If you want your component to always re-render when the state changes use `useState` React hook. - -```tsx -const Demo = () => { - const { isDarkMode } = useState(); - return
{isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -For `useSelector` React hook you specify a selector function, which will pick specific -data from the state. *Your component will update only when that specific part of the state changes.* - -```tsx -const selector = state => state.isDarkMode; -const Demo = () => { - const isDarkMode = useSelector(selector); - return
{isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -As an optional second argument for `useSelector` you can provide a `comparator` function, which -compares currently selected value with the previous and your component will re-render only if -`comparator` returns `true`. By default, it simply uses tripple equals `===` comparison. - -``` -useSelector(selector, comparator?) -``` - -Access state mutators by `useMutators` React hook. - -```tsx -const Demo = () => { - const { isDarkMode } = useState(); - const { setDarkMode } = useMutators(); - return ( - <> -
{isDarkMode ? '🌑' : '☀️'}
- - - - ); -}; -``` diff --git a/src/plugins/kibana_utils/docs/store/redux.md b/src/plugins/kibana_utils/docs/store/redux.md deleted file mode 100644 index 23be76f35b36e..0000000000000 --- a/src/plugins/kibana_utils/docs/store/redux.md +++ /dev/null @@ -1,19 +0,0 @@ -# Redux - -Internally `createStore()` uses Redux to manage the state. When you call `store.get()` -it is actually calling the Redux `.getState()` method. When you execute a mutation -it is actually dispatching a Redux action. - -You can access Redux *store* using `.redux`. - -```ts -store.redux; -``` - -But you should never need it, if you think you do, consult with Kibana App Architecture team. - -We use Redux internally for 3 main reasons: - -- We can reuse `react-redux` library to easily connect state containers to React. -- We can reuse Redux devtools. -- We can reuse battle-tested Redux library and action/reducer paradigm. diff --git a/src/plugins/kibana_utils/public/index.test.ts b/src/plugins/kibana_utils/public/index.test.ts index 0e2a4acf15f04..27c4d6c1c06e9 100644 --- a/src/plugins/kibana_utils/public/index.test.ts +++ b/src/plugins/kibana_utils/public/index.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { createStore, createContext } from '.'; +import { createStateContainer, createStateContainerReactHelpers } from '.'; test('exports store methods', () => { - expect(typeof createStore).toBe('function'); - expect(typeof createContext).toBe('function'); + expect(typeof createStateContainer).toBe('function'); + expect(typeof createStateContainerReactHelpers).toBe('function'); }); diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 04845c72cb755..3f5aeebac54d8 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -19,11 +19,13 @@ export * from './core'; export * from './errors'; -export * from './store'; -export * from './parse'; -export * from './resize_checker'; -export * from './render_complete'; -export * from './store'; export * from './errors'; export * from './field_mapping'; +export * from './parse'; +export * from './render_complete'; +export * from './resize_checker'; +export * from './state_containers'; export * from './storage'; +export * from './storage/hashed_item_store'; +export * from './state_management/state_hash'; +export * from './state_management/url'; diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts new file mode 100644 index 0000000000000..9165181299a90 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts @@ -0,0 +1,303 @@ +/* + * 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 { createStateContainer } from './create_state_container'; + +const create = (state: S, transitions: T = {} as T) => { + const pureTransitions = { + set: () => (newState: S) => newState, + ...transitions, + }; + const store = createStateContainer(state, pureTransitions); + return { store, mutators: store.transitions }; +}; + +test('can create store', () => { + const { store } = create({}); + expect(store).toMatchObject({ + getState: expect.any(Function), + state$: expect.any(Object), + transitions: expect.any(Object), + dispatch: expect.any(Function), + subscribe: expect.any(Function), + replaceReducer: expect.any(Function), + addMiddleware: expect.any(Function), + }); +}); + +test('can set default state', () => { + const defaultState = { + foo: 'bar', + }; + const { store } = create(defaultState); + expect(store.get()).toEqual(defaultState); + expect(store.getState()).toEqual(defaultState); +}); + +test('can set state', () => { + const defaultState = { + foo: 'bar', + }; + const newState = { + foo: 'baz', + }; + const { store, mutators } = create(defaultState); + + mutators.set(newState); + + expect(store.get()).toEqual(newState); + expect(store.getState()).toEqual(newState); +}); + +test('does not shallow merge states', () => { + const defaultState = { + foo: 'bar', + }; + const newState = { + foo2: 'baz', + }; + const { store, mutators } = create(defaultState); + + mutators.set(newState as any); + + expect(store.get()).toEqual(newState); + expect(store.getState()).toEqual(newState); +}); + +test('can subscribe and unsubscribe to state changes', () => { + const { store, mutators } = create({}); + const spy = jest.fn(); + const subscription = store.state$.subscribe(spy); + mutators.set({ a: 1 }); + mutators.set({ a: 2 }); + subscription.unsubscribe(); + mutators.set({ a: 3 }); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy.mock.calls[1][0]).toEqual({ a: 2 }); +}); + +test('multiple subscribers can subscribe', () => { + const { store, mutators } = create({}); + const spy1 = jest.fn(); + const spy2 = jest.fn(); + const subscription1 = store.state$.subscribe(spy1); + const subscription2 = store.state$.subscribe(spy2); + mutators.set({ a: 1 }); + subscription1.unsubscribe(); + mutators.set({ a: 2 }); + subscription2.unsubscribe(); + mutators.set({ a: 3 }); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(2); + expect(spy1.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy2.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy2.mock.calls[1][0]).toEqual({ a: 2 }); +}); + +test('creates impure mutators from pure mutators', () => { + const { mutators } = create( + {}, + { + setFoo: () => (bar: any) => ({ foo: bar }), + } + ); + + expect(typeof mutators.setFoo).toBe('function'); +}); + +test('mutators can update state', () => { + const { store, mutators } = create( + { + value: 0, + foo: 'bar', + }, + { + add: (state: any) => (increment: any) => ({ ...state, value: state.value + increment }), + setFoo: (state: any) => (bar: any) => ({ ...state, foo: bar }), + } + ); + + expect(store.get()).toEqual({ + value: 0, + foo: 'bar', + }); + + mutators.add(11); + mutators.setFoo('baz'); + + expect(store.get()).toEqual({ + value: 11, + foo: 'baz', + }); + + mutators.add(-20); + mutators.setFoo('bazooka'); + + expect(store.get()).toEqual({ + value: -9, + foo: 'bazooka', + }); +}); + +test('mutators methods are not bound', () => { + const { store, mutators } = create( + { value: -3 }, + { + add: (state: { value: number }) => (increment: number) => ({ + ...state, + value: state.value + increment, + }), + } + ); + + expect(store.get()).toEqual({ value: -3 }); + mutators.add(4); + expect(store.get()).toEqual({ value: 1 }); +}); + +test('created mutators are saved in store object', () => { + const { store, mutators } = create( + { value: -3 }, + { + add: (state: { value: number }) => (increment: number) => ({ + ...state, + value: state.value + increment, + }), + } + ); + + expect(typeof store.transitions.add).toBe('function'); + mutators.add(5); + expect(store.get()).toEqual({ value: 2 }); +}); + +test('throws when state is modified inline - 1', () => { + const container = createStateContainer({ a: 'b' }, {}); + + let error: TypeError | null = null; + try { + (container.get().a as any) = 'c'; + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(TypeError); +}); + +test('throws when state is modified inline - 2', () => { + const container = createStateContainer({ a: 'b' }, {}); + + let error: TypeError | null = null; + try { + (container.getState().a as any) = 'c'; + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(TypeError); +}); + +test('throws when state is modified inline in subscription', done => { + const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState }); + + container.subscribe(value => { + let error: TypeError | null = null; + try { + (value.a as any) = 'd'; + } catch (err) { + error = err; + } + expect(error).toBeInstanceOf(TypeError); + done(); + }); + container.transitions.set({ a: 'c' }); +}); + +describe('selectors', () => { + test('can specify no selectors, or can skip them', () => { + createStateContainer({}, {}); + createStateContainer({}, {}, {}); + }); + + test('selector object is available on .selectors key', () => { + const container1 = createStateContainer({}, {}, {}); + const container2 = createStateContainer({}, {}, { foo: () => () => 123 }); + const container3 = createStateContainer({}, {}, { bar: () => () => 1, baz: () => () => 1 }); + + expect(Object.keys(container1.selectors).sort()).toEqual([]); + expect(Object.keys(container2.selectors).sort()).toEqual(['foo']); + expect(Object.keys(container3.selectors).sort()).toEqual(['bar', 'baz']); + }); + + test('selector without arguments returns correct state slice', () => { + const container = createStateContainer( + { name: 'Oleg' }, + { + changeName: (state: { name: string }) => (name: string) => ({ ...state, name }), + }, + { getName: (state: { name: string }) => () => state.name } + ); + + expect(container.selectors.getName()).toBe('Oleg'); + container.transitions.changeName('Britney'); + expect(container.selectors.getName()).toBe('Britney'); + }); + + test('selector can accept an argument', () => { + const container = createStateContainer( + { + users: { + 1: { + name: 'Darth', + }, + }, + }, + {}, + { + getUser: (state: any) => (id: number) => state.users[id], + } + ); + + expect(container.selectors.getUser(1)).toEqual({ name: 'Darth' }); + expect(container.selectors.getUser(2)).toBe(undefined); + }); + + test('selector can accept multiple arguments', () => { + const container = createStateContainer( + { + users: { + 5: { + name: 'Darth', + surname: 'Vader', + }, + }, + }, + {}, + { + getName: (state: any) => (id: number, which: 'name' | 'surname') => state.users[id][which], + } + ); + + expect(container.selectors.getName(5, 'name')).toEqual('Darth'); + expect(container.selectors.getName(5, 'surname')).toEqual('Vader'); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts new file mode 100644 index 0000000000000..1ef4a1c012817 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts @@ -0,0 +1,89 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { skip } from 'rxjs/operators'; +import { RecursiveReadonly } from '@kbn/utility-types'; +import { + PureTransitionsToTransitions, + PureTransition, + ReduxLikeStateContainer, + PureSelectorsToSelectors, +} from './types'; + +const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable'; + +const freeze: (value: T) => RecursiveReadonly = + process.env.NODE_ENV !== 'production' + ? (value: T): RecursiveReadonly => { + if (!value) return value as RecursiveReadonly; + if (value instanceof Array) return value as RecursiveReadonly; + if (typeof value === 'object') return Object.freeze({ ...value }) as RecursiveReadonly; + else return value as RecursiveReadonly; + } + : (value: T) => value as RecursiveReadonly; + +export const createStateContainer = < + State, + PureTransitions extends object, + PureSelectors extends object = {} +>( + defaultState: State, + pureTransitions: PureTransitions, + pureSelectors: PureSelectors = {} as PureSelectors +): ReduxLikeStateContainer => { + const data$ = new BehaviorSubject>(freeze(defaultState)); + const state$ = data$.pipe(skip(1)); + const get = () => data$.getValue(); + const container: ReduxLikeStateContainer = { + get, + state$, + getState: () => data$.getValue(), + set: (state: State) => { + data$.next(freeze(state)); + }, + reducer: (state, action) => { + const pureTransition = (pureTransitions as Record>)[ + action.type + ]; + return pureTransition ? freeze(pureTransition(state)(...action.args)) : state; + }, + replaceReducer: nextReducer => (container.reducer = nextReducer), + dispatch: action => data$.next(container.reducer(get(), action)), + transitions: Object.keys(pureTransitions).reduce>( + (acc, type) => ({ ...acc, [type]: (...args: any) => container.dispatch({ type, args }) }), + {} as PureTransitionsToTransitions + ), + selectors: Object.keys(pureSelectors).reduce>( + (acc, selector) => ({ + ...acc, + [selector]: (...args: any) => (pureSelectors as any)[selector](get())(...args), + }), + {} as PureSelectorsToSelectors + ), + addMiddleware: middleware => + (container.dispatch = middleware(container as any)(container.dispatch)), + subscribe: (listener: (state: RecursiveReadonly) => void) => { + const subscription = state$.subscribe(listener); + return () => subscription.unsubscribe(); + }, + [$$observable]: state$, + }; + return container; +}; diff --git a/src/plugins/kibana_utils/public/store/react.test.tsx b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx similarity index 65% rename from src/plugins/kibana_utils/public/store/react.test.tsx rename to src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx index e629e9d0e1257..8f5810f3e147d 100644 --- a/src/plugins/kibana_utils/public/store/react.test.tsx +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx @@ -20,8 +20,17 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { act, Simulate } from 'react-dom/test-utils'; -import { createStore } from './create_store'; -import { createContext } from './react'; +import { createStateContainer } from './create_state_container'; +import { createStateContainerReactHelpers } from './create_state_container_react_helpers'; + +const create = (state: S, transitions: T = {} as T) => { + const pureTransitions = { + set: () => (newState: S) => newState, + ...transitions, + }; + const store = createStateContainer(state, pureTransitions); + return { store, mutators: store.transitions }; +}; let container: HTMLDivElement | null; @@ -36,27 +45,23 @@ afterEach(() => { }); test('can create React context', () => { - const store = createStore({ foo: 'bar' }); - const context = createContext(store); + const context = createStateContainerReactHelpers(); expect(context).toMatchObject({ - Provider: expect.any(Function), - Consumer: expect.any(Function), + Provider: expect.any(Object), + Consumer: expect.any(Object), connect: expect.any(Function), - context: { - Provider: expect.any(Object), - Consumer: expect.any(Object), - }, + context: expect.any(Object), }); }); test(' passes state to ', () => { - const store = createStore({ hello: 'world' }); - const { Provider, Consumer } = createContext(store); + const { store } = create({ hello: 'world' }); + const { Provider, Consumer } = createStateContainerReactHelpers(); ReactDOM.render( - - {({ hello }) => hello} + + {(s: typeof store) => s.get().hello} , container ); @@ -74,8 +79,8 @@ interface Props1 { } test(' passes state to connect()()', () => { - const store = createStore({ hello: 'Bob' }); - const { Provider, connect } = createContext(store); + const { store } = create({ hello: 'Bob' }); + const { Provider, connect } = createStateContainerReactHelpers(); const Demo: React.FC = ({ message, stop }) => ( <> @@ -87,7 +92,7 @@ test(' passes state to connect()()', () => { const DemoConnected = connect(mergeProps)(Demo); ReactDOM.render( - + , container @@ -97,13 +102,13 @@ test(' passes state to connect()()', () => { }); test('context receives Redux store', () => { - const store = createStore({ foo: 'bar' }); - const { Provider, context } = createContext(store); + const { store } = create({ foo: 'bar' }); + const { Provider, context } = createStateContainerReactHelpers(); ReactDOM.render( /* eslint-disable no-shadow */ - - {({ store }) => store.getState().foo} + + {store => store.get().foo} , /* eslint-enable no-shadow */ container @@ -117,16 +122,16 @@ xtest('can use multiple stores in one React app', () => {}); describe('hooks', () => { describe('useStore', () => { test('can select store using useStore hook', () => { - const store = createStore({ foo: 'bar' }); - const { Provider, useStore } = createContext(store); + const { store } = create({ foo: 'bar' }); + const { Provider, useContainer } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { // eslint-disable-next-line no-shadow - const store = useStore(); + const store = useContainer(); return <>{store.get().foo}; }; ReactDOM.render( - + , container @@ -138,15 +143,15 @@ describe('hooks', () => { describe('useState', () => { test('can select state using useState hook', () => { - const store = createStore({ foo: 'qux' }); - const { Provider, useState } = createContext(store); + const { store } = create({ foo: 'qux' }); + const { Provider, useState } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const { foo } = useState(); return <>{foo}; }; ReactDOM.render( - + , container @@ -156,18 +161,23 @@ describe('hooks', () => { }); test('re-renders when state changes', () => { - const store = createStore({ foo: 'bar' }); - const { setFoo } = store.createMutators({ - setFoo: state => foo => ({ ...state, foo }), - }); - const { Provider, useState } = createContext(store); + const { + store, + mutators: { setFoo }, + } = create( + { foo: 'bar' }, + { + setFoo: (state: { foo: string }) => (foo: string) => ({ ...state, foo }), + } + ); + const { Provider, useState } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const { foo } = useState(); return <>{foo}; }; ReactDOM.render( - + , container @@ -181,26 +191,31 @@ describe('hooks', () => { }); }); - describe('useMutations', () => { - test('useMutations hook returns mutations that can update state', () => { - const store = createStore< + describe('useTransitions', () => { + test('useTransitions hook returns mutations that can update state', () => { + const { store } = create< { cnt: number; }, + any + >( { - increment: (value: number) => void; + cnt: 0, + }, + { + increment: (state: { cnt: number }) => (value: number) => ({ + ...state, + cnt: state.cnt + value, + }), } - >({ - cnt: 0, - }); - store.createMutators({ - increment: state => value => ({ ...state, cnt: state.cnt + value }), - }); + ); - const { Provider, useState, useMutators } = createContext(store); + const { Provider, useState, useTransitions } = createStateContainerReactHelpers< + typeof store + >(); const Demo: React.FC<{}> = () => { const { cnt } = useState(); - const { increment } = useMutators(); + const { increment } = useTransitions(); return ( <> {cnt} @@ -210,7 +225,7 @@ describe('hooks', () => { }; ReactDOM.render( - + , container @@ -230,7 +245,7 @@ describe('hooks', () => { describe('useSelector', () => { test('can select deeply nested value', () => { - const store = createStore({ + const { store } = create({ foo: { bar: { baz: 'qux', @@ -238,14 +253,14 @@ describe('hooks', () => { }, }); const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const value = useSelector(selector); return <>{value}; }; ReactDOM.render( - + , container @@ -255,7 +270,7 @@ describe('hooks', () => { }); test('re-renders when state changes', () => { - const store = createStore({ + const { store, mutators } = create({ foo: { bar: { baz: 'qux', @@ -263,14 +278,14 @@ describe('hooks', () => { }, }); const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const value = useSelector(selector); return <>{value}; }; ReactDOM.render( - + , container @@ -278,7 +293,7 @@ describe('hooks', () => { expect(container!.innerHTML).toBe('qux'); act(() => { - store.set({ + mutators.set({ foo: { bar: { baz: 'quux', @@ -290,9 +305,9 @@ describe('hooks', () => { }); test("re-renders only when selector's result changes", async () => { - const store = createStore({ a: 'b', foo: 'bar' }); + const { store, mutators } = create({ a: 'b', foo: 'bar' }); const selector = (state: { foo: string }) => state.foo; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -301,7 +316,7 @@ describe('hooks', () => { return <>{value}; }; ReactDOM.render( - + , container @@ -311,24 +326,24 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ a: 'c', foo: 'bar' }); + mutators.set({ a: 'c', foo: 'bar' }); }); await new Promise(r => setTimeout(r, 1)); expect(cnt).toBe(1); act(() => { - store.set({ a: 'd', foo: 'bar 2' }); + mutators.set({ a: 'd', foo: 'bar 2' }); }); await new Promise(r => setTimeout(r, 1)); expect(cnt).toBe(2); }); - test('re-renders on same shape object', async () => { - const store = createStore({ foo: { bar: 'baz' } }); + test('does not re-render on same shape object', async () => { + const { store, mutators } = create({ foo: { bar: 'baz' } }); const selector = (state: { foo: any }) => state.foo; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -337,7 +352,7 @@ describe('hooks', () => { return <>{JSON.stringify(value)}; }; ReactDOM.render( - + , container @@ -347,7 +362,14 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ foo: { bar: 'baz' } }); + mutators.set({ foo: { bar: 'baz' } }); + }); + + await new Promise(r => setTimeout(r, 1)); + expect(cnt).toBe(1); + + act(() => { + mutators.set({ foo: { bar: 'qux' } }); }); await new Promise(r => setTimeout(r, 1)); @@ -355,10 +377,15 @@ describe('hooks', () => { }); test('can set custom comparator function to prevent re-renders on deep equality', async () => { - const store = createStore({ foo: { bar: 'baz' } }); + const { store, mutators } = create( + { foo: { bar: 'baz' } }, + { + set: () => (newState: { foo: { bar: string } }) => newState, + } + ); const selector = (state: { foo: any }) => state.foo; const comparator = (prev: any, curr: any) => JSON.stringify(prev) === JSON.stringify(curr); - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -367,7 +394,7 @@ describe('hooks', () => { return <>{JSON.stringify(value)}; }; ReactDOM.render( - + , container @@ -377,7 +404,7 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ foo: { bar: 'baz' } }); + mutators.set({ foo: { bar: 'baz' } }); }); await new Promise(r => setTimeout(r, 1)); diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts new file mode 100644 index 0000000000000..e94165cc48376 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts @@ -0,0 +1,77 @@ +/* + * 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 * as React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import defaultComparator from 'fast-deep-equal'; +import { Comparator, Connect, StateContainer, UnboxState } from './types'; + +const { useContext, useLayoutEffect, useRef, createElement: h } = React; + +export const createStateContainerReactHelpers = >() => { + const context = React.createContext(null as any); + + const useContainer = (): Container => useContext(context); + + const useState = (): UnboxState => { + const { state$, get } = useContainer(); + const value = useObservable(state$, get()); + return value; + }; + + const useTransitions = () => useContainer().transitions; + + const useSelector = ( + selector: (state: UnboxState) => Result, + comparator: Comparator = defaultComparator + ): Result => { + const { state$, get } = useContainer(); + const lastValueRef = useRef(get()); + const [value, setValue] = React.useState(() => { + const newValue = selector(get()); + lastValueRef.current = newValue; + return newValue; + }); + useLayoutEffect(() => { + const subscription = state$.subscribe((currentState: UnboxState) => { + const newValue = selector(currentState); + if (!comparator(lastValueRef.current, newValue)) { + lastValueRef.current = newValue; + setValue(newValue); + } + }); + return () => subscription.unsubscribe(); + }, [state$, comparator]); + return value; + }; + + const connect: Connect> = mapStateToProp => component => props => + h(component, { ...useSelector(mapStateToProp), ...props } as any); + + return { + Provider: context.Provider, + Consumer: context.Consumer, + context, + useContainer, + useState, + useTransitions, + useSelector, + connect, + }; +}; diff --git a/src/plugins/kibana_utils/public/state_containers/index.ts b/src/plugins/kibana_utils/public/state_containers/index.ts new file mode 100644 index 0000000000000..43e204ecb79f7 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './types'; +export * from './create_state_container'; +export * from './create_state_container_react_helpers'; diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/public/state_containers/types.ts new file mode 100644 index 0000000000000..e0a1a18972635 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/types.ts @@ -0,0 +1,99 @@ +/* + * 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 { Observable } from 'rxjs'; +import { Ensure, RecursiveReadonly } from '@kbn/utility-types'; + +export interface TransitionDescription { + type: Type; + args: Args; +} +export type Transition = (...args: Args) => State; +export type PureTransition = ( + state: RecursiveReadonly +) => Transition; +export type EnsurePureTransition = Ensure>; +export type PureTransitionToTransition> = ReturnType; +export type PureTransitionsToTransitions = { + [K in keyof T]: PureTransitionToTransition>; +}; + +export interface BaseStateContainer { + get: () => RecursiveReadonly; + set: (state: State) => void; + state$: Observable>; +} + +export interface StateContainer< + State, + PureTransitions extends object, + PureSelectors extends object = {} +> extends BaseStateContainer { + transitions: Readonly>; + selectors: Readonly>; +} + +export interface ReduxLikeStateContainer< + State, + PureTransitions extends object, + PureSelectors extends object = {} +> extends StateContainer { + getState: () => RecursiveReadonly; + reducer: Reducer>; + replaceReducer: (nextReducer: Reducer>) => void; + dispatch: (action: TransitionDescription) => void; + addMiddleware: (middleware: Middleware>) => void; + subscribe: (listener: (state: RecursiveReadonly) => void) => () => void; +} + +export type Dispatch = (action: T) => void; + +export type Middleware = ( + store: Pick, 'getState' | 'dispatch'> +) => ( + next: (action: TransitionDescription) => TransitionDescription | any +) => Dispatch; + +export type Reducer = (state: State, action: TransitionDescription) => State; + +export type UnboxState< + Container extends StateContainer +> = Container extends StateContainer ? T : never; +export type UnboxTransitions< + Container extends StateContainer +> = Container extends StateContainer ? T : never; + +export type Selector = (...args: Args) => Result; +export type PureSelector = ( + state: State +) => Selector; +export type EnsurePureSelector = Ensure>; +export type PureSelectorToSelector> = ReturnType< + EnsurePureSelector +>; +export type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; + +export type Comparator = (previous: Result, current: Result) => boolean; + +export type MapStateToProps = (state: State) => StateProps; +export type Connect = ( + mapStateToProp: MapStateToProps> +) => (component: React.ComponentType) => React.FC>; diff --git a/src/plugins/kibana_utils/public/store/index.ts b/src/plugins/kibana_utils/public/state_management/state_hash/index.ts similarity index 93% rename from src/plugins/kibana_utils/public/store/index.ts rename to src/plugins/kibana_utils/public/state_management/state_hash/index.ts index 468e8ab8c5ade..0e52c4c55872d 100644 --- a/src/plugins/kibana_utils/public/store/index.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './create_store'; -export * from './react'; +export * from './state_hash'; diff --git a/src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts similarity index 93% rename from src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts rename to src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts index 5cbf0c8fdcfc5..cccb74acaf1e5 100644 --- a/src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts @@ -18,12 +18,12 @@ */ import { encode as encodeRison } from 'rison-node'; -import { mockSessionStorage } from '../state_storage/mock'; -import { createStateHash, isStateHash } from '../state_hashing'; +import { mockStorage } from '../../storage/hashed_item_store/mock'; +import { createStateHash, isStateHash } from './state_hash'; describe('stateHash', () => { beforeEach(() => { - mockSessionStorage.clear(); + mockStorage.clear(); }); describe('#createStateHash', () => { diff --git a/src/legacy/ui/public/state_management/state_hashing/state_hash.ts b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts similarity index 94% rename from src/legacy/ui/public/state_management/state_hashing/state_hash.ts rename to src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts index a7a75eec63ccb..a3eb5272b112d 100644 --- a/src/legacy/ui/public/state_management/state_hashing/state_hash.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts @@ -18,7 +18,7 @@ */ import { Sha256 } from '../../../../../core/public/utils'; -import { HashedItemStoreSingleton } from '../state_storage'; +import { hashedItemStore } from '../../storage/hashed_item_store'; // This prefix is used to identify hash strings that have been encoded in the URL. const HASH_PREFIX = 'h@'; @@ -42,7 +42,7 @@ export function createStateHash( shortenedHash = hash.slice(0, i); const existingJson = existingJsonProvider ? existingJsonProvider(shortenedHash) - : HashedItemStoreSingleton.getItem(shortenedHash); + : hashedItemStore.getItem(shortenedHash); if (existingJson === null || existingJson === json) break; } diff --git a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts similarity index 87% rename from src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts rename to src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts index 8a298be8636c8..a85158acddefd 100644 --- a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts @@ -17,14 +17,14 @@ * under the License. */ -import { mockSessionStorage } from '../state_storage/mock'; -import { HashedItemStore } from '../state_storage/hashed_item_store'; +import { mockStorage } from '../../storage/hashed_item_store/mock'; +import { HashedItemStore } from '../../storage/hashed_item_store'; import { hashUrl, unhashUrl } from './hash_unhash_url'; describe('hash unhash url', () => { beforeEach(() => { - mockSessionStorage.clear(); - mockSessionStorage.setStubbedSizeLimit(5000000); + mockStorage.clear(); + mockStorage.setStubbedSizeLimit(5000000); }); describe('hash url', () => { @@ -96,8 +96,8 @@ describe('hash unhash url', () => { expect(result).toMatchInlineSnapshot( `"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02"` ); - expect(mockSessionStorage.getItem('kbn.hashedItemsIndex.v1')).toBeTruthy(); - expect(mockSessionStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true })); + expect(mockStorage.getItem('kbn.hashedItemsIndex.v1')).toBeTruthy(); + expect(mockStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true })); }); it('if uses multiple states params', () => { @@ -112,15 +112,15 @@ describe('hash unhash url', () => { expect(result).toMatchInlineSnapshot( `"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02&_a=h@61fa078&_b=(yes:!f)"` ); - expect(mockSessionStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true })); - expect(mockSessionStorage.getItem('h@61fa078')).toEqual(JSON.stringify({ yes: false })); + expect(mockStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true })); + expect(mockStorage.getItem('h@61fa078')).toEqual(JSON.stringify({ yes: false })); if (!HashedItemStore.PERSISTED_INDEX_KEY) { // This is very brittle and depends upon HashedItemStore implementation details, // so let's protect ourselves from accidentally breaking this test. throw new Error('Missing HashedItemStore.PERSISTED_INDEX_KEY'); } - expect(mockSessionStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY)).toBeTruthy(); - expect(mockSessionStorage.length).toBe(3); + expect(mockStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY)).toBeTruthy(); + expect(mockStorage.length).toBe(3); }); it('hashes only whitelisted properties', () => { @@ -136,14 +136,14 @@ describe('hash unhash url', () => { `"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02&_a=h@61fa078&_someother=(yes:!f)"` ); - expect(mockSessionStorage.length).toBe(3); // 2 hashes + HashedItemStoreSingleton.PERSISTED_INDEX_KEY + expect(mockStorage.length).toBe(3); // 2 hashes + HashedItemStoreSingleton.PERSISTED_INDEX_KEY }); }); it('throws error if unable to hash url', () => { const stateParamKey1 = '_g'; const stateParamValue1 = '(yes:!t)'; - mockSessionStorage.setStubbedSizeLimit(1); + mockStorage.setStubbedSizeLimit(1); const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValue1}`; expect(() => hashUrl(url)).toThrowError(); @@ -206,7 +206,7 @@ describe('hash unhash url', () => { const stateParamKey = '_g'; const stateParamValueHashed = 'h@4e60e02'; const state = { yes: true }; - mockSessionStorage.setItem(stateParamValueHashed, JSON.stringify(state)); + mockStorage.setItem(stateParamValueHashed, JSON.stringify(state)); const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey}=${stateParamValueHashed}`; const result = unhashUrl(url); @@ -224,8 +224,8 @@ describe('hash unhash url', () => { const stateParamValueHashed2 = 'h@61fa078'; const state2 = { yes: false }; - mockSessionStorage.setItem(stateParamValueHashed1, JSON.stringify(state1)); - mockSessionStorage.setItem(stateParamValueHashed2, JSON.stringify(state2)); + mockStorage.setItem(stateParamValueHashed1, JSON.stringify(state1)); + mockStorage.setItem(stateParamValueHashed2, JSON.stringify(state2)); const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValueHashed1}&${stateParamKey2}=${stateParamValueHashed2}`; const result = unhashUrl(url); @@ -247,9 +247,9 @@ describe('hash unhash url', () => { const stateParamValueHashed3 = 'h@61fa078'; const state3 = { yes: false }; - mockSessionStorage.setItem(stateParamValueHashed1, JSON.stringify(state1)); - mockSessionStorage.setItem(stateParamValueHashed2, JSON.stringify(state2)); - mockSessionStorage.setItem(stateParamValueHashed3, JSON.stringify(state3)); + mockStorage.setItem(stateParamValueHashed1, JSON.stringify(state1)); + mockStorage.setItem(stateParamValueHashed2, JSON.stringify(state2)); + mockStorage.setItem(stateParamValueHashed3, JSON.stringify(state3)); const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValueHashed1}&${stateParamKey2}=${stateParamValueHashed2}&${stateParamKey3}=${stateParamValueHashed3}`; const result = unhashUrl(url); diff --git a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts similarity index 92% rename from src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts rename to src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts index 345924b92467d..872e7953f938b 100644 --- a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts @@ -22,8 +22,8 @@ import rison, { RisonObject } from 'rison-node'; import { stringify as stringifyQueryString } from 'querystring'; import encodeUriQuery from 'encode-uri-query'; import { format as formatUrl, parse as parseUrl } from 'url'; -import { HashedItemStoreSingleton } from '../state_storage'; -import { createStateHash, isStateHash } from './state_hash'; +import { hashedItemStore } from '../../storage/hashed_item_store'; +import { createStateHash, isStateHash } from '../state_hash'; export type IParsedUrlQuery = Record; @@ -95,10 +95,10 @@ function createQueryReplacer( // src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts // maybe to become simplified stateless version export function retrieveState(stateHash: string): RisonObject { - const json = HashedItemStoreSingleton.getItem(stateHash); + const json = hashedItemStore.getItem(stateHash); const throwUnableToRestoreUrlError = () => { throw new Error( - i18n.translate('common.ui.stateManagement.unableToRestoreUrlErrorMessage', { + i18n.translate('kibana_utils.stateManagement.url.unableToRestoreUrlErrorMessage', { defaultMessage: 'Unable to completely restore the URL, be sure to use the share functionality.', }) @@ -121,11 +121,11 @@ export function persistState(state: RisonObject): string { const json = JSON.stringify(state); const hash = createStateHash(json); - const isItemSet = HashedItemStoreSingleton.setItem(hash, json); + const isItemSet = hashedItemStore.setItem(hash, json); if (isItemSet) return hash; // If we ran out of space trying to persist the state, notify the user. const message = i18n.translate( - 'common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage', + 'kibana_utils.stateManagement.url.unableToStoreHistoryInSessionErrorMessage', { defaultMessage: 'Kibana is unable to store history items in your session ' + diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts new file mode 100644 index 0000000000000..30c5696233db7 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export * from './hash_unhash_url'; diff --git a/src/legacy/ui/public/state_management/state_storage/hashed_item_store.test.ts b/src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.test.ts similarity index 79% rename from src/legacy/ui/public/state_management/state_storage/hashed_item_store.test.ts rename to src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.test.ts index 3eb4e045f2cf1..f0ff77d516270 100644 --- a/src/legacy/ui/public/state_management/state_storage/hashed_item_store.test.ts +++ b/src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.test.ts @@ -27,7 +27,8 @@ describe('hashedItemStore', () => { const sessionStorage = new StubBrowserStorage(); const spy = jest.spyOn(sessionStorage, 'getItem'); - new HashedItemStore(sessionStorage); + const hashedItemStore = new HashedItemStore(sessionStorage); + (hashedItemStore as any).getIndexedItems(); // trigger retrieving of indexedItems array from HashedItemStore.PERSISTED_INDEX_KEY expect(spy).toBeCalledWith(HashedItemStore.PERSISTED_INDEX_KEY); spy.mockReset(); }); @@ -54,7 +55,7 @@ describe('hashedItemStore', () => { sessionStorage.setItem(HashedItemStore.PERSISTED_INDEX_KEY, JSON.stringify({ a, b, c })); const hashedItemStore = new HashedItemStore(sessionStorage); - expect((hashedItemStore as any).indexedItems).toEqual([a, c, b]); + expect((hashedItemStore as any).getIndexedItems()).toEqual([a, c, b]); }); }); @@ -260,6 +261,86 @@ describe('hashedItemStore', () => { }); }); }); + + describe('#removeItem', () => { + describe('if the item exists in sessionStorage', () => { + let sessionStorage: Storage; + let hashedItemStore: HashedItemStore; + + beforeEach(() => { + sessionStorage = new StubBrowserStorage(); + hashedItemStore = new HashedItemStore(sessionStorage); + hashedItemStore.setItem('1', 'a'); + hashedItemStore.setItem('2', 'b'); + }); + + it('removes and returns an item', () => { + const removedItem1 = hashedItemStore.removeItem('1'); + expect(removedItem1).toBe('a'); + expect(hashedItemStore.getItem('1')).toBeNull(); + expect(hashedItemStore.getItem('2')).not.toBeNull(); + expect((hashedItemStore as any).getIndexedItems()).toHaveLength(1); + + const removedItem2 = hashedItemStore.removeItem('2'); + expect(removedItem2).toBe('b'); + expect(hashedItemStore.getItem('1')).toBeNull(); + expect(hashedItemStore.getItem('2')).toBeNull(); + expect((hashedItemStore as any).getIndexedItems()).toHaveLength(0); + }); + }); + + describe(`if the item doesn't exist in sessionStorage`, () => { + let sessionStorage: Storage; + let hashedItemStore: HashedItemStore; + const hash = 'a'; + + beforeEach(() => { + sessionStorage = new StubBrowserStorage(); + hashedItemStore = new HashedItemStore(sessionStorage); + }); + + it('returns null', () => { + const removedItem = hashedItemStore.removeItem(hash); + expect(removedItem).toBe(null); + }); + }); + }); + + describe('#clear', () => { + describe('if the items exist in sessionStorage', () => { + let sessionStorage: Storage; + let hashedItemStore: HashedItemStore; + + beforeEach(() => { + sessionStorage = new StubBrowserStorage(); + hashedItemStore = new HashedItemStore(sessionStorage); + hashedItemStore.setItem('1', 'a'); + hashedItemStore.setItem('2', 'b'); + }); + + it('removes all items', () => { + hashedItemStore.clear(); + + expect(hashedItemStore.getItem('1')).toBeNull(); + expect(hashedItemStore.getItem('2')).toBeNull(); + expect((hashedItemStore as any).getIndexedItems()).toHaveLength(0); + }); + }); + + describe(`if items don't exist in sessionStorage`, () => { + let sessionStorage: Storage; + let hashedItemStore: HashedItemStore; + + beforeEach(() => { + sessionStorage = new StubBrowserStorage(); + hashedItemStore = new HashedItemStore(sessionStorage); + }); + + it("doesn't throw", () => { + expect(() => hashedItemStore.clear()).not.toThrowError(); + }); + }); + }); }); describe('behavior', () => { diff --git a/src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts b/src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.ts similarity index 70% rename from src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts rename to src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.ts index f49d993e15760..485aa643c4f01 100644 --- a/src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts +++ b/src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.ts @@ -71,34 +71,24 @@ */ import { pull, sortBy } from 'lodash'; +import { IStorage } from '../types'; interface IndexedItem { hash: string; touched?: number; // Date.now() } -export class HashedItemStore { +export class HashedItemStore implements IStorage { static readonly PERSISTED_INDEX_KEY = 'kbn.hashedItemsIndex.v1'; - private sessionStorage: Storage; - - // Store indexed items in descending order by touched (oldest first, newest last). We'll use - // this to remove older items when we run out of storage space. - private indexedItems: IndexedItem[] = []; + private storage: Storage; /** * HashedItemStore uses objects called indexed items to refer to items that have been persisted - * in sessionStorage. An indexed item is shaped {hash, touched}. The touched date is when the item + * in storage. An indexed item is shaped {hash, touched}. The touched date is when the item * was last referenced by the browser history. */ - constructor(sessionStorage: Storage) { - this.sessionStorage = sessionStorage; - - // Potentially restore a previously persisted index. This happens when - // we re-open a closed tab. - const persistedItemIndex = this.sessionStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY); - if (persistedItemIndex) { - this.indexedItems = sortBy(JSON.parse(persistedItemIndex) || [], 'touched'); - } + constructor(storage: Storage) { + this.storage = storage; } setItem(hash: string, item: string): boolean { @@ -112,7 +102,7 @@ export class HashedItemStore { } getItem(hash: string): string | null { - const item = this.sessionStorage.getItem(hash); + const item = this.storage.getItem(hash); if (item !== null) { this.touchHash(hash); @@ -121,17 +111,64 @@ export class HashedItemStore { return item; } - private getIndexedItem(hash: string) { - return this.indexedItems.find(indexedItem => indexedItem.hash === hash); + removeItem(hash: string): string | null { + const indexedItems = this.getIndexedItems(); + const itemToRemove = this.storage.getItem(hash); + const indexedItemToRemove = this.getIndexedItem(hash, indexedItems); + + if (indexedItemToRemove) { + pull(indexedItems, indexedItemToRemove); + this.setIndexedItems(indexedItems); + } + + if (itemToRemove) { + this.storage.removeItem(hash); + } + + return itemToRemove || null; + } + + clear() { + const indexedItems = this.getIndexedItems(); + indexedItems.forEach(({ hash }) => { + this.storage.removeItem(hash); + }); + this.setIndexedItems([]); + } + + // Store indexed items in descending order by touched (oldest first, newest last). We'll use + // this to remove older items when we run out of storage space. + private ensuredSorting = false; + private getIndexedItems(): IndexedItem[] { + // Restore a previously persisted index + const persistedItemIndex = this.storage.getItem(HashedItemStore.PERSISTED_INDEX_KEY); + let items = persistedItemIndex ? JSON.parse(persistedItemIndex) || [] : []; + + // ensure sorting once, as sorting all indexed items on each get is a performance hit + if (!this.ensuredSorting) { + items = sortBy(items, 'touched'); + this.setIndexedItems(items); + this.ensuredSorting = true; + } + + return items; + } + + private setIndexedItems(items: IndexedItem[]) { + this.storage.setItem(HashedItemStore.PERSISTED_INDEX_KEY, JSON.stringify(items)); + } + + private getIndexedItem(hash: string, indexedItems: IndexedItem[] = this.getIndexedItems()) { + return indexedItems.find(indexedItem => indexedItem.hash === hash); } private persistItem(hash: string, item: string): boolean { try { - this.sessionStorage.setItem(hash, item); + this.storage.setItem(hash, item); return true; } catch (e) { // If there was an error then we need to make some space for the item. - if (this.indexedItems.length === 0) { + if (this.getIndexedItems().length === 0) { // If there's nothing left to remove, then we've run out of space and we're trying to // persist too large an item. return false; @@ -147,31 +184,31 @@ export class HashedItemStore { } private removeOldestItem() { - const oldestIndexedItem = this.indexedItems.shift(); + const indexedItems = this.getIndexedItems(); + const oldestIndexedItem = indexedItems.shift(); if (oldestIndexedItem) { // Remove oldest item from storage. - this.sessionStorage.removeItem(oldestIndexedItem.hash); + this.storage.removeItem(oldestIndexedItem.hash); + this.setIndexedItems(indexedItems); } } private touchHash(hash: string) { + const indexedItems = this.getIndexedItems(); // Touching a hash indicates that it's been used recently, so it won't be the first in line // when we remove items to free up storage space. // either get or create an indexedItem - const indexedItem = this.getIndexedItem(hash) || { hash }; + const indexedItem = this.getIndexedItem(hash, indexedItems) || { hash }; // set/update the touched time to now so that it's the "newest" item in the index indexedItem.touched = Date.now(); // ensure that the item is last in the index - pull(this.indexedItems, indexedItem); - this.indexedItems.push(indexedItem); + pull(indexedItems, indexedItem); + indexedItems.push(indexedItem); // Regardless of whether this is a new or updated item, we need to persist the index. - this.sessionStorage.setItem( - HashedItemStore.PERSISTED_INDEX_KEY, - JSON.stringify(this.indexedItems) - ); + this.setIndexedItems(indexedItems); } } diff --git a/src/legacy/ui/public/state_management/state_storage/hashed_item_store_singleton.ts b/src/plugins/kibana_utils/public/storage/hashed_item_store/index.ts similarity index 89% rename from src/legacy/ui/public/state_management/state_storage/hashed_item_store_singleton.ts rename to src/plugins/kibana_utils/public/storage/hashed_item_store/index.ts index 234559c95ebf7..062266359c6c5 100644 --- a/src/legacy/ui/public/state_management/state_storage/hashed_item_store_singleton.ts +++ b/src/plugins/kibana_utils/public/storage/hashed_item_store/index.ts @@ -18,5 +18,5 @@ */ import { HashedItemStore } from './hashed_item_store'; - -export const HashedItemStoreSingleton = new HashedItemStore(window.sessionStorage); +export { HashedItemStore }; +export const hashedItemStore = new HashedItemStore(window.sessionStorage); diff --git a/src/legacy/ui/public/state_management/state_storage/mock.ts b/src/plugins/kibana_utils/public/storage/hashed_item_store/mock.ts similarity index 82% rename from src/legacy/ui/public/state_management/state_storage/mock.ts rename to src/plugins/kibana_utils/public/storage/hashed_item_store/mock.ts index 59700cb369e6e..e3360e0e3cf51 100644 --- a/src/legacy/ui/public/state_management/state_storage/mock.ts +++ b/src/plugins/kibana_utils/public/storage/hashed_item_store/mock.ts @@ -28,10 +28,11 @@ import { HashedItemStore } from './hashed_item_store'; * And all tests in the test file will use HashedItemStoreSingleton * with underlying mockSessionStorage we have access to */ -export const mockSessionStorage = new StubBrowserStorage(); -const mockHashedItemStore = new HashedItemStore(mockSessionStorage); -jest.mock('../state_storage', () => { +export const mockStorage = new StubBrowserStorage(); +const mockHashedItemStore = new HashedItemStore(mockStorage); +jest.mock('./', () => { return { - HashedItemStoreSingleton: mockHashedItemStore, + HashedItemStore: require('./hashed_item_store').HashedItemStore, + hashedItemStore: mockHashedItemStore, }; }); diff --git a/src/plugins/kibana_utils/public/storage/types.ts b/src/plugins/kibana_utils/public/storage/types.ts index 875bb44bcad17..a25d4729fd320 100644 --- a/src/plugins/kibana_utils/public/storage/types.ts +++ b/src/plugins/kibana_utils/public/storage/types.ts @@ -17,16 +17,16 @@ * under the License. */ -export interface IStorageWrapper { - get: (key: string) => any; - set: (key: string, value: any) => void; - remove: (key: string) => any; +export interface IStorageWrapper { + get: (key: string) => T | null; + set: (key: string, value: T) => S; + remove: (key: string) => T | null; clear: () => void; } -export interface IStorage { - getItem: (key: string) => any; - setItem: (key: string, value: any) => void; - removeItem: (key: string) => any; +export interface IStorage { + getItem: (key: string) => T | null; + setItem: (key: string, value: T) => S; + removeItem: (key: string) => T | null; clear: () => void; } diff --git a/src/plugins/kibana_utils/public/store/create_store.test.ts b/src/plugins/kibana_utils/public/store/create_store.test.ts deleted file mode 100644 index cfdeb76254003..0000000000000 --- a/src/plugins/kibana_utils/public/store/create_store.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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 { createStore } from './create_store'; - -test('can create store', () => { - const store = createStore({}); - expect(store).toMatchObject({ - get: expect.any(Function), - set: expect.any(Function), - state$: expect.any(Object), - createMutators: expect.any(Function), - mutators: expect.any(Object), - redux: { - getState: expect.any(Function), - dispatch: expect.any(Function), - subscribe: expect.any(Function), - }, - }); -}); - -test('can set default state', () => { - const defaultState = { - foo: 'bar', - }; - const store = createStore(defaultState); - expect(store.get()).toEqual(defaultState); - expect(store.redux.getState()).toEqual(defaultState); -}); - -test('can set state', () => { - const defaultState = { - foo: 'bar', - }; - const newState = { - foo: 'baz', - }; - const store = createStore(defaultState); - - store.set(newState); - - expect(store.get()).toEqual(newState); - expect(store.redux.getState()).toEqual(newState); -}); - -test('does not shallow merge states', () => { - const defaultState = { - foo: 'bar', - }; - const newState = { - foo2: 'baz', - }; - const store = createStore(defaultState); - - store.set(newState); - - expect(store.get()).toEqual(newState); - expect(store.redux.getState()).toEqual(newState); -}); - -test('can subscribe and unsubscribe to state changes', () => { - const store = createStore({}); - const spy = jest.fn(); - const subscription = store.state$.subscribe(spy); - store.set({ a: 1 }); - store.set({ a: 2 }); - subscription.unsubscribe(); - store.set({ a: 3 }); - - expect(spy).toHaveBeenCalledTimes(2); - expect(spy.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy.mock.calls[1][0]).toEqual({ a: 2 }); -}); - -test('multiple subscribers can subscribe', () => { - const store = createStore({}); - const spy1 = jest.fn(); - const spy2 = jest.fn(); - const subscription1 = store.state$.subscribe(spy1); - const subscription2 = store.state$.subscribe(spy2); - store.set({ a: 1 }); - subscription1.unsubscribe(); - store.set({ a: 2 }); - subscription2.unsubscribe(); - store.set({ a: 3 }); - - expect(spy1).toHaveBeenCalledTimes(1); - expect(spy2).toHaveBeenCalledTimes(2); - expect(spy1.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy2.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy2.mock.calls[1][0]).toEqual({ a: 2 }); -}); - -test('creates impure mutators from pure mutators', () => { - const store = createStore({}); - const mutators = store.createMutators({ - setFoo: _ => bar => ({ foo: bar }), - }); - - expect(typeof mutators.setFoo).toBe('function'); -}); - -test('mutators can update state', () => { - const store = createStore({ - value: 0, - foo: 'bar', - }); - const mutators = store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - setFoo: state => bar => ({ ...state, foo: bar }), - }); - - expect(store.get()).toEqual({ - value: 0, - foo: 'bar', - }); - - mutators.add(11); - mutators.setFoo('baz'); - - expect(store.get()).toEqual({ - value: 11, - foo: 'baz', - }); - - mutators.add(-20); - mutators.setFoo('bazooka'); - - expect(store.get()).toEqual({ - value: -9, - foo: 'bazooka', - }); -}); - -test('mutators methods are not bound', () => { - const store = createStore({ value: -3 }); - const { add } = store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - }); - - expect(store.get()).toEqual({ value: -3 }); - add(4); - expect(store.get()).toEqual({ value: 1 }); -}); - -test('created mutators are saved in store object', () => { - const store = createStore< - any, - { - add: (increment: number) => void; - } - >({ value: -3 }); - - store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - }); - - expect(typeof store.mutators.add).toBe('function'); - store.mutators.add(5); - expect(store.get()).toEqual({ value: 2 }); -}); diff --git a/src/plugins/kibana_utils/public/store/create_store.ts b/src/plugins/kibana_utils/public/store/create_store.ts deleted file mode 100644 index 315523360f92d..0000000000000 --- a/src/plugins/kibana_utils/public/store/create_store.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 { createStore as createReduxStore, Reducer } from 'redux'; -import { Subject, Observable } from 'rxjs'; -import { AppStore, Mutators, PureMutators } from './types'; - -const SET = '__SET__'; - -export const createStore = < - State extends {}, - StateMutators extends Mutators> = {} ->( - defaultState: State -): AppStore => { - const pureMutators: PureMutators = {}; - const mutators: StateMutators = {} as StateMutators; - const reducer: Reducer = (state, action) => { - const pureMutator = pureMutators[action.type]; - if (pureMutator) { - return pureMutator(state)(...action.args); - } - - switch (action.type) { - case SET: - return action.state; - default: - return state; - } - }; - const redux = createReduxStore(reducer, defaultState as any); - - const get = redux.getState; - - const set = (state: State) => - redux.dispatch({ - type: SET, - state, - }); - - const state$ = new Subject(); - redux.subscribe(() => { - state$.next(get()); - }); - - const createMutators: AppStore['createMutators'] = newPureMutators => { - const result: Mutators = {}; - for (const type of Object.keys(newPureMutators)) { - result[type] = (...args) => { - redux.dispatch({ - type, - args, - }); - }; - } - Object.assign(pureMutators, newPureMutators); - Object.assign(mutators, result); - return result; - }; - - return { - get, - set, - redux, - state$: (state$ as unknown) as Observable, - createMutators, - mutators, - }; -}; diff --git a/src/plugins/kibana_utils/public/store/observable_selector.ts b/src/plugins/kibana_utils/public/store/observable_selector.ts deleted file mode 100644 index 6ba6f42296a6c..0000000000000 --- a/src/plugins/kibana_utils/public/store/observable_selector.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { Observable, BehaviorSubject } from 'rxjs'; - -export type Selector = (state: State) => Result; -export type Comparator = (previous: Result, current: Result) => boolean; -export type Unsubscribe = () => void; - -const defaultComparator: Comparator = (previous, current) => previous === current; - -export const observableSelector = ( - state: State, - state$: Observable, - selector: Selector, - comparator: Comparator = defaultComparator -): [Observable, Unsubscribe] => { - let previousResult: Result = selector(state); - const result$ = new BehaviorSubject(previousResult); - - const subscription = state$.subscribe(value => { - const result = selector(value); - const isEqual: boolean = comparator(previousResult, result); - if (!isEqual) { - result$.next(result); - } - previousResult = result; - }); - - return [(result$ as unknown) as Observable, subscription.unsubscribe]; -}; diff --git a/src/plugins/kibana_utils/public/store/react.ts b/src/plugins/kibana_utils/public/store/react.ts deleted file mode 100644 index 00861b2b0b8fe..0000000000000 --- a/src/plugins/kibana_utils/public/store/react.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 * as React from 'react'; -import { Provider as ReactReduxProvider, connect as reactReduxConnect } from 'react-redux'; -import { Store } from 'redux'; -import { AppStore, Mutators, PureMutators } from './types'; -import { observableSelector, Selector, Comparator } from './observable_selector'; -// TODO: Below import is temporary, use `react-use` lib instead. -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { useObservable } from '../../../kibana_react/public/util/use_observable'; - -const { useMemo, useLayoutEffect, useContext, createElement, Fragment } = React; - -/** - * @note - * Types in `react-redux` seem to be quite off compared to reality - * that's why a lot of `any`s below. - */ - -export interface ConsumerProps { - children: (state: State) => React.ReactChild; -} - -export type MapStateToProps = (state: State) => StateProps; - -// TODO: `Omit` is generally part of TypeScript, but it currently does not exist in our build. -type Omit = Pick>; -export type Connect = ( - mapStateToProp: MapStateToProps> -) => (component: React.ComponentType) => React.FC>; - -interface ReduxContextValue { - store: Store; -} - -const mapDispatchToProps = () => ({}); -const mergeProps: any = (stateProps: any, dispatchProps: any, ownProps: any) => ({ - ...ownProps, - ...stateProps, - ...dispatchProps, -}); - -export const createContext = < - State extends {}, - StateMutators extends Mutators> = {} ->( - store: AppStore -) => { - const { redux } = store; - (redux as any).__appStore = store; - const context = React.createContext({ store: redux }); - - const useStore = (): AppStore => { - // eslint-disable-next-line no-shadow - const { store } = useContext(context); - return (store as any).__appStore; - }; - - const useState = (): State => { - const { state$, get } = useStore(); - const state = useObservable(state$, get()); - return state; - }; - - const useMutators = (): StateMutators => useStore().mutators; - - const useSelector = ( - selector: Selector, - comparator?: Comparator - ): Result => { - const { state$, get } = useStore(); - /* eslint-disable react-hooks/exhaustive-deps */ - const [observable$, unsubscribe] = useMemo( - () => observableSelector(get(), state$, selector, comparator), - [state$] - ); - /* eslint-enable react-hooks/exhaustive-deps */ - useLayoutEffect(() => unsubscribe, [observable$, unsubscribe]); - const value = useObservable(observable$, selector(get())); - return value; - }; - - const Provider: React.FC<{}> = ({ children }) => - createElement(ReactReduxProvider, { - store: redux, - context, - children, - } as any); - - const Consumer: React.FC> = ({ children }) => { - const state = useState(); - return createElement(Fragment, { children: children(state) }); - }; - - const options: any = { context }; - const connect: Connect = mapStateToProps => - reactReduxConnect(mapStateToProps, mapDispatchToProps, mergeProps, options) as any; - - return { - Provider, - Consumer, - connect, - context, - useStore, - useState, - useMutators, - useSelector, - }; -}; diff --git a/src/plugins/kibana_utils/public/store/types.ts b/src/plugins/kibana_utils/public/store/types.ts deleted file mode 100644 index 952ee07f18baf..0000000000000 --- a/src/plugins/kibana_utils/public/store/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { Observable } from 'rxjs'; -import { Store as ReduxStore } from 'redux'; - -export interface AppStore< - State extends {}, - StateMutators extends Mutators> = {} -> { - redux: ReduxStore; - get: () => State; - set: (state: State) => void; - state$: Observable; - createMutators: >(pureMutators: M) => Mutators; - mutators: StateMutators; -} - -export type PureMutator = (state: State) => (...args: any[]) => State; -export type Mutator> = (...args: Parameters>) => void; - -export interface PureMutators { - [name: string]: PureMutator; -} - -export type Mutators> = { [K in keyof M]: Mutator }; diff --git a/src/test_utils/public/stub_index_pattern.js b/src/test_utils/public/stub_index_pattern.js index b76da4b4eca3e..f4659ffa120d4 100644 --- a/src/test_utils/public/stub_index_pattern.js +++ b/src/test_utils/public/stub_index_pattern.js @@ -21,20 +21,26 @@ import sinon from 'sinon'; // TODO: We should not be importing from the data plugin directly here; this is only necessary // because it is one of the few places that we need to access the IndexPattern class itself, rather // than just the type. Doing this as a temporary measure; it will be left behind when migrating to NP. -import { IndexPattern } from '../../legacy/core_plugins/data/public/'; + import { FieldList, - getRoutes, - formatHitProvider, - flattenHitWrapper, -} from 'ui/index_patterns'; -import { FIELD_FORMAT_IDS, + IndexPattern, + indexPatterns, } from '../../plugins/data/public'; +import { setFieldFormats } from '../../plugins/data/public/services'; + +setFieldFormats({ + getDefaultInstance: () => ({ + getConverterFor: () => value => value, + convert: value => JSON.stringify(value) + }), +}); + import { getFieldFormatsRegistry } from './stub_field_formats'; -export default function StubIndexPattern(pattern, getConfig, timeField, fields, uiSettings) { +export default function StubIndexPattern(pattern, getConfig, timeField, fields, uiSettings) { const registeredFieldFormats = getFieldFormatsRegistry(uiSettings); this.id = pattern; @@ -49,11 +55,11 @@ export default function StubIndexPattern(pattern, getConfig, timeField, fields, this.getSourceFiltering = sinon.stub(); this.metaFields = ['_id', '_type', '_source']; this.fieldFormatMap = {}; - this.routes = getRoutes(); + this.routes = indexPatterns.getRoutes(); this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); - this.flattenHit = flattenHitWrapper(this, this.metaFields); - this.formatHit = formatHitProvider(this, registeredFieldFormats.getDefaultInstance(FIELD_FORMAT_IDS.STRING)); + this.flattenHit = indexPatterns.flattenHitWrapper(this, this.metaFields); + this.formatHit = indexPatterns.formatHitProvider(this, registeredFieldFormats.getDefaultInstance(FIELD_FORMAT_IDS.STRING)); this.fieldsFetcher = { apiClient: { baseUrl: '' } }; this.formatField = this.formatHit.formatField; diff --git a/style_guides/accessibility_guide.md b/style_guides/accessibility_guide.md index 28fb69ec38185..4827d93991510 100644 --- a/style_guides/accessibility_guide.md +++ b/style_guides/accessibility_guide.md @@ -1,59 +1,25 @@ # Accessibility (A11Y) Guide -This document provides some technical guidelines how to prevent several common -accessibility issues. +[EUI's accessibility guidelines](https://elastic.github.io/eui/#/guidelines/accessibility) should be your first stop for all things. -## Naming elements +## Automated accessibility testing -### `aria-label` and `aria-labelledby` +To run the tests locally: -Every element on a page will have a name, that is read out to an assistive technology -like a screen reader. This will for most elements be the content of the element. -For form elements it will be the content of the associated `
-``` - -### Don't create keyboard traps - -**TL;DR** *If you can't leave an element with Tab again, it needs a special interaction model.* - -If an interactive element consumes the Tab key (e.g. a code editor to -create an actual tabular indentation) it will prevent a keyboard user to leave -that element again. Also see [WCAG 2.1.2](https://www.w3.org/TR/WCAG20/#keyboard-operation-trapping). - -Those kind of elements, require a special interaction model. A [code editor](https://github.com/elastic/kibana/pull/13339) -could require an Enter keypress before starting editing mode, and -could leave that mode on Escape again. - -Unfortunately there is no universal solution for this problem, so be aware when creating -such elements, that would consume tabbing, to think about an accessible interaction -model. - -*Hint:* If you create that kind of interactive elements `role="application"` might -be a good `role` (also see below) for that element. It is meant for elements providing -their own interaction schemes. - -## Roles - -Each DOM element has an implicit role in the accessibility tree (that assistive technologies -use). The mapping of elements to default roles can be found in the -[Accessibility API Mappings](https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings). -You can overwrite this role via the `role` attribute on an element, and the -assistive technology will now behave the same like any other element with that role -(e.g. behave like it is a button when it has `role="button"`). - -### Landmark roles - -Some roles can be used to declare so called landmarks. These landmarks tag important -parts of a web page. Screen readers offer a quick way to jump to these -parts of the page (*landmark navigation*). - -#### role=main - -The `main` role (or equivalent using the `
` tag) declares the main part -of a page. This can be used in the landmark navigation to quickly jump to the -actual main area of the page (and skip all headers, navigations, etc.). - -#### `
` - -The `
` element, can be used to mark a region on the page, so that it -appears in the landmark navigation. The section element therefore needs to have -an *accessible name*, i.e. you should add an `aria-label`, that gives a short -title to that section of the page. - -### role=search - -**TL;DR** *Place `role="search"` neither on the `` nor the `
`, but -some `div` in between.* - -Role search can be used to mark a region as used for searching. This can be used -by assistive technologies to quickly find and navigate to this section. - -If you place it on the `input` you will overwrite the implicit `textbox` or `searchbox` -role, and as such confuse the user, since it loses it meaning as in input element. -If you place it on the `form` element you will also overwrite its role and -remove it from a quick jump navigation to all forms. - -That's why it should be placed to an `div` (or any other container) between the -`form` and the `input`. In most cases we already have a div there that you can -easily put this role to. - -**Related Links:** - -* [Where to put your search role?](http://adrianroselli.com/2015/08/where-to-put-your-search-role.html) -* Discussions about making `search` role inherit the `form` role: - [wcag/113](https://github.com/w3c/wcag/issues/113), - [html-aria/118](https://github.com/w3c/html-aria/issues/18), - [aria/85](https://github.com/w3c/aria/issues/85) diff --git a/test/api_integration/apis/general/csp.js b/test/api_integration/apis/general/csp.js index ae5cf27ff68ff..9bff9354d609f 100644 --- a/test/api_integration/apis/general/csp.js +++ b/test/api_integration/apis/general/csp.js @@ -37,8 +37,7 @@ export default function ({ getService }) { const entries = Array.from(parsed.entries()); expect(entries).to.eql([ [ 'script-src', [ '\'unsafe-eval\'', '\'self\'' ] ], - [ 'worker-src', [ 'blob:' ] ], - [ 'child-src', [ 'blob:' ] ], + [ 'worker-src', [ 'blob:', '\'self\'' ] ], [ 'style-src', [ '\'unsafe-inline\'', '\'self\'' ] ] ]); }); diff --git a/test/common/services/index.ts b/test/common/services/index.ts index 3454964f35e07..0a714e9875c37 100644 --- a/test/common/services/index.ts +++ b/test/common/services/index.ts @@ -23,6 +23,7 @@ import { EsArchiverProvider } from './es_archiver'; import { KibanaServerProvider } from './kibana_server'; import { RetryProvider } from './retry'; import { RandomnessProvider } from './randomness'; +import { SecurityServiceProvider } from './security'; export const services = { legacyEs: LegacyEsProvider, @@ -31,4 +32,5 @@ export const services = { kibanaServer: KibanaServerProvider, retry: RetryProvider, randomness: RandomnessProvider, + security: SecurityServiceProvider, }; diff --git a/test/common/services/security/index.ts b/test/common/services/security/index.ts new file mode 100644 index 0000000000000..ea83cfc390177 --- /dev/null +++ b/test/common/services/security/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { SecurityServiceProvider } from './security'; diff --git a/x-pack/test/common/services/security/role.ts b/test/common/services/security/role.ts similarity index 63% rename from x-pack/test/common/services/security/role.ts rename to test/common/services/security/role.ts index 341b7d0b877f1..0e7572882f80d 100644 --- a/x-pack/test/common/services/security/role.ts +++ b/test/common/services/security/role.ts @@ -1,8 +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. + * 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 axios, { AxiosInstance } from 'axios'; import util from 'util'; import { ToolingLog } from '@kbn/dev-utils'; diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts new file mode 100644 index 0000000000000..6649a765a9e50 --- /dev/null +++ b/test/common/services/security/security.ts @@ -0,0 +1,35 @@ +/* + * 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 { format as formatUrl } from 'url'; + +import { Role } from './role'; +import { User } from './user'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function SecurityServiceProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new (class SecurityService { + role = new Role(url, log); + user = new User(url, log); + })(); +} diff --git a/x-pack/test/common/services/security/user.ts b/test/common/services/security/user.ts similarity index 64% rename from x-pack/test/common/services/security/user.ts rename to test/common/services/security/user.ts index d591e6a651582..e1c9b3fb998ad 100644 --- a/x-pack/test/common/services/security/user.ts +++ b/test/common/services/security/user.ts @@ -1,8 +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. + * 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 axios, { AxiosInstance } from 'axios'; import util from 'util'; import { ToolingLog } from '@kbn/dev-utils'; diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 8fbc40f86e8dc..3ae8f51fb76dc 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const browser = getService('browser'); const defaultSettings = { defaultIndex: 'logstash-*', @@ -86,6 +87,17 @@ export default function ({ getService, getPageObjects }) { expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); }); + it('preserves the currently loaded query when the page is reloaded', async () => { + await browser.refresh(); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + expect(await PageObjects.discover.getHitCount()).to.be('2,792'); + expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); + }); + + it('allows saving changes to a currently loaded query via the saved query management component', async () => { await queryBar.setQuery('response:404'); await savedQueryManagementComponent.updateCurrentlyLoadedQuery( diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 937f703308881..ed45b3fbe069b 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -288,7 +288,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } async getSharedItemContainers() { - const cssSelector = '[data-shared-item-container]'; + const cssSelector = '[data-shared-items-container]'; return find.allByCssSelector(cssSelector); } diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 201354fff48ef..50303fbad7aab 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -136,6 +136,20 @@ async function attemptToCreateCommand( // See: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode firefoxOptions.headless(); } + + // Windows issue with stout socket https://github.com/elastic/kibana/issues/52053 + if (process.platform === 'win32') { + const session = await new Builder() + .forBrowser(browserType) + .setFirefoxOptions(firefoxOptions) + .setFirefoxService(new firefox.ServiceBuilder(geckoDriver.path)) + .build(); + return { + session, + consoleLog$: Rx.EMPTY, + }; + } + const { input, chunk$, cleanup } = await createStdoutSocket(); lifecycle.on('cleanup', cleanup); diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index d6de0be0c172e..9f0a8ded649b2 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -26,6 +26,15 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide const retry = getService('retry'); class SavedQueryManagementComponent { + public async getCurrentlyLoadedQueryID() { + await this.openSavedQueryManagementComponent(); + try { + return await testSubjects.getVisibleText('~saved-query-list-item-selected'); + } catch { + return undefined; + } + } + public async saveNewQuery( name: string, description: string, diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 51b9a5c5f1786..ae4fa1578fb2b 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "16.0.0", + "@elastic/eui": "17.0.0", "react": "^16.12.0", "react-dom": "^16.12.0" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index fd0ce478eb6fb..32da6eade2b42 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "16.0.0", + "@elastic/eui": "17.0.0", "react": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 98df7f4b246dc..0ab65667d6f82 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "16.0.0", + "@elastic/eui": "17.0.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 32f441ba6ccda..6f661ab2a79e1 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "16.0.0", + "@elastic/eui": "17.0.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/typings/encode_uri_query.d.ts b/test/typings/encode_uri_query.d.ts new file mode 100644 index 0000000000000..4bfc554624446 --- /dev/null +++ b/test/typings/encode_uri_query.d.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +declare module 'encode-uri-query' { + function encodeUriQuery(query: string, usePercentageSpace?: boolean): string; + // eslint-disable-next-line import/no-default-export + export default encodeUriQuery; +} diff --git a/test/typings/rison_node.d.ts b/test/typings/rison_node.d.ts new file mode 100644 index 0000000000000..2592c36e8ae9a --- /dev/null +++ b/test/typings/rison_node.d.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +declare module 'rison-node' { + export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; + + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface RisonArray extends Array {} + + export interface RisonObject { + [key: string]: RisonValue; + } + + export const decode: (input: string) => RisonValue; + + // eslint-disable-next-line @typescript-eslint/camelcase + export const decode_object: (input: string) => RisonObject; + + export const encode: (input: Input) => string; + + // eslint-disable-next-line @typescript-eslint/camelcase + export const encode_object: (input: Input) => string; +} diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts index bc9ded352faf8..fd31a4d8b6e4f 100644 --- a/test/visual_regression/services/visual_testing/visual_testing.ts +++ b/test/visual_regression/services/visual_testing/visual_testing.ts @@ -23,7 +23,7 @@ import _ from 'lodash'; import testSubjSelector from '@kbn/test-subj-selector'; -import { pkg } from '../../../../src/legacy/utils/package_json'; +import { pkg } from '../../../../src/core/server/utils'; import { FtrProviderContext } from '../../ftr_provider_context'; // @ts-ignore internal js that is passed to the browser as is diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 0058f21f2356b..77907a07addd1 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -31,15 +31,15 @@ def withWorkers(name, preWorkerClosure = {}, workerClosures = [:]) { } catchError { - runbld.junit() + runErrorReporter() } catchError { - publishJunit() + runbld.junit() } catchError { - runErrorReporter() + publishJunit() } } } @@ -103,10 +103,10 @@ def legacyJobRunner(name) { uploadAllGcsArtifacts(name) } catchError { - publishJunit() + runErrorReporter() } catchError { - runErrorReporter() + publishJunit() } } } @@ -262,10 +262,13 @@ def buildXpack() { } def runErrorReporter() { + def status = buildUtils.getBuildStatus() + def dryRun = status != "ABORTED" ? "" : "--no-github-update" + bash( """ source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests + node scripts/report_failed_tests ${dryRun} """, "Report failed tests, if necessary" ) diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6d0da2f0b693d..180aafe504c63 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -9,6 +9,7 @@ "xpack.canvas": "legacy/plugins/canvas", "xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication", "xpack.dashboardMode": "legacy/plugins/dashboard_mode", + "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "legacy/plugins/file_upload", "xpack.graph": "legacy/plugins/graph", @@ -18,20 +19,20 @@ "xpack.infra": "legacy/plugins/infra", "xpack.kueryAutocomplete": "legacy/plugins/kuery_autocomplete", "xpack.lens": "legacy/plugins/lens", - "xpack.licensing": "plugins/licensing", "xpack.licenseMgmt": "legacy/plugins/license_management", - "xpack.maps": "legacy/plugins/maps", - "xpack.ml": "legacy/plugins/ml", + "xpack.licensing": "plugins/licensing", "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", + "xpack.maps": "legacy/plugins/maps", + "xpack.ml": "legacy/plugins/ml", "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "legacy/plugins/remote_clusters", "xpack.reporting": [ "plugins/reporting", "legacy/plugins/reporting" ], "xpack.rollupJobs": "legacy/plugins/rollup", "xpack.searchProfiler": "legacy/plugins/searchprofiler", - "xpack.siem": "legacy/plugins/siem", "xpack.security": ["legacy/plugins/security", "plugins/security"], "xpack.server": "legacy/server", + "xpack.siem": "legacy/plugins/siem", "xpack.snapshotRestore": "legacy/plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", diff --git a/x-pack/README.md b/x-pack/README.md index 3f1fc819d145b..e92d6849538bb 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -19,7 +19,7 @@ By default, this will also set the password for native realm accounts to the pas | Test runner | Test location | Runner command (working directory is kibana/x-pack) | | ------------ | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | Jest | `x-pack/**/*.test.js`
`x-pack/**/*.test.ts` | `cd x-pack && node scripts/jest -t regexp [test path]` | -| Functional | `x-pack/test/*integration/**/config.js`
`x-pack/test/*functional/config.js` | `node scripts/functional_tests_server --config x-pack/test/[directory]/config.js`
`node scripts/functional_test_runner --config x-pack/test/[directory]/config.js --grep=regexp` | +| Functional | `x-pack/test/*integration/**/config.js`
`x-pack/test/*functional/config.js`
`x-pack/test/accessibility/config.js` | `node scripts/functional_tests_server --config x-pack/test/[directory]/config.js`
`node scripts/functional_test_runner --config x-pack/test/[directory]/config.js --grep=regexp` | Examples: - Run the jest test case whose description matches 'filtering should skip values of null': diff --git a/x-pack/index.js b/x-pack/index.js index 2b467d2525d9b..729c651ab399a 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -20,7 +20,6 @@ import { beats } from './legacy/plugins/beats_management'; import { apm } from './legacy/plugins/apm'; import { maps } from './legacy/plugins/maps'; import { licenseManagement } from './legacy/plugins/license_management'; -import { cloud } from './legacy/plugins/cloud'; import { indexManagement } from './legacy/plugins/index_management'; import { indexLifecycleManagement } from './legacy/plugins/index_lifecycle_management'; import { consoleExtensions } from './legacy/plugins/console_extensions'; @@ -64,7 +63,6 @@ module.exports = function (kibana) { maps(kibana), canvas(kibana), licenseManagement(kibana), - cloud(kibana), indexManagement(kibana), consoleExtensions(kibana), indexLifecycleManagement(kibana), diff --git a/x-pack/legacy/plugins/apm/cypress/ci/Dockerfile b/x-pack/legacy/plugins/apm/cypress/ci/Dockerfile new file mode 100644 index 0000000000000..e59e1f47da0b9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/cypress/ci/Dockerfile @@ -0,0 +1,23 @@ +FROM node:12 + +RUN apt-get -qq update \ + && apt-get -y -qq install xvfb \ + libgtk-3-0 \ + libxtst6 \ + libnotify-dev \ + libgconf-2-4 \ + libnss3 \ + libxss1 \ + libasound2 \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +## Add host.docker.internal to localhost +RUN apt-get -qq update \ + && apt-get -y -qq install dnsutils \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +COPY entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/x-pack/legacy/plugins/apm/cypress/ci/entrypoint.sh b/x-pack/legacy/plugins/apm/cypress/ci/entrypoint.sh new file mode 100755 index 0000000000000..1bcddac3b8020 --- /dev/null +++ b/x-pack/legacy/plugins/apm/cypress/ci/entrypoint.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -xe + +## host.docker.internal is not available in native docker installations +kibana=$(dig +short host.docker.internal) +if [ -z "${kibana}" ] ; then + kibana=127.0.0.1 +fi + +export CYPRESS_BASE_URL=http://${kibana}:5601 + +## To avoid issues with the home and caching artifacts +export HOME=/tmp +npm config set cache ${HOME} + +## To avoid issues with volumes. +#rsync -rv --exclude=.git --exclude=docs \ +# --exclude=.cache --exclude=node_modules \ +# --exclude=test/ \ +# --exclude=src/ \ +# --exclude=packages/ \ +# --exclude=built_assets --exclude=target \ +# --exclude=data /app ${HOME}/ +#cd ${HOME}/app/x-pack/legacy/plugins/apm/cypress + +cd /app/x-pack/legacy/plugins/apm/cypress +## Install dependencies for cypress +CI=true npm install +yarn install + +# Wait for the kibana to be up and running +npm install wait-on +./node_modules/.bin/wait-on ${CYPRESS_BASE_URL}/status && echo 'Kibana is up and running' + +# Run cypress +./node_modules/.bin/cypress run diff --git a/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml b/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml new file mode 100644 index 0000000000000..900fb8d47cecf --- /dev/null +++ b/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml @@ -0,0 +1,57 @@ +## +# Disabled plugins +######################## +# data.enabled: false +# interpreter.enabled: false +# visualizations.enabled: false +# xpack.apm.enabled: false +# console.enabled: false +console_extensions.enabled: false +dashboard_embeddable_container.enabled: false +dashboard_mode.enabled: false +embeddable_api.enabled: false +file_upload.enabled: false +# input_control_vis.enabled: false +inspector_views.enabled: false +kibana_react.enabled: false +markdown_vis.enabled: false +metric_vis.enabled: false +metrics.enabled: false +region_map.enabled: false +table_vis.enabled: false +tagcloud.enabled: false +tile_map.enabled: false +timelion.enabled: false +ui_metric.enabled: false +vega.enabled: false +xpack.actions.enabled: false +xpack.alerting.enabled: false +xpack.beats.enabled: false +xpack.canvas.enabled: false +xpack.cloud.enabled: false +xpack.code.enabled: false +xpack.encryptedSavedObjects.enabled: false +xpack.graph.enabled: false +xpack.grokdebugger.enabled: false +xpack.index_management.enabled: false +xpack.infra.enabled: false +# xpack.license_management.enabled: false +xpack.lens.enabled: false +xpack.logstash.enabled: false +xpack.maps.enabled: false +xpack.ml.enabled: false +xpack.monitoring.enabled: false +xpack.oss_telemetry.enabled: false +xpack.remote_clusters.enabled: false +xpack.rollup.enabled: false +xpack.searchprofiler.enabled: false +# xpack.security.enabled: false +xpack.siem.enabled: false +xpack.snapshot_restore.enabled: false +xpack.spaces.enabled: false +xpack.task_manager.enabled: false +xpack.tilemap.enabled: false +xpack.upgrade_assistant.enabled: false +xpack.uptime.enabled: false +xpack.watcher.enabled: false +logging.verbose: true diff --git a/x-pack/legacy/plugins/apm/cypress/ci/prepare-kibana.sh b/x-pack/legacy/plugins/apm/cypress/ci/prepare-kibana.sh new file mode 100755 index 0000000000000..d6fd620195b94 --- /dev/null +++ b/x-pack/legacy/plugins/apm/cypress/ci/prepare-kibana.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -e + +CYPRESS_DIR="x-pack/legacy/plugins/apm/cypress" + +echo "1/3 Install dependencies ..." +# shellcheck disable=SC1091 +source src/dev/ci_setup/setup_env.sh true +yarn kbn bootstrap +cp ${CYPRESS_DIR}/ci/kibana.dev.yml config/kibana.dev.yml +echo 'elasticsearch:' >> config/kibana.dev.yml +cp ${CYPRESS_DIR}/ci/kibana.dev.yml config/kibana.yml + +echo "2/3 Ingest test data ..." +pushd ${CYPRESS_DIR} +yarn install +curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/events.json --output ingest-data/events.json +node ingest-data/replay.js --server-url http://localhost:8200 --secret-token abcd --events ./events.json > ingest-data.log + +echo "3/3 Start Kibana ..." +popd +## Might help to avoid FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory +export NODE_OPTIONS="--max-old-space-size=4096" +nohup node scripts/kibana --no-base-path --csp.strict=false --optimize.watch=false> kibana.log 2>&1 & diff --git a/x-pack/legacy/plugins/apm/cypress/cypress.json b/x-pack/legacy/plugins/apm/cypress/cypress.json index aa2bf480ae468..69852346359fa 100644 --- a/x-pack/legacy/plugins/apm/cypress/cypress.json +++ b/x-pack/legacy/plugins/apm/cypress/cypress.json @@ -1,7 +1,6 @@ { "baseUrl": "http://localhost:5601", "video": false, - "trashAssetsBeforeRuns": false, "fileServerFolder": "../", "fixturesFolder": "./fixtures", @@ -10,5 +9,10 @@ "screenshotsFolder": "./screenshots", "supportFile": "./support/index.ts", "videosFolder": "./videos", - "useRelativeSnapshots": true + "useRelativeSnapshots": true, + "reporter": "junit", + "reporterOptions": { + "mochaFile": "[hash]-e2e-tests.xml", + "toConsole": false + } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx index 4096d69673636..2b3f1368f6fa0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx @@ -28,9 +28,9 @@ import { ServiceMap } from '../ServiceMap'; import { usePlugins } from '../../../new-platform/plugin'; function getHomeTabs({ - apmServiceMapEnabled = false + serviceMapEnabled = false }: { - apmServiceMapEnabled: boolean; + serviceMapEnabled: boolean; }) { const homeTabs = [ { @@ -57,7 +57,7 @@ function getHomeTabs({ } ]; - if (apmServiceMapEnabled) { + if (serviceMapEnabled) { homeTabs.push({ link: ( @@ -83,8 +83,8 @@ interface Props { export function Home({ tab }: Props) { const { apm } = usePlugins(); - const { apmServiceMapEnabled } = apm.config; - const homeTabs = getHomeTabs({ apmServiceMapEnabled }); + const { serviceMapEnabled } = apm.config; + const homeTabs = getHomeTabs({ serviceMapEnabled }); const selectedTab = homeTabs.find( homeTab => homeTab.name === tab ) as $ElementType; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js index 40c77a4c97cbd..c2009cd0ae3a1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js @@ -19,7 +19,7 @@ const coreMock = { jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); -const routes = getRoutes({ apmServiceMapEnabled: true }); +const routes = getRoutes({ serviceMapEnabled: true }); function expectBreadcrumbToMatchSnapshot(route, params = '') { mount( diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx index ba2786ebf64de..1d76a98573617 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -43,9 +43,9 @@ const renderAsRedirectTo = (to: string) => { }; export function getRoutes({ - apmServiceMapEnabled + serviceMapEnabled }: { - apmServiceMapEnabled: boolean; + serviceMapEnabled: boolean; }): BreadcrumbRoute[] { const routes: BreadcrumbRoute[] = [ { @@ -201,7 +201,7 @@ export function getRoutes({ } ]; - if (apmServiceMapEnabled) { + if (serviceMapEnabled) { routes.push( { exact: true, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index 82b641b060259..dda3c494e39ad 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -32,7 +32,7 @@ export function ServiceDetailTabs({ tab }: Props) { const { serviceName } = urlParams; const { agentName } = useAgentName(); const { apm } = usePlugins(); - const { apmServiceMapEnabled } = apm.config; + const { serviceMapEnabled } = apm.config; if (!serviceName) { // this never happens, urlParams type is not accurate enough @@ -107,7 +107,7 @@ export function ServiceDetailTabs({ tab }: Props) { name: 'service-map' }; - if (apmServiceMapEnabled) { + if (serviceMapEnabled) { tabs.push(serviceMapTab); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index c598a2fb1da2c..3625fb430ff29 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -151,9 +151,9 @@ export class WatcherFlyout extends Component< }; public createWatch = ({ - apmIndexPatternTitle + indexPatternTitle }: { - apmIndexPatternTitle: string; + indexPatternTitle: string; }) => () => { const { serviceName } = this.props.urlParams; const core = this.context; @@ -199,7 +199,7 @@ export class WatcherFlyout extends Component< slackUrl, threshold: this.state.threshold, timeRange, - apmIndexPatternTitle + apmIndexPatternTitle: indexPatternTitle }) .then((id: string) => { this.props.onClose(); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx new file mode 100644 index 0000000000000..21a39e19657a1 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx @@ -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. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ServiceNodeMetrics } from '.'; + +describe('ServiceNodeMetrics', () => { + describe('render', () => { + it('renders', () => { + expect(() => shallow()).not.toThrowError(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx index 79874d6648d0f..3929c153ae419 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx @@ -117,6 +117,22 @@ export function ServiceNodeMetrics() { ) : ( + + + {serviceName} + + } + /> + ; }; }; + savedObjects: { + client: { + get: jest.Mock; + }; + }; }; }; diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index a09cdbf91ec6e..56c9255844009 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -73,7 +73,10 @@ export async function setupRequest( const { config } = context; const { query } = context.params; - const indices = await getApmIndices(context); + const indices = await getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config + }); const dynamicIndexPattern = await getDynamicIndexPattern({ context, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 27c013ab4c6d3..542fdd99e2635 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -44,6 +44,7 @@ Object { exports[`agent configuration queries fetches configurations 1`] = ` Object { "index": "myIndex", + "size": 200, } `; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts index 12faa9fba1074..585de740bc88d 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts @@ -15,7 +15,8 @@ export async function listConfigurations({ setup }: { setup: Setup }) { const { internalClient, indices } = setup; const params = { - index: indices.apmAgentConfigurationIndex + index: indices.apmAgentConfigurationIndex, + size: 200 }; const resp = await internalClient.search(params); diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index 0ed30ec4cdd27..e451f89af5620 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -54,15 +54,25 @@ export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig { }; } -export async function getApmIndices(context: APMRequestHandlerContext) { +// export async function getApmIndices(context: APMRequestHandlerContext) { +// return _getApmIndices(context.core, context.config); +// } + +export async function getApmIndices({ + config, + savedObjectsClient +}: { + config: APMConfig; + savedObjectsClient: SavedObjectsClientContract; +}) { try { const apmIndicesSavedObject = await getApmIndicesSavedObject( - context.core.savedObjects.client + savedObjectsClient ); - const apmIndicesConfig = getApmIndicesConfig(context.config); + const apmIndicesConfig = getApmIndicesConfig(config); return merge({}, apmIndicesConfig, apmIndicesSavedObject); } catch (error) { - return getApmIndicesConfig(context.config); + return getApmIndicesConfig(config); } } diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts index b66eb05f6eda5..a69fba52be3f0 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts @@ -26,7 +26,10 @@ export const apmIndicesRoute = createRoute(() => ({ method: 'GET', path: '/api/apm/settings/apm-indices', handler: async ({ context }) => { - return await getApmIndices(context); + return await getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config: context.config + }); } })); diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts index 83c610800b89b..8771181639f4d 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -8,7 +8,6 @@ import { isEmpty } from 'lodash'; import { npStart } from 'ui/new_platform'; import { ElasticsearchAdapter } from './adapter_types'; import { AutocompleteSuggestion, esKuery } from '../../../../../../../../src/plugins/data/public'; -import { setup as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; const getAutocompleteProvider = (language: string) => npStart.plugins.data.autocomplete.getProvider(language); @@ -64,7 +63,7 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { if (this.cachedIndexPattern) { return this.cachedIndexPattern; } - const res = await data.indexPatterns.indexPatterns.getFieldsForWildcard({ + const res = await npStart.plugins.data.indexPatterns.getFieldsForWildcard({ pattern: this.indexPatternName, }); if (isEmpty(res.fields)) { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot index ef301c08cdfe8..6c9d4ce4459b3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot @@ -12,6 +12,7 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = ` >
-

Cycle Slides -

+

-

Cycle Slides -

+

-

Cycle Slides -

+

-

Hide Toolbar -

+
-

Hide Toolbar -

+
-

Hide Toolbar -

+
can navigate Autoplay Settings 2`] = ` aria-labelledby="generated-id" class="euiSwitch__button" id="cycle" + name="cycle" role="switch" type="button" > @@ -356,12 +357,12 @@ exports[` can navigate Autoplay Settings 2`] = ` -

Cycle Slides -

+

can navigate Autoplay Settings 2`] = ` >
can navigate Autoplay Settings 2`] = ` >
can navigate Toolbar Settings, closes when activated 2`] = >
can navigate Toolbar Settings, closes when activated 2`] = class="euiSwitch__button" data-test-subj="hideToolbarSwitch" id="generated-id" + name="toolbarHide" role="switch" type="button" > @@ -824,12 +829,12 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] = -

Hide Toolbar -

+
can navigate Toolbar Settings, closes when activated 2`] =
`; -exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings

Hide Toolbar

Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts b/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts deleted file mode 100644 index 660cd256cebcd..0000000000000 --- a/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts +++ /dev/null @@ -1,60 +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 sinon from 'sinon'; -import { Server } from 'hapi'; -import { createCollectorFetch, createCloudUsageCollector } from './cloud_usage_collector'; - -const CLOUD_ID_STAGING = - 'staging:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; -const CLOUD_ID = - 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; - -const mockUsageCollection = () => ({ - makeUsageCollector: sinon.stub(), -}); - -const getMockServer = (cloudId?: string) => - ({ - config() { - return { - get(path: string) { - switch (path) { - case 'xpack.cloud': - return { id: cloudId }; - default: - throw Error(`server.config().get(${path}) should not be called by this collector.`); - } - }, - }; - }, - } as Server); - -describe('Cloud usage collector', () => { - describe('collector', () => { - it('returns `isCloudEnabled: false` if `xpack.cloud.id` is not defined', async () => { - const mockServer = getMockServer(); - const collector = await createCollectorFetch(mockServer)(); - expect(collector.isCloudEnabled).toBe(false); - }); - - it('returns `isCloudEnabled: true` if `xpack.cloud.id` is defined', async () => { - const stagingCollector = await createCollectorFetch(getMockServer(CLOUD_ID))(); - const collector = await createCollectorFetch(getMockServer(CLOUD_ID_STAGING))(); - expect(collector.isCloudEnabled).toBe(true); - expect(stagingCollector.isCloudEnabled).toBe(true); - }); - }); -}); - -describe('createCloudUsageCollector', () => { - it('returns calls `makeUsageCollector`', () => { - const mockServer = getMockServer(); - const usageCollection = mockUsageCollection(); - createCloudUsageCollector(usageCollection as any, mockServer); - expect(usageCollection.makeUsageCollector.calledOnce).toBe(true); - }); -}); diff --git a/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts b/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts deleted file mode 100644 index 7fdf32144972c..0000000000000 --- a/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts +++ /dev/null @@ -1,36 +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 { Server } from 'hapi'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { KIBANA_CLOUD_STATS_TYPE } from './constants'; - -export interface UsageStats { - isCloudEnabled: boolean; -} - -export function createCollectorFetch(server: Server) { - return async function fetchUsageStats(): Promise { - const { id } = server.config().get(`xpack.cloud`); - - return { - isCloudEnabled: !!id, - }; - }; -} - -export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { - return usageCollection.makeUsageCollector({ - type: KIBANA_CLOUD_STATS_TYPE, - isReady: () => true, - fetch: createCollectorFetch(server), - }); -} - -export function registerCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { - const collector = createCloudUsageCollector(usageCollection, server); - usageCollection.registerCollector(collector); -} diff --git a/x-pack/legacy/plugins/cloud/index.js b/x-pack/legacy/plugins/cloud/index.js deleted file mode 100644 index c2fd35eea5292..0000000000000 --- a/x-pack/legacy/plugins/cloud/index.js +++ /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 { registerCloudUsageCollector } from './cloud_usage_collector'; - -export const cloud = kibana => { - return new kibana.Plugin({ - id: 'cloud', - configPrefix: 'xpack.cloud', - require: ['kibana', 'elasticsearch', 'xpack_main'], - - uiExports: { - injectDefaultVars(server, options) { - return { - isCloudEnabled: !!options.id, - cloudId: options.id - }; - }, - }, - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - id: Joi.string(), - apm: Joi.object({ - url: Joi.string(), - secret_token: Joi.string(), - ui: Joi.object({ - url: Joi.string(), - }).default(), - }).default(), - }).default(); - }, - - init(server) { - const config = server.config().get(`xpack.cloud`); - server.expose('config', { - isCloudEnabled: !!config.id - }); - const { usageCollection } = server.newPlatform.setup.plugins; - registerCloudUsageCollector(usageCollection, server); - } - }); -}; diff --git a/x-pack/legacy/plugins/file_upload/public/kibana_services.js b/x-pack/legacy/plugins/file_upload/public/kibana_services.js index 3c00ab5709660..1645040629195 100644 --- a/x-pack/legacy/plugins/file_upload/public/kibana_services.js +++ b/x-pack/legacy/plugins/file_upload/public/kibana_services.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { npStart } from 'ui/new_platform'; import { DEFAULT_KBN_VERSION } from '../common/constants/file_import'; -export const indexPatternService = data.indexPatterns.indexPatterns; +export const indexPatternService = npStart.plugins.data.indexPatterns; export let savedObjectsClient; export let basePath; diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx index a0514576877d1..9dec725a7a2a1 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx @@ -51,6 +51,9 @@ function wrapSearchBarInContext(testProps: OuterSearchBarProps) { query: { savedQueries: {}, }, + autocomplete: { + getProvider: () => undefined, + }, }, }; @@ -88,21 +91,23 @@ describe('search_bar', () => { ); }); - function mountSearchBar() { + async function mountSearchBar() { jest.clearAllMocks(); const wrappedSearchBar = wrapSearchBarInContext({ ...defaultProps }); - instance = mountWithIntl({wrappedSearchBar}); + await act(async () => { + instance = mountWithIntl({wrappedSearchBar}); + }); } - it('should render search bar and fetch index pattern', () => { - mountSearchBar(); + it('should render search bar and fetch index pattern', async () => { + await mountSearchBar(); expect(defaultProps.indexPatternProvider.get).toHaveBeenCalledWith('123'); }); it('should render search bar and submit queries', async () => { - mountSearchBar(); + await mountSearchBar(); await waitForIndexPatternFetch(); @@ -118,7 +123,7 @@ describe('search_bar', () => { }); it('should translate kql query into JSON dsl', async () => { - mountSearchBar(); + await mountSearchBar(); await waitForIndexPatternFetch(); @@ -136,8 +141,8 @@ describe('search_bar', () => { }); }); - it('should open index pattern picker', () => { - mountSearchBar(); + it('should open index pattern picker', async () => { + await mountSearchBar(); // pick the button component out of the tree because // it's part of a popover and thus not covered by enzyme diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 988aa78695095..712d08c106425 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -18,7 +18,6 @@ import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_regis import { npSetup, npStart } from 'ui/new_platform'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; import { start as navigation } from '../../../../../src/legacy/core_plugins/navigation/public/legacy'; import { GraphPlugin } from './plugin'; @@ -51,7 +50,6 @@ async function getAngularInjectedDependencies(): Promise; navigation: NavigationStart; } @@ -31,7 +29,6 @@ export interface GraphPluginStartDependencies { } export class GraphPlugin implements Plugin { - private dataStart: DataStart | null = null; private navigationStart: NavigationStart | null = null; private npDataStart: ReturnType | null = null; private savedObjectsClient: SavedObjectsClientContract | null = null; @@ -61,7 +58,7 @@ export class GraphPlugin implements Plugin { chrome: contextCore.chrome, config: contextCore.uiSettings, toastNotifications: contextCore.notifications.toasts, - indexPatterns: this.dataStart!.indexPatterns.indexPatterns, + indexPatterns: this.npDataStart!.indexPatterns, ...this.angularDependencies!, }); }, @@ -70,10 +67,9 @@ export class GraphPlugin implements Plugin { start( core: CoreStart, - { data, npData, navigation, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies + { npData, navigation, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies ) { this.navigationStart = navigation; - this.dataStart = data; this.npDataStart = npData; this.angularDependencies = angularDependencies; this.savedObjectsClient = core.savedObjects.client; diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 1beee2e73721b..0f3c52d38a01c 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -21,7 +21,6 @@ import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_to import { confirmModalFactory } from 'ui/modals/confirm_modal'; // type imports -import { DataStart } from 'src/legacy/core_plugins/data/public'; import { AppMountContext, ChromeStart, @@ -32,7 +31,10 @@ import { } from 'kibana/public'; // @ts-ignore import { initGraphApp } from './app'; -import { Plugin as DataPlugin } from '../../../../../src/plugins/data/public'; +import { + Plugin as DataPlugin, + IndexPatternsContract, +} from '../../../../../src/plugins/data/public'; import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public'; /** @@ -50,7 +52,7 @@ export interface GraphDependencies extends LegacyAngularInjectedDependencies { chrome: ChromeStart; config: IUiSettingsClient; toastNotifications: ToastsStart; - indexPatterns: DataStart['indexPatterns']['indexPatterns']; + indexPatterns: IndexPatternsContract; npData: ReturnType; savedObjectsClient: SavedObjectsClientContract; xpackInfo: { get(path: string): unknown }; diff --git a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts index 5b9389a073002..ace61e13193c8 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts @@ -5,7 +5,6 @@ */ import * as rt from 'io-ts'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; export const InfraMetadataNodeTypeRT = rt.keyof({ host: null, @@ -67,6 +66,7 @@ export const InfraMetadataInfoRT = rt.partial({ }); const InfraMetadataRequiredRT = rt.type({ + id: rt.string, name: rt.string, features: rt.array(InfraMetadataFeatureRT), }); @@ -81,8 +81,6 @@ export type InfraMetadata = rt.TypeOf; export type InfraMetadataRequest = rt.TypeOf; -export type InfraMetadataWrappedRequest = InfraWrappableRequest; - export type InfraMetadataFeature = rt.TypeOf; export type InfraMetadataInfo = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts b/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts index 607d71654032e..46aab881bce4c 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts @@ -6,7 +6,6 @@ import * as rt from 'io-ts'; import { InventoryMetricRT, ItemTypeRT } from '../inventory_models/types'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; import { InfraTimerangeInputRT } from './snapshot_api'; const NodeDetailsDataPointRT = rt.intersection([ @@ -53,6 +52,4 @@ export const NodeDetailsRequestRT = rt.intersection([ // export type NodeDetailsRequest = InfraWrappableRequest; export type NodeDetailsRequest = rt.TypeOf; -export type NodeDetailsWrappedRequest = InfraWrappableRequest; - export type NodeDetailsMetricDataResponse = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts index 24ca0fed73338..3e6aec4bad972 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts @@ -5,7 +5,6 @@ */ import * as rt from 'io-ts'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types'; export const SnapshotNodePathRT = rt.intersection([ @@ -64,6 +63,5 @@ export const SnapshotRequestRT = rt.intersection([ ]); export type SnapshotRequest = rt.TypeOf; -export type SnapshotWrappedRequest = InfraWrappableRequest; export type SnapshotNode = rt.TypeOf; export type SnapshotNodeResponse = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/index.ts b/x-pack/legacy/plugins/infra/index.ts index 9bf679fb5ff80..dbf1f4ad61de3 100644 --- a/x-pack/legacy/plugins/infra/index.ts +++ b/x-pack/legacy/plugins/infra/index.ts @@ -7,9 +7,14 @@ import { i18n } from '@kbn/i18n'; import JoiNamespace from 'joi'; import { resolve } from 'path'; - -import { getConfigSchema, initServerWithKibana } from './server/kibana.index'; +import { PluginInitializerContext } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import KbnServer from 'src/legacy/server/kbn_server'; +import { getConfigSchema } from './server/kibana.index'; import { savedObjectMappings } from './server/saved_objects'; +import { plugin, InfraServerPluginDeps } from './server/new_platform_index'; +import { InfraSetup } from '../../../plugins/infra/server'; +import { APMPluginContract } from '../../../plugins/apm/server/plugin'; const APP_ID = 'infra'; const logsSampleDataLinkLabel = i18n.translate('xpack.infra.sampleDataLinkLabel', { @@ -70,9 +75,52 @@ export function infra(kibana: any) { config(Joi: typeof JoiNamespace) { return getConfigSchema(Joi); }, - init(server: any) { - initServerWithKibana(server); - server.addAppLinksToSampleDataset('logs', [ + init(legacyServer: any) { + const { newPlatform } = legacyServer as KbnServer; + const { core, plugins } = newPlatform.setup; + + const infraSetup = (plugins.infra as unknown) as InfraSetup; // chef's kiss + + const initContext = ({ + config: infraSetup.__legacy.config, + } as unknown) as PluginInitializerContext; + // NP_TODO: Use real types from the other plugins as they are migrated + const pluginDeps: InfraServerPluginDeps = { + usageCollection: plugins.usageCollection as UsageCollectionSetup, + indexPatterns: { + indexPatternsServiceFactory: legacyServer.indexPatternsServiceFactory, + }, + metrics: legacyServer.plugins.metrics, + spaces: plugins.spaces, + features: plugins.features, + // NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that + // originate from the New Platform router (and are very different to the old request object). + // Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just + // the requestContext, this can be removed. + ___legacy: { + tsvb: { + elasticsearch: legacyServer.plugins.elasticsearch, + __internals: legacyServer.newPlatform.__internals, + }, + }, + apm: plugins.apm as APMPluginContract, + }; + + const infraPluginInstance = plugin(initContext); + infraPluginInstance.setup(core, pluginDeps); + + // NP_TODO: EVERYTHING BELOW HERE IS LEGACY + + const libs = infraPluginInstance.getLibs(); + + // NP_TODO how do we replace this? Answer: return from setup function. + legacyServer.expose( + 'defineInternalSourceConfiguration', + libs.sources.defineInternalSourceConfiguration.bind(libs.sources) + ); + + // NP_TODO: How do we move this to new platform? + legacyServer.addAppLinksToSampleDataset('logs', [ { path: `/app/${APP_ID}#/logs`, label: logsSampleDataLinkLabel, diff --git a/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx b/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx index 2fc96fbd22a9e..8df479f36e2f9 100644 --- a/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx +++ b/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx @@ -23,11 +23,12 @@ import { } from '@elastic/eui'; interface Props { + isInvalid: boolean; close(): void; save(name: string, shouldIncludeTime: boolean): void; } -export const SavedViewCreateModal = ({ close, save }: Props) => { +export const SavedViewCreateModal = ({ close, save, isInvalid }: Props) => { const [viewName, setViewName] = useState(''); const [includeTime, setIncludeTime] = useState(false); const onCheckChange = useCallback(e => setIncludeTime(e.target.checked), []); @@ -35,7 +36,6 @@ export const SavedViewCreateModal = ({ close, save }: Props) => { const saveView = useCallback(() => { save(viewName, includeTime); - close(); }, [viewName, includeTime]); return ( @@ -52,6 +52,7 @@ export const SavedViewCreateModal = ({ close, save }: Props) => { (props: Props) { find, errorOnFind, errorOnCreate, + createdId, } = useSavedView(props.defaultViewState, props.viewType); const [modalOpen, setModalOpen] = useState(false); + const [isInvalid, setIsInvalid] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false); - const openSaveModal = useCallback(() => setCreateModalOpen(true), []); - const closeCreateModal = useCallback(() => setCreateModalOpen(false), []); + const openSaveModal = useCallback(() => { + setIsInvalid(false); + setCreateModalOpen(true); + }, []); const closeModal = useCallback(() => setModalOpen(false), []); + const closeCreateModal = useCallback(() => setCreateModalOpen(false), []); const loadViews = useCallback(() => { find(); setModalOpen(true); @@ -50,6 +55,19 @@ export function SavedViewsToolbarControls(props: Props) { [props.viewState, saveView] ); + useEffect(() => { + if (errorOnCreate) { + setIsInvalid(true); + } + }, [errorOnCreate]); + + useEffect(() => { + if (createdId !== undefined) { + // INFO: Close the modal after the view is created. + closeCreateModal(); + } + }, [createdId, closeCreateModal]); + useEffect(() => { if (deletedId !== undefined) { // INFO: Refresh view list after an item is deleted @@ -59,9 +77,9 @@ export function SavedViewsToolbarControls(props: Props) { useEffect(() => { if (errorOnCreate) { - toastNotifications.addWarning(getErrorToast('create')!); + toastNotifications.addWarning(getErrorToast('create', errorOnCreate)!); } else if (errorOnFind) { - toastNotifications.addWarning(getErrorToast('find')!); + toastNotifications.addWarning(getErrorToast('find', errorOnFind)!); } }, [errorOnCreate, errorOnFind]); @@ -82,7 +100,9 @@ export function SavedViewsToolbarControls(props: Props) { - {createModalOpen && } + {createModalOpen && ( + + )} {modalOpen && ( loading={loading} @@ -96,18 +116,22 @@ export function SavedViewsToolbarControls(props: Props) { ); } -const getErrorToast = (type: 'create' | 'find') => { +const getErrorToast = (type: 'create' | 'find', msg?: string) => { if (type === 'create') { return { - title: i18n.translate('xpack.infra.savedView.errorOnCreate.title', { - defaultMessage: `An error occured saving view.`, - }), + title: + msg || + i18n.translate('xpack.infra.savedView.errorOnCreate.title', { + defaultMessage: `An error occured saving view.`, + }), }; } else if (type === 'find') { return { - title: i18n.translate('xpack.infra.savedView.findError.title', { - defaultMessage: `An error occurred while loading views.`, - }), + title: + msg || + i18n.translate('xpack.infra.savedView.findError.title', { + defaultMessage: `An error occurred while loading views.`, + }), }; } }; diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_create_saved_object.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_create_saved_object.tsx index 944249033e10b..80811a6d6c7bf 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_create_saved_object.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_create_saved_object.tsx @@ -12,6 +12,7 @@ import { SavedObjectAttributes } from 'src/core/server'; export const useCreateSavedObject = (type: string) => { const [data, setData] = useState | null>(null); + const [createdId, setCreatedId] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); @@ -21,6 +22,7 @@ export const useCreateSavedObject = (type: string) => { const save = async () => { try { const d = await npStart.core.savedObjects.client.create(type, attributes, options); + setCreatedId(d.id); setError(null); setData(d); setLoading(false); @@ -39,5 +41,6 @@ export const useCreateSavedObject = (type: string) => { loading, error, create, + createdId, }; }; diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_find_saved_object.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_find_saved_object.tsx index 27e01b23e6b4c..949a2344418e9 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_find_saved_object.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_find_saved_object.tsx @@ -37,7 +37,16 @@ export const useFindSavedObject = { + const objects = await npStart.core.savedObjects.client.find({ + type, + }); + + return objects.savedObjects.filter(o => o.attributes.name === name).length > 0; + }; + return { + hasView, data, loading, error, diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts b/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts index f3d4833a5a755..8db0ed28d9b21 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts +++ b/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { useFindSavedObject } from './use_find_saved_object'; import { useCreateSavedObject } from './use_create_saved_object'; @@ -16,18 +16,37 @@ export type SavedView = ViewState & { isDefault?: boolean; }; -export type SavedViewSavedObject = ViewState & { +export type SavedViewSavedObject = ViewState & { name: string; }; export const useSavedView = (defaultViewState: ViewState, viewType: string) => { - const { data, loading, find, error: errorOnFind } = useFindSavedObject< + const { data, loading, find, error: errorOnFind, hasView } = useFindSavedObject< SavedViewSavedObject >(viewType); - const { create, error: errorOnCreate } = useCreateSavedObject(viewType); + const { create, error: errorOnCreate, createdId } = useCreateSavedObject(viewType); const { deleteObject, deletedId } = useDeleteSavedObject(viewType); const deleteView = useCallback((id: string) => deleteObject(id), []); - const saveView = useCallback((d: { [p: string]: any }) => create(d), []); + const [createError, setCreateError] = useState(null); + + useEffect(() => setCreateError(createError), [errorOnCreate, setCreateError]); + + const saveView = useCallback((d: { [p: string]: any }) => { + const doSave = async () => { + const exists = await hasView(d.name); + if (exists) { + setCreateError( + i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', { + defaultMessage: `A view with that name already exists.`, + }) + ); + return; + } + create(d); + }; + setCreateError(null); + doSave(); + }, []); const savedObjects = data ? data.savedObjects : []; const views = useMemo(() => { @@ -61,8 +80,9 @@ export const useSavedView = (defaultViewState: ViewState, viewType: s saveView, loading, deletedId, + createdId, errorOnFind, - errorOnCreate, + errorOnCreate: createError, deleteView, find, }; diff --git a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts index d70a42473b710..f91b40815a3ae 100644 --- a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -24,7 +24,7 @@ import { const ROOT_ELEMENT_ID = 'react-infra-root'; const BREADCRUMBS_ELEMENT_ID = 'react-infra-breadcrumbs'; -export class InfraKibanaFrameworkAdapter implements InfraFrameworkAdapter { +export class KibanaFramework implements InfraFrameworkAdapter { public appState: object; public kbnVersion?: string; public timezone?: string; diff --git a/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts b/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts index 086691e665b03..9b0beb3ad519c 100644 --- a/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts +++ b/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts @@ -20,7 +20,7 @@ import { HttpLink } from 'apollo-link-http'; import { withClientState } from 'apollo-link-state'; import { InfraFrontendLibs } from '../lib'; import introspectionQueryResultData from '../../graphql/introspection.json'; -import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; export function compose(): InfraFrontendLibs { @@ -57,7 +57,7 @@ export function compose(): InfraFrontendLibs { const infraModule = uiModules.get('app/infa'); - const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider); + const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider); const libs: InfraFrontendLibs = { apolloClient, diff --git a/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts b/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts index 14fd66d378121..1e0b2f079497d 100644 --- a/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts +++ b/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts @@ -17,7 +17,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; import { SchemaLink } from 'apollo-link-schema'; import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools'; -import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; import { InfraFrontendLibs } from '../lib'; @@ -27,7 +27,7 @@ export function compose(): InfraFrontendLibs { basePath: chrome.getBasePath(), xsrfToken: chrome.getXsrfToken(), }); - const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider); + const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider); const typeDefs = ` Query {} `; diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx index 1916e84ef21d3..7a63406bb419a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx @@ -34,11 +34,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct container filter', () => { @@ -47,11 +47,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct pod filter', () => { @@ -60,11 +60,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct position', () => { @@ -75,11 +75,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct user-defined filter', () => { @@ -92,11 +92,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct custom source id', () => { @@ -107,11 +107,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); }); diff --git a/x-pack/legacy/plugins/infra/server/features.ts b/x-pack/legacy/plugins/infra/server/features.ts new file mode 100644 index 0000000000000..fc20813c777b6 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/features.ts @@ -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. + */ + +import { i18n } from '@kbn/i18n'; + +export const METRICS_FEATURE = { + id: 'infrastructure', + name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { + defaultMessage: 'Infrastructure', + }), + icon: 'infraApp', + navLinkId: 'infra:home', + app: ['infra', 'kibana'], + catalogue: ['infraops'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: ['index-pattern'], + }, + ui: ['show', 'configureSource', 'save'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['infrastructure-ui-source', 'index-pattern'], + }, + ui: ['show'], + }, + }, +}; + +export const LOGS_FEATURE = { + id: 'logs', + name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { + defaultMessage: 'Logs', + }), + icon: 'loggingApp', + navLinkId: 'infra:logs', + app: ['infra', 'kibana'], + catalogue: ['infralogging'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: [], + }, + ui: ['show', 'configureSource', 'save'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['infrastructure-ui-source'], + }, + ui: ['show'], + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index edccf5f413ab4..845e54e18c7c5 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -30,7 +30,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { typeDefs: schemas, }); - libs.framework.registerGraphQLEndpoint('/api/infra/graphql', schema); + libs.framework.registerGraphQLEndpoint('/graphql', schema); initIpToHostName(libs); initLogAnalysisGetLogEntryRateRoute(libs); diff --git a/x-pack/legacy/plugins/infra/server/kibana.index.ts b/x-pack/legacy/plugins/infra/server/kibana.index.ts index 91bcd6be95a75..b4301b3edf367 100644 --- a/x-pack/legacy/plugins/infra/server/kibana.index.ts +++ b/x-pack/legacy/plugins/infra/server/kibana.index.ts @@ -4,97 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { Server } from 'hapi'; import JoiNamespace from 'joi'; -import { initInfraServer } from './infra_server'; -import { compose } from './lib/compose/kibana'; -import { UsageCollector } from './usage/usage_collector'; -import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; -import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; -export const initServerWithKibana = (kbnServer: Server) => { - const { usageCollection } = kbnServer.newPlatform.setup.plugins; - const libs = compose(kbnServer); - initInfraServer(libs); - - kbnServer.expose( - 'defineInternalSourceConfiguration', - libs.sources.defineInternalSourceConfiguration.bind(libs.sources) - ); - - // Register a function with server to manage the collection of usage stats - UsageCollector.registerUsageCollector(usageCollection); - - const xpackMainPlugin = kbnServer.plugins.xpack_main; - xpackMainPlugin.registerFeature({ - id: 'infrastructure', - name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { - defaultMessage: 'Metrics', - }), - icon: 'metricsApp', - navLinkId: 'infra:home', - app: ['infra', 'kibana'], - catalogue: ['infraops'], - privileges: { - all: { - api: ['infra'], - savedObject: { - all: [ - 'infrastructure-ui-source', - inventoryViewSavedObjectType, - metricsExplorerViewSavedObjectType, - ], - read: ['index-pattern'], - }, - ui: ['show', 'configureSource', 'save'], - }, - read: { - api: ['infra'], - savedObject: { - all: [], - read: [ - 'infrastructure-ui-source', - 'index-pattern', - inventoryViewSavedObjectType, - metricsExplorerViewSavedObjectType, - ], - }, - ui: ['show'], - }, - }, - }); - - xpackMainPlugin.registerFeature({ - id: 'logs', - name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { - defaultMessage: 'Logs', - }), - icon: 'logsApp', - navLinkId: 'infra:logs', - app: ['infra', 'kibana'], - catalogue: ['infralogging'], - privileges: { - all: { - api: ['infra'], - savedObject: { - all: ['infrastructure-ui-source'], - read: [], - }, - ui: ['show', 'configureSource', 'save'], - }, - read: { - api: ['infra'], - savedObject: { - all: [], - read: ['infrastructure-ui-source'], - }, - ui: ['show'], - }, - }, - }); -}; +export interface KbnServer extends Server { + usage: any; +} +// NP_TODO: this is only used in the root index file AFAICT, can remove after migrating to NP export const getConfigSchema = (Joi: typeof JoiNamespace) => { const InfraDefaultSourceConfigSchema = Joi.object({ metricAlias: Joi.string(), @@ -111,6 +28,7 @@ export const getConfigSchema = (Joi: typeof JoiNamespace) => { }), }); + // NP_TODO: make sure this is all represented in the NP config schema const InfraRootConfigSchema = Joi.object({ enabled: Joi.boolean().default(true), query: Joi.object({ diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts deleted file mode 100644 index b0856cf3da361..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts +++ /dev/null @@ -1,19 +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. - */ - -export interface InfraConfigurationAdapter< - Configuration extends InfraBaseConfiguration = InfraBaseConfiguration -> { - get(): Promise; -} - -export interface InfraBaseConfiguration { - enabled: boolean; - query: { - partitionSize: number; - partitionFactor: number; - }; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts deleted file mode 100644 index 472fa72939565..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { InfraBaseConfiguration, InfraConfigurationAdapter } from './adapter_types'; - -export class InfraInmemoryConfigurationAdapter - implements InfraConfigurationAdapter { - constructor(private readonly configuration: Configuration) {} - - public async get() { - return this.configuration; - } -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts deleted file mode 100644 index 4d87878e9aa87..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts +++ /dev/null @@ -1,40 +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 { InfraKibanaConfigurationAdapter } from './kibana_configuration_adapter'; - -describe('the InfraKibanaConfigurationAdapter', () => { - test('queries the xpack.infra configuration of the server', async () => { - const mockConfig = { - get: jest.fn(), - }; - - const configurationAdapter = new InfraKibanaConfigurationAdapter({ - config: () => mockConfig, - }); - - await configurationAdapter.get(); - - expect(mockConfig.get).toBeCalledWith('xpack.infra'); - }); - - test('applies the query defaults', async () => { - const configurationAdapter = new InfraKibanaConfigurationAdapter({ - config: () => ({ - get: () => ({}), - }), - }); - - const configuration = await configurationAdapter.get(); - - expect(configuration).toMatchObject({ - query: { - partitionSize: expect.any(Number), - partitionFactor: expect.any(Number), - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts deleted file mode 100644 index d3699a4820cf0..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts +++ /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 Joi from 'joi'; - -import { InfraBaseConfiguration, InfraConfigurationAdapter } from './adapter_types'; - -export class InfraKibanaConfigurationAdapter implements InfraConfigurationAdapter { - private readonly server: ServerWithConfig; - - constructor(server: any) { - if (!isServerWithConfig(server)) { - throw new Error('Failed to find configuration on server.'); - } - - this.server = server; - } - - public async get() { - const config = this.server.config(); - - if (!isKibanaConfiguration(config)) { - throw new Error('Failed to access configuration of server.'); - } - - const configuration = config.get('xpack.infra') || {}; - const configurationWithDefaults: InfraBaseConfiguration = { - enabled: true, - query: { - partitionSize: 75, - partitionFactor: 1.2, - ...(configuration.query || {}), - }, - ...configuration, - }; - - // we assume this to be the configuration because Kibana would have already validated it - return configurationWithDefaults; - } -} - -interface ServerWithConfig { - config(): any; -} - -function isServerWithConfig(maybeServer: any): maybeServer is ServerWithConfig { - return ( - Joi.validate( - maybeServer, - Joi.object({ - config: Joi.func().required(), - }).unknown() - ).error === null - ); -} - -interface KibanaConfiguration { - get(key: string): any; -} - -function isKibanaConfiguration(maybeConfiguration: any): maybeConfiguration is KibanaConfiguration { - return ( - Joi.validate( - maybeConfiguration, - Joi.object({ - get: Joi.func().required(), - }).unknown() - ).error === null - ); -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts index 66081e60e7e10..3aaa23b378096 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraFrameworkRequest } from '../framework'; +import { RequestHandlerContext } from 'src/core/server'; export interface FieldsAdapter { getIndexFields( - req: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts index a6881a05f6f93..01306901e9caa 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts @@ -6,11 +6,9 @@ import { startsWith, uniq, first } from 'lodash'; import { idx } from '@kbn/elastic-idx'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { InfraDatabaseSearchResponse } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { FieldsAdapter, IndexFieldDescriptor } from './adapter_types'; import { getAllowedListForPrefix } from '../../../../common/ecs_allowed_list'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; @@ -31,22 +29,26 @@ interface DataSetResponse { } export class FrameworkFieldsAdapter implements FieldsAdapter { - private framework: InfraBackendFrameworkAdapter; + private framework: KibanaFramework; - constructor(framework: InfraBackendFrameworkAdapter) { + constructor(framework: KibanaFramework) { this.framework = framework; } public async getIndexFields( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise { - const indexPatternsService = this.framework.getIndexPatternsService(request); + const indexPatternsService = this.framework.getIndexPatternsService(requestContext); const response = await indexPatternsService.getFieldsForWildcard({ pattern: indices, }); - const { dataSets, modules } = await this.getDataSetsAndModules(request, indices, timefield); + const { dataSets, modules } = await this.getDataSetsAndModules( + requestContext, + indices, + timefield + ); const allowedList = modules.reduce( (acc, name) => uniq([...acc, ...getAllowedListForPrefix(name)]), [] as string[] @@ -59,7 +61,7 @@ export class FrameworkFieldsAdapter implements FieldsAdapter { } private async getDataSetsAndModules( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise<{ dataSets: string[]; modules: string[] }> { @@ -109,7 +111,7 @@ export class FrameworkFieldsAdapter implements FieldsAdapter { const buckets = await getAllCompositeData( this.framework, - request, + requestContext, params, bucketSelector, handleAfterKey diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 63fded49d8222..625607c098028 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -4,91 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; -import { GraphQLSchema } from 'graphql'; -import { Lifecycle, ResponseToolkit, RouteOptions } from 'hapi'; -import { Legacy } from 'kibana'; - -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { JsonObject } from '../../../../common/typed_json'; -import { TSVBMetricModel } from '../../../../common/inventory_models/types'; - -export const internalInfraFrameworkRequest = Symbol('internalInfraFrameworkRequest'); - -/* eslint-disable @typescript-eslint/unified-signatures */ -export interface InfraBackendFrameworkAdapter { - version: string; - exposeStaticDir(urlPath: string, dir: string): void; - registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; - registerRoute( - route: InfraFrameworkRouteOptions - ): void; - callWithRequest( - req: InfraFrameworkRequest, - method: 'search', - options?: object - ): Promise>; - callWithRequest( - req: InfraFrameworkRequest, - method: 'msearch', - options?: object - ): Promise>; - callWithRequest( - req: InfraFrameworkRequest, - method: 'fieldCaps', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.existsAlias', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.getAlias', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.get', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'ml.getBuckets', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: string, - options?: object - ): Promise; - getIndexPatternsService(req: InfraFrameworkRequest): Legacy.IndexPatternsService; - getSavedObjectsService(): Legacy.SavedObjectsService; - getSpaceId(request: InfraFrameworkRequest): string; - makeTSVBRequest( - req: InfraFrameworkRequest, - model: TSVBMetricModel, - timerange: { min: number; max: number }, - filters: JsonObject[] - ): Promise; - config(req: InfraFrameworkRequest): KibanaConfig; -} -/* eslint-enable @typescript-eslint/unified-signatures */ - -export interface InfraFrameworkRequest< - InternalRequest extends InfraWrappableRequest = InfraWrappableRequest -> { - [internalInfraFrameworkRequest]: InternalRequest; - payload: InternalRequest['payload']; - params: InternalRequest['params']; - query: InternalRequest['query']; -} - -export interface InfraWrappableRequest { - payload: Payload; - params: Params; - query: Query; +import { SearchResponse, GenericParams } from 'elasticsearch'; +import { Lifecycle } from 'hapi'; +import { ObjectType } from '@kbn/config-schema'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { RouteMethod, RouteConfig } from '../../../../../../../../src/core/server'; +import { APMPluginContract } from '../../../../../../../plugins/apm/server/plugin'; + +// NP_TODO: Compose real types from plugins we depend on, no "any" +export interface InfraServerPluginDeps { + usageCollection: UsageCollectionSetup; + spaces: any; + metrics: { + getVisData: any; + }; + indexPatterns: { + indexPatternsServiceFactory: any; + }; + features: any; + apm: APMPluginContract; + ___legacy: any; +} + +export interface CallWithRequestParams extends GenericParams { + max_concurrent_shard_requests?: number; + name?: string; + index?: string; + ignore_unavailable?: boolean; + allow_no_indices?: boolean; + size?: number; + terminate_after?: number; + fields?: string; } export type InfraResponse = Lifecycle.ReturnValue; @@ -98,22 +44,6 @@ export interface InfraFrameworkPluginOptions { options: any; } -export interface InfraFrameworkRouteOptions< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse -> { - path: string; - method: string | string[]; - vhost?: string; - handler: InfraFrameworkRouteHandler; - options?: Pick>; -} - -export type InfraFrameworkRouteHandler< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse -> = (request: InfraFrameworkRequest, h: ResponseToolkit) => RouteResponse; - export interface InfraDatabaseResponse { took: number; timeout: boolean; @@ -235,3 +165,12 @@ export interface InfraTSVBSeries { } export type InfraTSVBDataPoint = [number, number]; + +export type InfraRouteConfig< + params extends ObjectType, + query extends ObjectType, + body extends ObjectType, + method extends RouteMethod +> = { + method: RouteMethod; +} & RouteConfig; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts deleted file mode 100644 index da858217468f1..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts +++ /dev/null @@ -1,117 +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 GraphiQL from 'apollo-server-module-graphiql'; -import Boom from 'boom'; -import { Plugin, Request, ResponseToolkit, RouteOptions, Server } from 'hapi'; - -import { GraphQLOptions, runHttpQuery } from 'apollo-server-core'; - -export type HapiOptionsFunction = (req: Request) => GraphQLOptions | Promise; - -export interface HapiGraphQLPluginOptions { - path: string; - vhost?: string; - route?: RouteOptions; - graphqlOptions: GraphQLOptions | HapiOptionsFunction; -} - -export const graphqlHapi: Plugin = { - name: 'graphql', - register: (server: Server, options: HapiGraphQLPluginOptions) => { - if (!options || !options.graphqlOptions) { - throw new Error('Apollo Server requires options.'); - } - - server.route({ - options: options.route || {}, - handler: async (request: Request, h: ResponseToolkit) => { - try { - const query = - request.method === 'post' - ? (request.payload as Record) - : (request.query as Record); - - const gqlResponse = await runHttpQuery([request], { - method: request.method.toUpperCase(), - options: options.graphqlOptions, - query, - }); - - return h.response(gqlResponse).type('application/json'); - } catch (error) { - if ('HttpQueryError' !== error.name) { - const queryError = Boom.boomify(error); - - queryError.output.payload.message = error.message; - - return queryError; - } - - if (error.isGraphQLError === true) { - return h - .response(error.message) - .code(error.statusCode) - .type('application/json'); - } - - const genericError = new Boom(error.message, { statusCode: error.statusCode }); - - if (error.headers) { - Object.keys(error.headers).forEach(header => { - genericError.output.headers[header] = error.headers[header]; - }); - } - - // Boom hides the error when status code is 500 - - genericError.output.payload.message = error.message; - - throw genericError; - } - }, - method: ['GET', 'POST'], - path: options.path || '/graphql', - vhost: options.vhost || undefined, - }); - }, -}; - -export type HapiGraphiQLOptionsFunction = ( - req?: Request -) => GraphiQL.GraphiQLData | Promise; - -export interface HapiGraphiQLPluginOptions { - path: string; - - route?: any; - - graphiqlOptions: GraphiQL.GraphiQLData | HapiGraphiQLOptionsFunction; -} - -export const graphiqlHapi: Plugin = { - name: 'graphiql', - register: (server: Server, options: HapiGraphiQLPluginOptions) => { - if (!options || !options.graphiqlOptions) { - throw new Error('Apollo Server GraphiQL requires options.'); - } - - server.route({ - options: options.route || {}, - handler: async (request: Request, h: ResponseToolkit) => { - const graphiqlString = await GraphiQL.resolveGraphiQLString( - request.query, - options.graphiqlOptions, - request - ); - - return h.response(graphiqlString).type('text/html'); - }, - method: 'GET', - path: options.path || '/graphiql', - }); - }, -}; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index e96f1687bbb2e..19121d92f02c9 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -4,116 +4,207 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/array-type */ + import { GenericParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; import { Legacy } from 'kibana'; - -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { get } from 'lodash'; +import { runHttpQuery } from 'apollo-server-core'; +import { schema, TypeOf, ObjectType } from '@kbn/config-schema'; import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraFrameworkRouteOptions, - InfraResponse, + InfraRouteConfig, InfraTSVBResponse, - InfraWrappableRequest, - internalInfraFrameworkRequest, + InfraServerPluginDeps, + CallWithRequestParams, + InfraDatabaseSearchResponse, + InfraDatabaseMultiResponse, + InfraDatabaseFieldCapsResponse, + InfraDatabaseGetIndicesResponse, + InfraDatabaseGetIndicesAliasResponse, } from './adapter_types'; -import { - graphiqlHapi, - graphqlHapi, - HapiGraphiQLPluginOptions, - HapiGraphQLPluginOptions, -} from './apollo_server_hapi'; import { TSVBMetricModel } from '../../../../common/inventory_models/types'; +import { + CoreSetup, + IRouter, + KibanaRequest, + RequestHandlerContext, + KibanaResponseFactory, + RouteMethod, +} from '../../../../../../../../src/core/server'; +import { RequestHandler } from '../../../../../../../../src/core/server'; +import { InfraConfig } from '../../../../../../../plugins/infra/server'; -interface CallWithRequestParams extends GenericParams { - max_concurrent_shard_requests?: number; -} - -export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFrameworkAdapter { - public version: string; +export class KibanaFramework { + public router: IRouter; + private core: CoreSetup; + public plugins: InfraServerPluginDeps; - constructor(private server: Legacy.Server) { - this.version = server.config().get('pkg.version'); + constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { + this.router = core.http.createRouter(); + this.core = core; + this.plugins = plugins; } - public config(req: InfraFrameworkRequest): KibanaConfig { - const internalRequest = req[internalInfraFrameworkRequest]; - return internalRequest.server.config(); + public registerRoute< + params extends ObjectType = any, + query extends ObjectType = any, + body extends ObjectType = any, + method extends RouteMethod = any + >( + config: InfraRouteConfig, + handler: RequestHandler + ) { + const defaultOptions = { + tags: ['access:infra'], + }; + const routeConfig = { + path: config.path, + validate: config.validate, + // Currently we have no use of custom options beyond tags, this can be extended + // beyond defaultOptions if it's needed. + options: defaultOptions, + }; + switch (config.method) { + case 'get': + this.router.get(routeConfig, handler); + break; + case 'post': + this.router.post(routeConfig, handler); + break; + case 'delete': + this.router.delete(routeConfig, handler); + break; + case 'put': + this.router.put(routeConfig, handler); + break; + } } - public exposeStaticDir(urlPath: string, dir: string): void { - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } + public registerGraphQLEndpoint(routePath: string, gqlSchema: GraphQLSchema) { + // These endpoints are validated by GraphQL at runtime and with GraphQL generated types + const body = schema.object({}, { allowUnknowns: true }); + type Body = TypeOf; - public registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void { - this.server.register({ - options: { - graphqlOptions: (req: Legacy.Request) => ({ - context: { req: wrapRequest(req) }, - schema, - }), - path: routePath, - route: { - tags: ['access:infra'], - }, + const routeOptions = { + path: `/api/infra${routePath}`, + validate: { + body, }, - plugin: graphqlHapi, - }); - - this.server.register({ options: { - graphiqlOptions: request => ({ - endpointURL: request ? `${request.getBasePath()}${routePath}` : routePath, - passHeader: `'kbn-version': '${this.version}'`, - }), - path: `${routePath}/graphiql`, - route: { - tags: ['access:infra'], - }, + tags: ['access:infra'], }, - plugin: graphiqlHapi, - }); - } + }; + async function handler( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + try { + const query = + request.route.method === 'post' + ? (request.body as Record) + : (request.query as Record); - public registerRoute< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse - >(route: InfraFrameworkRouteOptions) { - const wrappedHandler = (request: any, h: Legacy.ResponseToolkit) => - route.handler(wrapRequest(request), h); - - this.server.route({ - handler: wrappedHandler, - options: route.options, - method: route.method, - path: route.path, - }); + const gqlResponse = await runHttpQuery([context, request], { + method: request.route.method.toUpperCase(), + options: (req: RequestHandlerContext, rawReq: KibanaRequest) => ({ + context: { req, rawReq }, + schema: gqlSchema, + }), + query, + }); + + return response.ok({ + body: gqlResponse, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + const errorBody = { + message: error.message, + }; + + if ('HttpQueryError' !== error.name) { + return response.internalError({ + body: errorBody, + }); + } + + if (error.isGraphQLError === true) { + return response.customError({ + statusCode: error.statusCode, + body: errorBody, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + const { headers = [], statusCode = 500 } = error; + return response.customError({ + statusCode, + headers, + body: errorBody, + }); + + // NP_TODO: Do we still need to re-throw this error in this case? if we do, can we + // still call the response.customError method to control the HTTP response? + // throw error; + } + } + this.router.post(routeOptions, handler); + this.router.get(routeOptions, handler); } - public async callWithRequest( - req: InfraFrameworkRequest, + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'search', + options?: CallWithRequestParams + ): Promise>; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'msearch', + options?: CallWithRequestParams + ): Promise>; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'fieldCaps', + options?: CallWithRequestParams + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'indices.existsAlias', + options?: CallWithRequestParams + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + method: 'indices.getAlias', + options?: object + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + method: 'indices.get' | 'ml.getBuckets', + options?: object + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: string, + options?: CallWithRequestParams + ): Promise; + + public async callWithRequest( + requestContext: RequestHandlerContext, endpoint: string, - params: CallWithRequestParams, - ...rest: any[] + params: CallWithRequestParams ) { - const internalRequest = req[internalInfraFrameworkRequest]; - const { elasticsearch } = internalRequest.server.plugins; - const { callWithRequest } = elasticsearch.getCluster('data'); - const includeFrozen = await internalRequest.getUiSettingsService().get('search:includeFrozen'); + const { elasticsearch, uiSettings } = requestContext.core; + + const includeFrozen = await uiSettings.client.get('search:includeFrozen'); if (endpoint === 'msearch') { - const maxConcurrentShardRequests = await internalRequest - .getUiSettingsService() - .get('courier:maxConcurrentShardRequests'); + const maxConcurrentShardRequests = await uiSettings.client.get( + 'courier:maxConcurrentShardRequests' + ); if (maxConcurrentShardRequests > 0) { params = { ...params, max_concurrent_shard_requests: maxConcurrentShardRequests }; } @@ -125,95 +216,79 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework } : {}; - const fields = await callWithRequest( - internalRequest, - endpoint, - { - ...params, - ...frozenIndicesParams, - }, - ...rest - ); - return fields; + return elasticsearch.dataClient.callAsCurrentUser(endpoint, { + ...params, + ...frozenIndicesParams, + }); } public getIndexPatternsService( - request: InfraFrameworkRequest + requestContext: RequestHandlerContext ): Legacy.IndexPatternsService { - return this.server.indexPatternsServiceFactory({ + return this.plugins.indexPatterns.indexPatternsServiceFactory({ callCluster: async (method: string, args: [GenericParams], ...rest: any[]) => { - const fieldCaps = await this.callWithRequest( - request, - method, - { ...args, allowNoIndices: true } as GenericParams, - ...rest - ); + const fieldCaps = await this.callWithRequest(requestContext, method, { + ...args, + allowNoIndices: true, + } as GenericParams); return fieldCaps; }, }); } - public getSpaceId(request: InfraFrameworkRequest): string { - const spacesPlugin = this.server.plugins.spaces; + public getSpaceId(request: KibanaRequest): string { + const spacesPlugin = this.plugins.spaces; - if (spacesPlugin && typeof spacesPlugin.getSpaceId === 'function') { - return spacesPlugin.getSpaceId(request[internalInfraFrameworkRequest]); + if ( + spacesPlugin && + spacesPlugin.spacesService && + typeof spacesPlugin.spacesService.getSpaceId === 'function' + ) { + return spacesPlugin.spacesService.getSpaceId(request); } else { return 'default'; } } - public getSavedObjectsService() { - return this.server.savedObjects; - } - + // NP_TODO: This method needs to no longer require full KibanaRequest public async makeTSVBRequest( - req: InfraFrameworkRequest, + request: KibanaRequest, model: TSVBMetricModel, timerange: { min: number; max: number }, - filters: any[] - ) { - const internalRequest = req[internalInfraFrameworkRequest]; - const server = internalRequest.server; - const getVisData = get(server, 'plugins.metrics.getVisData'); + filters: any[], + requestContext: RequestHandlerContext + ): Promise { + const { getVisData } = this.plugins.metrics; if (typeof getVisData !== 'function') { throw new Error('TSVB is not available'); } - - // getBasePath returns randomized base path AND spaces path - const basePath = internalRequest.getBasePath(); - const url = `${basePath}/api/metrics/vis/data`; - + const url = this.core.http.basePath.prepend('/api/metrics/vis/data'); // For the following request we need a copy of the instnace of the internal request // but modified for our TSVB request. This will ensure all the instance methods // are available along with our overriden values - const request = Object.assign( - Object.create(Object.getPrototypeOf(internalRequest)), - internalRequest, - { - url, - method: 'POST', - payload: { - timerange, - panels: [model], - filters, + const requestCopy = Object.assign({}, request, { + url, + method: 'POST', + payload: { + timerange, + panels: [model], + filters, + }, + // NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that + // originate from the New Platform router (and are very different to the old request object). + // Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just + // the requestContext, this can be removed. + server: { + plugins: { + elasticsearch: this.plugins.___legacy.tsvb.elasticsearch, }, - } - ); - const result = await getVisData(request); - return result as InfraTSVBResponse; + newPlatform: { + __internals: this.plugins.___legacy.tsvb.__internals, + }, + }, + getUiSettingsService: () => requestContext.core.uiSettings.client, + getSavedObjectsClient: () => requestContext.core.savedObjects.client, + }); + return getVisData(requestCopy); } } - -export function wrapRequest( - req: InternalRequest -): InfraFrameworkRequest { - const { params, payload, query } = req; - - return { - [internalInfraFrameworkRequest]: req, - params, - payload, - query, - }; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 547e74eecb67c..ec45171baa7b0 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -15,6 +15,7 @@ import zip from 'lodash/fp/zip'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity, constant } from 'fp-ts/lib/function'; +import { RequestHandlerContext } from 'src/core/server'; import { compareTimeKeys, isTimeKey, TimeKey } from '../../../../common/time'; import { JsonObject } from '../../../../common/typed_json'; import { @@ -24,8 +25,8 @@ import { LogSummaryBucket, } from '../../domains/log_entries_domain'; import { InfraSourceConfiguration } from '../../sources'; -import { InfraFrameworkRequest, SortedSearchHit } from '../framework'; -import { InfraBackendFrameworkAdapter } from '../framework'; +import { SortedSearchHit } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; const DAY_MILLIS = 24 * 60 * 60 * 1000; const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000, Infinity].map(days => days * DAY_MILLIS); @@ -39,10 +40,10 @@ interface LogItemHit { } export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { - constructor(private readonly framework: InfraBackendFrameworkAdapter) {} + constructor(private readonly framework: KibanaFramework) {} public async getAdjacentLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -64,7 +65,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } const documentsInInterval = await this.getLogEntryDocumentsBetween( - request, + requestContext, sourceConfiguration, fields, intervalStart, @@ -82,7 +83,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getContainedLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -91,7 +92,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { highlightQuery?: LogEntryQuery ): Promise { const documents = await this.getLogEntryDocumentsBetween( - request, + requestContext, sourceConfiguration, fields, start.time, @@ -106,7 +107,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getContainedLogSummaryBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, start: number, end: number, @@ -165,7 +166,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { }, }; - const response = await this.framework.callWithRequest(request, 'search', query); + const response = await this.framework.callWithRequest(requestContext, 'search', query); return pipe( LogSummaryResponseRuntimeType.decode(response), @@ -179,12 +180,12 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, sourceConfiguration: InfraSourceConfiguration ) { const search = (searchOptions: object) => - this.framework.callWithRequest(request, 'search', searchOptions); + this.framework.callWithRequest(requestContext, 'search', searchOptions); const params = { index: sourceConfiguration.logAlias, @@ -212,7 +213,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } private async getLogEntryDocumentsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: number, @@ -298,7 +299,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { }; const response = await this.framework.callWithRequest( - request, + requestContext, 'search', query ); diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts index adb8c811ed57d..acd7a2528bb42 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; import { InfraMetric, InfraMetricData, InfraNodeType, InfraTimerangeInput, } from '../../../graphql/types'; - import { InfraSourceConfiguration } from '../../sources'; -import { InfraFrameworkRequest } from '../framework'; export interface InfraMetricsRequestOptions { nodeIds: { @@ -27,8 +26,9 @@ export interface InfraMetricsRequestOptions { export interface InfraMetricsAdapter { getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + request: KibanaRequest // NP_TODO: temporarily needed until metrics getVisData no longer needs full request ): Promise; } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 331abd4ffb35a..db3c516841cd4 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -6,10 +6,9 @@ import { i18n } from '@kbn/i18n'; import { flatten, get } from 'lodash'; - -import Boom from 'boom'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { InfraMetric, InfraMetricData, InfraNodeType } from '../../../graphql/types'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types'; import { checkValidNode } from './lib/check_valid_node'; import { metrics } from '../../../../common/inventory_models'; @@ -17,15 +16,16 @@ import { TSVBMetricModelCreator } from '../../../../common/inventory_models/type import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { - private framework: InfraBackendFrameworkAdapter; + private framework: KibanaFramework; - constructor(framework: InfraBackendFrameworkAdapter) { + constructor(framework: KibanaFramework) { this.framework = framework; } public async getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + rawRequest: KibanaRequest // NP_TODO: Temporarily needed until metrics getVisData no longer needs full request ): Promise { const fields = { [InfraNodeType.host]: options.sourceConfiguration.fields.host, @@ -35,11 +35,11 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; const nodeField = fields[options.nodeType]; const search = (searchOptions: object) => - this.framework.callWithRequest<{}, Aggregation>(req, 'search', searchOptions); + this.framework.callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); const validNode = await checkValidNode(search, indexPattern, nodeField, options.nodeIds.nodeId); if (!validNode) { - throw Boom.notFound( + throw new Error( i18n.translate('xpack.infra.kibanaMetrics.nodeDoesNotExistErrorMessage', { defaultMessage: '{nodeId} does not exist.', values: { @@ -50,7 +50,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { } const requests = options.metrics.map(metricId => - this.makeTSVBRequest(metricId, options, req, nodeField) + this.makeTSVBRequest(metricId, options, rawRequest, nodeField, requestContext) ); return Promise.all(requests) @@ -92,12 +92,13 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { async makeTSVBRequest( metricId: InfraMetric, options: InfraMetricsRequestOptions, - req: InfraFrameworkRequest, - nodeField: string + req: KibanaRequest, + nodeField: string, + requestContext: RequestHandlerContext ) { const createTSVBModel = get(metrics, ['tsvb', metricId]) as TSVBMetricModelCreator | undefined; if (!createTSVBModel) { - throw Boom.badRequest( + throw new Error( i18n.translate('xpack.infra.metrics.missingTSVBModelError', { defaultMessage: 'The TSVB model for {metricId} does not exist for {nodeType}', values: { @@ -121,7 +122,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ); const calculatedInterval = await calculateMetricInterval( this.framework, - req, + requestContext, { indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, timestampField: options.sourceConfiguration.fields.timestamp, @@ -135,7 +136,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { } if (model.id_type === 'cloud' && !options.nodeIds.cloudId) { - throw Boom.badRequest( + throw new Error( i18n.translate('xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage', { defaultMessage: 'Model for {metricId} requires a cloudId, but none was given for {nodeId}.', @@ -152,6 +153,6 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ? [{ match: { [model.map_field_to]: id } }] : [{ match: { [nodeField]: id } }]; - return this.framework.makeTSVBRequest(req, model, timerange, filters); + return this.framework.makeTSVBRequest(req, model, timerange, filters, requestContext); } } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts index e66da3f3fa6cb..635f6ff9762c5 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts @@ -4,26 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { InfraSourceStatusAdapter } from '../../source_status'; -import { - InfraBackendFrameworkAdapter, - InfraDatabaseGetIndicesResponse, - InfraFrameworkRequest, -} from '../framework'; +import { InfraDatabaseGetIndicesResponse } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusAdapter { - constructor(private readonly framework: InfraBackendFrameworkAdapter) {} + constructor(private readonly framework: KibanaFramework) {} - public async getIndexNames(request: InfraFrameworkRequest, aliasName: string) { + public async getIndexNames(requestContext: RequestHandlerContext, aliasName: string) { const indexMaps = await Promise.all([ this.framework - .callWithRequest(request, 'indices.getAlias', { + .callWithRequest(requestContext, 'indices.getAlias', { name: aliasName, filterPath: '*.settings.index.uuid', // to keep the response size as small as possible }) .catch(withDefaultIfNotFound({})), this.framework - .callWithRequest(request, 'indices.get', { + .callWithRequest(requestContext, 'indices.get', { index: aliasName, filterPath: '*.settings.index.uuid', // to keep the response size as small as possible }) @@ -36,15 +34,15 @@ export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusA ); } - public async hasAlias(request: InfraFrameworkRequest, aliasName: string) { - return await this.framework.callWithRequest(request, 'indices.existsAlias', { + public async hasAlias(requestContext: RequestHandlerContext, aliasName: string) { + return await this.framework.callWithRequest(requestContext, 'indices.existsAlias', { name: aliasName, }); } - public async hasIndices(request: InfraFrameworkRequest, indexNames: string) { + public async hasIndices(requestContext: RequestHandlerContext, indexNames: string) { return await this.framework - .callWithRequest(request, 'search', { + .callWithRequest(requestContext, 'search', { ignore_unavailable: true, allow_no_indices: true, index: indexNames, diff --git a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts index 215c41bcf6b7c..305841aa52d36 100644 --- a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts @@ -3,12 +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 { Server } from 'hapi'; - -import { InfraKibanaConfigurationAdapter } from '../adapters/configuration/kibana_configuration_adapter'; import { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter'; -import { InfraKibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter'; import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter'; import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status'; @@ -20,13 +16,14 @@ import { InfraLogAnalysis } from '../log_analysis'; import { InfraSnapshot } from '../snapshot'; import { InfraSourceStatus } from '../source_status'; import { InfraSources } from '../sources'; +import { InfraConfig } from '../../../../../../plugins/infra/server'; +import { CoreSetup } from '../../../../../../../src/core/server'; +import { InfraServerPluginDeps } from '../adapters/framework/adapter_types'; -export function compose(server: Server): InfraBackendLibs { - const configuration = new InfraKibanaConfigurationAdapter(server); - const framework = new InfraKibanaBackendFrameworkAdapter(server); +export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { + const framework = new KibanaFramework(core, config, plugins); const sources = new InfraSources({ - configuration, - savedObjects: framework.getSavedObjectsService(), + config, }); const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), { sources, @@ -34,6 +31,7 @@ export function compose(server: Server): InfraBackendLibs { const snapshot = new InfraSnapshot({ sources, framework }); const logAnalysis = new InfraLogAnalysis({ framework }); + // TODO: separate these out individually and do away with "domains" as a temporary group const domainLibs: InfraDomainLibs = { fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { sources, @@ -45,7 +43,7 @@ export function compose(server: Server): InfraBackendLibs { }; const libs: InfraBackendLibs = { - configuration, + configuration: config, // NP_TODO: Do we ever use this anywhere? framework, logAnalysis, snapshot, diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts index c5a3bbeb87449..a00c76216da4c 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { InfraIndexField, InfraIndexType } from '../../graphql/types'; import { FieldsAdapter } from '../adapters/fields'; -import { InfraFrameworkRequest } from '../adapters/framework'; import { InfraSources } from '../sources'; export class InfraFieldsDomain { @@ -16,16 +16,19 @@ export class InfraFieldsDomain { ) {} public async getFields( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, indexType: InfraIndexType ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const includeMetricIndices = [InfraIndexType.ANY, InfraIndexType.METRICS].includes(indexType); const includeLogIndices = [InfraIndexType.ANY, InfraIndexType.LOGS].includes(indexType); const fields = await this.adapter.getIndexFields( - request, + requestContext, `${includeMetricIndices ? configuration.metricAlias : ''},${ includeLogIndices ? configuration.logAlias : '' }`, diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 0127f80b31357..597073b1e901f 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -7,6 +7,7 @@ import stringify from 'json-stable-stringify'; import { sortBy } from 'lodash'; +import { RequestHandlerContext } from 'src/core/server'; import { TimeKey } from '../../../../common/time'; import { JsonObject } from '../../../../common/typed_json'; import { @@ -16,7 +17,6 @@ import { InfraLogSummaryBucket, InfraLogSummaryHighlightBucket, } from '../../../graphql/types'; -import { InfraFrameworkRequest } from '../../adapters/framework'; import { InfraSourceConfiguration, InfraSources, @@ -40,7 +40,7 @@ export class InfraLogEntriesDomain { ) {} public async getLogEntriesAround( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, key: TimeKey, maxCountBefore: number, @@ -55,14 +55,17 @@ export class InfraLogEntriesDomain { }; } - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); const requiredFields = getRequiredFields(configuration, messageFormattingRules); const documentsBefore = await this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, key, @@ -80,7 +83,7 @@ export class InfraLogEntriesDomain { }; const documentsAfter = await this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, lastKeyBefore, @@ -101,20 +104,23 @@ export class InfraLogEntriesDomain { } public async getLogEntriesBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startKey: TimeKey, endKey: TimeKey, filterQuery?: LogEntryQuery, highlightQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); const requiredFields = getRequiredFields(configuration, messageFormattingRules); const documents = await this.adapter.getContainedLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -129,7 +135,7 @@ export class InfraLogEntriesDomain { } public async getLogEntryHighlights( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startKey: TimeKey, endKey: TimeKey, @@ -140,7 +146,10 @@ export class InfraLogEntriesDomain { }>, filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); @@ -158,7 +167,7 @@ export class InfraLogEntriesDomain { : highlightQuery; const [documentsBefore, documents, documentsAfter] = await Promise.all([ this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -168,7 +177,7 @@ export class InfraLogEntriesDomain { highlightQuery ), this.adapter.getContainedLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -177,7 +186,7 @@ export class InfraLogEntriesDomain { highlightQuery ), this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, endKey, @@ -203,16 +212,19 @@ export class InfraLogEntriesDomain { } public async getLogSummaryBucketsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, start: number, end: number, bucketSize: number, filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( - request, + requestContext, configuration, start, end, @@ -223,7 +235,7 @@ export class InfraLogEntriesDomain { } public async getLogSummaryHighlightBucketsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, start: number, end: number, @@ -231,7 +243,10 @@ export class InfraLogEntriesDomain { highlightQueries: string[], filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); @@ -248,7 +263,7 @@ export class InfraLogEntriesDomain { } : highlightQuery; const summaryBuckets = await this.adapter.getContainedLogSummaryBuckets( - request, + requestContext, configuration, start, end, @@ -266,11 +281,11 @@ export class InfraLogEntriesDomain { } public async getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, sourceConfiguration: InfraSourceConfiguration ): Promise { - const document = await this.adapter.getLogItem(request, id, sourceConfiguration); + const document = await this.adapter.getLogItem(requestContext, id, sourceConfiguration); const defaultFields = [ { field: '_index', value: document._index }, { field: '_id', value: document._id }, @@ -300,7 +315,7 @@ interface LogItemHit { export interface LogEntriesAdapter { getAdjacentLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -311,7 +326,7 @@ export interface LogEntriesAdapter { ): Promise; getContainedLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -321,7 +336,7 @@ export interface LogEntriesAdapter { ): Promise; getContainedLogSummaryBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, start: number, end: number, @@ -330,7 +345,7 @@ export interface LogEntriesAdapter { ): Promise; getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, source: InfraSourceConfiguration ): Promise; diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts index 862ca8b4c823f..5d7d54a6a2e50 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { InfraMetricData } from '../../graphql/types'; -import { InfraFrameworkRequest } from '../adapters/framework/adapter_types'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from '../adapters/metrics/adapter_types'; export class InfraMetricsDomain { @@ -16,9 +16,10 @@ export class InfraMetricsDomain { } public async getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + rawRequest: KibanaRequest // NP_TODO: temporarily needed until metrics getVisData no longer needs full request ): Promise { - return await this.adapter.getMetrics(req, options); + return await this.adapter.getMetrics(requestContext, options, rawRequest); } } diff --git a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts index b436bb7e4fe58..46d32885600df 100644 --- a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts @@ -5,8 +5,6 @@ */ import { InfraSourceConfiguration } from '../../public/graphql/types'; -import { InfraConfigurationAdapter } from './adapters/configuration'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from './adapters/framework'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; @@ -14,6 +12,15 @@ import { InfraLogAnalysis } from './log_analysis/log_analysis'; import { InfraSnapshot } from './snapshot'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; +import { InfraConfig } from '../../../../../plugins/infra/server'; +import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; + +// NP_TODO: We shouldn't need this context anymore but I am +// not sure how the graphql stuff uses it, so we can't remove it yet +export interface InfraContext { + req: any; + rawReq?: any; +} export interface InfraDomainLibs { fields: InfraFieldsDomain; @@ -22,8 +29,8 @@ export interface InfraDomainLibs { } export interface InfraBackendLibs extends InfraDomainLibs { - configuration: InfraConfigurationAdapter; - framework: InfraBackendFrameworkAdapter; + configuration: InfraConfig; + framework: KibanaFramework; logAnalysis: InfraLogAnalysis; snapshot: InfraSnapshot; sources: InfraSources; @@ -40,7 +47,3 @@ export interface InfraConfiguration { default: InfraSourceConfiguration; }; } - -export interface InfraContext { - req: InfraFrameworkRequest; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts index d970a142c5c23..fac49a7980f26 100644 --- a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts @@ -9,7 +9,7 @@ import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { getJobId } from '../../../common/log_analysis'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../adapters/framework'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { NoLogRateResultsIndexError } from './errors'; import { logRateModelPlotResponseRT, @@ -17,37 +17,38 @@ import { LogRateModelPlotBucket, CompositeTimestampPartitionKey, } from './queries'; +import { RequestHandlerContext, KibanaRequest } from '../../../../../../../src/core/server'; const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; export class InfraLogAnalysis { constructor( private readonly libs: { - framework: InfraBackendFrameworkAdapter; + framework: KibanaFramework; } ) {} - public getJobIds(request: InfraFrameworkRequest, sourceId: string) { + public getJobIds(request: KibanaRequest, sourceId: string) { return { logEntryRate: getJobId(this.libs.framework.getSpaceId(request), sourceId, 'log-entry-rate'), }; } public async getLogEntryRateBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startTime: number, endTime: number, - bucketDuration: number + bucketDuration: number, + request: KibanaRequest ) { const logRateJobId = this.getJobIds(request, sourceId).logEntryRate; - let mlModelPlotBuckets: LogRateModelPlotBucket[] = []; let afterLatestBatchKey: CompositeTimestampPartitionKey | undefined; while (true) { const mlModelPlotResponse = await this.libs.framework.callWithRequest( - request, + requestContext, 'search', createLogEntryRateQuery( logRateJobId, diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts index 741293f61056e..59a4e8911a94d 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts @@ -5,6 +5,7 @@ */ import { idx } from '@kbn/elastic-idx'; +import { RequestHandlerContext } from 'src/core/server'; import { InfraSnapshotGroupbyInput, InfraSnapshotMetricInput, @@ -13,11 +14,8 @@ import { InfraNodeType, InfraSourceConfiguration, } from '../../graphql/types'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../adapters/framework'; +import { InfraDatabaseSearchResponse } from '../adapters/framework'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraSources } from '../sources'; import { JsonObject } from '../../../common/typed_json'; @@ -49,20 +47,18 @@ export interface InfraSnapshotRequestOptions { } export class InfraSnapshot { - constructor( - private readonly libs: { sources: InfraSources; framework: InfraBackendFrameworkAdapter } - ) {} + constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {} public async getNodes( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions ): Promise { // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged // when they have both been completed. - const groupedNodesPromise = requestGroupedNodes(request, options, this.libs.framework); - const nodeMetricsPromise = requestNodeMetrics(request, options, this.libs.framework); + const groupedNodesPromise = requestGroupedNodes(requestContext, options, this.libs.framework); + const nodeMetricsPromise = requestNodeMetrics(requestContext, options, this.libs.framework); const groupedNodeBuckets = await groupedNodesPromise; const nodeMetricBuckets = await nodeMetricsPromise; @@ -79,9 +75,9 @@ const handleAfterKey = createAfterKeyHandler('body.aggregations.nodes.composite. ); const requestGroupedNodes = async ( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions, - framework: InfraBackendFrameworkAdapter + framework: KibanaFramework ): Promise => { const query = { allowNoIndices: true, @@ -130,13 +126,13 @@ const requestGroupedNodes = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeGroupByBucket - >(framework, request, query, bucketSelector, handleAfterKey); + >(framework, requestContext, query, bucketSelector, handleAfterKey); }; const requestNodeMetrics = async ( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions, - framework: InfraBackendFrameworkAdapter + framework: KibanaFramework ): Promise => { const index = options.metric.type === 'logRate' @@ -191,7 +187,7 @@ const requestNodeMetrics = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeMetricsBucket - >(framework, request, query, bucketSelector, handleAfterKey); + >(framework, requestContext, query, bucketSelector, handleAfterKey); }; // buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] diff --git a/x-pack/legacy/plugins/infra/server/lib/source_status.ts b/x-pack/legacy/plugins/infra/server/lib/source_status.ts index f9f37b5aa9e5a..1f0845b6b223f 100644 --- a/x-pack/legacy/plugins/infra/server/lib/source_status.ts +++ b/x-pack/legacy/plugins/infra/server/lib/source_status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraFrameworkRequest } from './adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; import { InfraSources } from './sources'; export class InfraSourceStatus { @@ -14,58 +14,85 @@ export class InfraSourceStatus { ) {} public async getLogIndexNames( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const indexNames = await this.adapter.getIndexNames( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return indexNames; } public async getMetricIndexNames( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const indexNames = await this.adapter.getIndexNames( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return indexNames; } - public async hasLogAlias(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasLogAlias( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasAlias = await this.adapter.hasAlias( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return hasAlias; } - public async hasMetricAlias(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasMetricAlias( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasAlias = await this.adapter.hasAlias( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return hasAlias; } - public async hasLogIndices(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasLogIndices( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasIndices = await this.adapter.hasIndices( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return hasIndices; } public async hasMetricIndices( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasIndices = await this.adapter.hasIndices( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return hasIndices; @@ -73,7 +100,7 @@ export class InfraSourceStatus { } export interface InfraSourceStatusAdapter { - getIndexNames(request: InfraFrameworkRequest, aliasName: string): Promise; - hasAlias(request: InfraFrameworkRequest, aliasName: string): Promise; - hasIndices(request: InfraFrameworkRequest, indexNames: string): Promise; + getIndexNames(requestContext: RequestHandlerContext, aliasName: string): Promise; + hasAlias(requestContext: RequestHandlerContext, aliasName: string): Promise; + hasIndices(requestContext: RequestHandlerContext, indexNames: string): Promise; } diff --git a/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts index 2374a83a642df..4a83ca730ff83 100644 --- a/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts @@ -3,34 +3,31 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { InfraInmemoryConfigurationAdapter } from '../adapters/configuration/inmemory_configuration_adapter'; import { InfraSources } from './sources'; describe('the InfraSources lib', () => { describe('getSourceConfiguration method', () => { test('returns a source configuration if it exists', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({}), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: { - metricAlias: 'METRIC_ALIAS', - logAlias: 'LOG_ALIAS', - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, - }, - }), + config: createMockStaticConfiguration({}), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: { + metricAlias: 'METRIC_ALIAS', + logAlias: 'LOG_ALIAS', + fields: { + container: 'CONTAINER', + host: 'HOST', + pod: 'POD', + tiebreaker: 'TIEBREAKER', + timestamp: 'TIMESTAMP', + }, + }, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -52,7 +49,7 @@ describe('the InfraSources lib', () => { test('adds missing attributes from the static configuration to a source configuration', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({ + config: createMockStaticConfiguration({ default: { metricAlias: 'METRIC_ALIAS', logAlias: 'LOG_ALIAS', @@ -64,19 +61,18 @@ describe('the InfraSources lib', () => { }, }, }), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: { - fields: { - container: 'CONTAINER', - }, - }, - }), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: { + fields: { + container: 'CONTAINER', + }, + }, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -98,16 +94,15 @@ describe('the InfraSources lib', () => { test('adds missing attributes from the default configuration to a source configuration', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({}), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: {}, - }), + config: createMockStaticConfiguration({}), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: {}, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -129,29 +124,30 @@ describe('the InfraSources lib', () => { }); }); -const createMockStaticConfiguration = (sources: any) => - new InfraInmemoryConfigurationAdapter({ - enabled: true, - query: { - partitionSize: 1, - partitionFactor: 1, - }, - sources, - }); - -const createMockSavedObjectsService = (savedObject?: any) => ({ - getScopedSavedObjectsClient() { - return { - async get() { - return savedObject; - }, - } as any; +const createMockStaticConfiguration = (sources: any) => ({ + enabled: true, + query: { + partitionSize: 1, + partitionFactor: 1, }, - SavedObjectsClient: { - errors: { - isNotFoundError() { - return typeof savedObject === 'undefined'; + sources, +}); + +const createRequestContext = (savedObject?: any) => { + return { + core: { + savedObjects: { + client: { + async get() { + return savedObject; + }, + errors: { + isNotFoundError() { + return typeof savedObject === 'undefined'; + }, + }, + }, }, }, - }, -}); + }; +}; diff --git a/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts b/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts index 951556a0fe642..2b38d81e4a8d5 100644 --- a/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts @@ -6,14 +6,10 @@ import * as runtimeTypes from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { Legacy } from 'kibana'; - import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; -import { Pick3 } from '../../../common/utility_types'; -import { InfraConfigurationAdapter } from '../adapters/configuration'; -import { InfraFrameworkRequest, internalInfraFrameworkRequest } from '../adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; @@ -25,19 +21,21 @@ import { SourceConfigurationSavedObjectRuntimeType, StaticSourceConfigurationRuntimeType, } from './types'; +import { InfraConfig } from '../../../../../../plugins/infra/server'; + +interface Libs { + config: InfraConfig; +} export class InfraSources { private internalSourceConfigurations: Map = new Map(); + private readonly libs: Libs; - constructor( - private readonly libs: { - configuration: InfraConfigurationAdapter; - savedObjects: Pick & - Pick3; - } - ) {} + constructor(libs: Libs) { + this.libs = libs; + } - public async getSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { + public async getSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) @@ -53,7 +51,7 @@ export class InfraSources { })) .catch(err => err instanceof NotFoundError - ? this.getSavedSourceConfiguration(request, sourceId).then(result => ({ + ? this.getSavedSourceConfiguration(requestContext, sourceId).then(result => ({ ...result, configuration: mergeSourceConfiguration( staticDefaultSourceConfiguration, @@ -63,7 +61,7 @@ export class InfraSources { : Promise.reject(err) ) .catch(err => - this.libs.savedObjects.SavedObjectsClient.errors.isNotFoundError(err) + requestContext.core.savedObjects.client.errors.isNotFoundError(err) ? Promise.resolve({ id: sourceId, version: undefined, @@ -77,10 +75,10 @@ export class InfraSources { return savedSourceConfiguration; } - public async getAllSourceConfigurations(request: InfraFrameworkRequest) { + public async getAllSourceConfigurations(requestContext: RequestHandlerContext) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(request); + const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(requestContext); return savedSourceConfigurations.map(savedSourceConfiguration => ({ ...savedSourceConfiguration, @@ -92,7 +90,7 @@ export class InfraSources { } public async createSourceConfiguration( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, source: InfraSavedSourceConfiguration ) { @@ -104,13 +102,11 @@ export class InfraSources { ); const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .create( - infraSourceConfigurationSavedObjectType, - pickSavedSourceConfiguration(newSourceConfiguration) as any, - { id: sourceId } - ) + await requestContext.core.savedObjects.client.create( + infraSourceConfigurationSavedObjectType, + pickSavedSourceConfiguration(newSourceConfiguration) as any, + { id: sourceId } + ) ); return { @@ -122,20 +118,21 @@ export class InfraSources { }; } - public async deleteSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .delete(infraSourceConfigurationSavedObjectType, sourceId); + public async deleteSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { + await requestContext.core.savedObjects.client.delete( + infraSourceConfigurationSavedObjectType, + sourceId + ); } public async updateSourceConfiguration( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, sourceProperties: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const { configuration, version } = await this.getSourceConfiguration(request, sourceId); + const { configuration, version } = await this.getSourceConfiguration(requestContext, sourceId); const updatedSourceConfigurationAttributes = mergeSourceConfiguration( configuration, @@ -143,16 +140,14 @@ export class InfraSources { ); const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .update( - infraSourceConfigurationSavedObjectType, - sourceId, - pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, - { - version, - } - ) + await requestContext.core.savedObjects.client.update( + infraSourceConfigurationSavedObjectType, + sourceId, + pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, + { + version, + } + ) ); return { @@ -184,7 +179,6 @@ export class InfraSources { } private async getStaticDefaultSourceConfiguration() { - const staticConfiguration = await this.libs.configuration.get(); const staticSourceConfiguration = pipe( runtimeTypes .type({ @@ -192,7 +186,7 @@ export class InfraSources { default: StaticSourceConfigurationRuntimeType, }), }) - .decode(staticConfiguration), + .decode(this.libs.config), map(({ sources: { default: defaultConfiguration } }) => defaultConfiguration), fold(constant({}), identity) ); @@ -200,12 +194,11 @@ export class InfraSources { return mergeSourceConfiguration(defaultSourceConfiguration, staticSourceConfiguration); } - private async getSavedSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { - const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient( - request[internalInfraFrameworkRequest] - ); - - const savedObject = await savedObjectsClient.get( + private async getSavedSourceConfiguration( + requestContext: RequestHandlerContext, + sourceId: string + ) { + const savedObject = await requestContext.core.savedObjects.client.get( infraSourceConfigurationSavedObjectType, sourceId ); @@ -213,12 +206,8 @@ export class InfraSources { return convertSavedObjectToSavedSourceConfiguration(savedObject); } - private async getAllSavedSourceConfigurations(request: InfraFrameworkRequest) { - const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient( - request[internalInfraFrameworkRequest] - ); - - const savedObjects = await savedObjectsClient.find({ + private async getAllSavedSourceConfigurations(requestContext: RequestHandlerContext) { + const savedObjects = await requestContext.core.savedObjects.client.find({ type: infraSourceConfigurationSavedObjectType, }); diff --git a/x-pack/legacy/plugins/infra/server/new_platform_index.ts b/x-pack/legacy/plugins/infra/server/new_platform_index.ts new file mode 100644 index 0000000000000..6b759ecfe9fde --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/new_platform_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 { PluginInitializerContext } from 'src/core/server'; +import { InfraServerPlugin } from './new_platform_plugin'; +import { config, InfraConfig } from '../../../../plugins/infra/server'; +import { InfraServerPluginDeps } from './lib/adapters/framework'; + +export { config, InfraConfig, InfraServerPluginDeps }; + +export function plugin(context: PluginInitializerContext) { + return new InfraServerPlugin(context); +} diff --git a/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts b/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts new file mode 100644 index 0000000000000..462a07574b2dd --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts @@ -0,0 +1,107 @@ +/* + * 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 { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { Server } from 'hapi'; +import { InfraConfig } from '../../../../plugins/infra/server'; +import { initInfraServer } from './infra_server'; +import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; +import { FrameworkFieldsAdapter } from './lib/adapters/fields/framework_fields_adapter'; +import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter'; +import { InfraKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter'; +import { KibanaMetricsAdapter } from './lib/adapters/metrics/kibana_metrics_adapter'; +import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_status'; +import { InfraFieldsDomain } from './lib/domains/fields_domain'; +import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain'; +import { InfraMetricsDomain } from './lib/domains/metrics_domain'; +import { InfraLogAnalysis } from './lib/log_analysis'; +import { InfraSnapshot } from './lib/snapshot'; +import { InfraSourceStatus } from './lib/source_status'; +import { InfraSources } from './lib/sources'; +import { InfraServerPluginDeps } from './lib/adapters/framework'; +import { METRICS_FEATURE, LOGS_FEATURE } from './features'; +import { UsageCollector } from './usage/usage_collector'; + +export interface KbnServer extends Server { + usage: any; +} + +const DEFAULT_CONFIG: InfraConfig = { + enabled: true, + query: { + partitionSize: 75, + partitionFactor: 1.2, + }, +}; + +export class InfraServerPlugin { + public config: InfraConfig = DEFAULT_CONFIG; + public libs: InfraBackendLibs | undefined; + + constructor(context: PluginInitializerContext) { + const config$ = context.config.create(); + config$.subscribe(configValue => { + this.config = { + ...DEFAULT_CONFIG, + enabled: configValue.enabled, + query: { + ...DEFAULT_CONFIG.query, + ...configValue.query, + }, + }; + }); + } + + getLibs() { + if (!this.libs) { + throw new Error('libs not set up yet'); + } + return this.libs; + } + + setup(core: CoreSetup, plugins: InfraServerPluginDeps) { + const framework = new KibanaFramework(core, this.config, plugins); + const sources = new InfraSources({ + config: this.config, + }); + const sourceStatus = new InfraSourceStatus( + new InfraElasticsearchSourceStatusAdapter(framework), + { + sources, + } + ); + const snapshot = new InfraSnapshot({ sources, framework }); + const logAnalysis = new InfraLogAnalysis({ framework }); + + // TODO: separate these out individually and do away with "domains" as a temporary group + const domainLibs: InfraDomainLibs = { + fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { + sources, + }), + logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), { + sources, + }), + metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), + }; + + this.libs = { + configuration: this.config, + framework, + logAnalysis, + snapshot, + sources, + sourceStatus, + ...domainLibs, + }; + + plugins.features.registerFeature(METRICS_FEATURE); + plugins.features.registerFeature(LOGS_FEATURE); + + initInfraServer(this.libs); + + // Telemetry + UsageCollector.registerUsageCollector(plugins.usageCollection); + } +} diff --git a/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts b/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts index 16837298f0704..5ad79b3d17a13 100644 --- a/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts +++ b/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts @@ -3,18 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { boomify, notFound } from 'boom'; import { first } from 'lodash'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../lib/infra_types'; -import { InfraWrappableRequest } from '../lib/adapters/framework'; - -interface IpToHostRequest { - ip: string; - index_pattern: string; -} - -type IpToHostWrappedRequest = InfraWrappableRequest; export interface IpToHostResponse { host: string; @@ -28,40 +19,47 @@ interface HostDoc { }; } -const ipToHostSchema = Joi.object({ - ip: Joi.string().required(), - index_pattern: Joi.string().required(), +const ipToHostSchema = schema.object({ + ip: schema.string(), + index_pattern: schema.string(), }); export const initIpToHostName = ({ framework }: InfraBackendLibs) => { const { callWithRequest } = framework; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/ip_to_host', - options: { - validate: { payload: ipToHostSchema }, + framework.registerRoute( + { + method: 'post', + path: '/api/infra/ip_to_host', + validate: { + body: ipToHostSchema, + }, }, - handler: async req => { + async (requestContext, { body }, response) => { try { const params = { - index: req.payload.index_pattern, + index: body.index_pattern, body: { size: 1, query: { - match: { 'host.ip': req.payload.ip }, + match: { 'host.ip': body.ip }, }, _source: ['host.name'], }, }; - const response = await callWithRequest(req, 'search', params); - if (response.hits.total.value === 0) { - throw notFound('Host with matching IP address not found.'); + const { hits } = await callWithRequest(requestContext, 'search', params); + if (hits.total.value === 0) { + return response.notFound({ + body: { message: 'Host with matching IP address not found.' }, + }); } - const hostDoc = first(response.hits.hits); - return { host: hostDoc._source.host.name }; - } catch (e) { - throw boomify(e); + const hostDoc = first(hits.hits); + return response.ok({ body: { host: hostDoc._source.host.name } }); + } catch ({ statusCode = 500, message = 'Unknown error occurred' }) { + return response.customError({ + statusCode, + body: { message }, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts index 0a369adb7ca29..1f64da1859b5f 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; - +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { LOG_ANALYSIS_VALIDATION_INDICES_PATH, @@ -20,64 +20,75 @@ import { import { throwErrors } from '../../../../common/runtime_types'; const partitionField = 'event.dataset'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => { - framework.registerRoute({ - method: 'POST', - path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, - handler: async (req, res) => { - const payload = pipe( - validationIndicesRequestPayloadRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { timestampField, indices } = payload.data; - const errors: ValidationIndicesError[] = []; + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validate: { body: escapeHatch }, + }, + async (requestContext, request, response) => { + try { + const payload = pipe( + validationIndicesRequestPayloadRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - // Query each pattern individually, to map correctly the errors - await Promise.all( - indices.map(async index => { - const fieldCaps = await framework.callWithRequest(req, 'fieldCaps', { - index, - fields: `${timestampField},${partitionField}`, - }); + const { timestampField, indices } = payload.data; + const errors: ValidationIndicesError[] = []; - if (fieldCaps.indices.length === 0) { - errors.push({ - error: 'INDEX_NOT_FOUND', + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async index => { + const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { index, + fields: `${timestampField},${partitionField}`, }); - return; - } - ([ - [timestampField, 'date'], - [partitionField, 'keyword'], - ] as const).forEach(([field, fieldType]) => { - const fieldMetadata = fieldCaps.fields[field]; - - if (fieldMetadata === undefined) { + if (fieldCaps.indices.length === 0) { errors.push({ - error: 'FIELD_NOT_FOUND', + error: 'INDEX_NOT_FOUND', index, - field, }); - } else { - const fieldTypes = Object.keys(fieldMetadata); + return; + } + + ([ + [timestampField, 'date'], + [partitionField, 'keyword'], + ] as const).forEach(([field, fieldType]) => { + const fieldMetadata = fieldCaps.fields[field]; - if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + if (fieldMetadata === undefined) { errors.push({ - error: `FIELD_NOT_VALID`, + error: 'FIELD_NOT_FOUND', index, field, }); - } - } - }); - }) - ); + } else { + const fieldTypes = Object.keys(fieldMetadata); - return res.response(validationIndicesResponsePayloadRT.encode({ data: { errors } })); - }, - }); + if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + errors.push({ + error: `FIELD_NOT_VALID`, + index, + field, + }); + } + } + }); + }) + ); + return response.ok({ + body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts index fc06ea48f4353..973080c880e6d 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts @@ -9,6 +9,7 @@ import Boom from 'boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, @@ -19,46 +20,58 @@ import { import { throwErrors } from '../../../../common/runtime_types'; import { NoLogRateResultsIndexError } from '../../../lib/log_analysis'; +const anyObject = schema.object({}, { allowUnknowns: true }); + export const initLogAnalysisGetLogEntryRateRoute = ({ framework, logAnalysis, }: InfraBackendLibs) => { - framework.registerRoute({ - method: 'POST', - path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - handler: async (req, res) => { + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, + validate: { + // short-circuit forced @kbn/config-schema validation so we can do io-ts validation + body: anyObject, + }, + }, + async (requestContext, request, response) => { const payload = pipe( - getLogEntryRateRequestPayloadRT.decode(req.payload), + getLogEntryRateRequestPayloadRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const logEntryRateBuckets = await logAnalysis - .getLogEntryRateBuckets( - req, + try { + const logEntryRateBuckets = await logAnalysis.getLogEntryRateBuckets( + requestContext, payload.data.sourceId, payload.data.timeRange.startTime, payload.data.timeRange.endTime, - payload.data.bucketDuration - ) - .catch(err => { - if (err instanceof NoLogRateResultsIndexError) { - throw Boom.boomify(err, { statusCode: 404 }); - } + payload.data.bucketDuration, + request + ); - throw Boom.boomify(err, { statusCode: ('statusCode' in err && err.statusCode) || 500 }); + return response.ok({ + body: getLogEntryRateSuccessReponsePayloadRT.encode({ + data: { + bucketDuration: payload.data.bucketDuration, + histogramBuckets: logEntryRateBuckets, + totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), + }, + }), }); - - return res.response( - getLogEntryRateSuccessReponsePayloadRT.encode({ - data: { - bucketDuration: payload.data.bucketDuration, - histogramBuckets: logEntryRateBuckets, - totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), - }, - }) - ); - }, - }); + } catch (e) { + const { statusCode = 500, message = 'Unknown error occurred' } = e; + if (e instanceof NoLogRateResultsIndexError) { + return response.notFound({ body: { message } }); + } + return response.customError({ + statusCode, + body: { message }, + }); + } + } + ); }; const getTotalNumberOfLogEntries = ( diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts index 8cdb121aebf1e..a1f6311a103eb 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom, { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; import { get } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { - InfraMetadata, - InfraMetadataWrappedRequest, InfraMetadataFeature, InfraMetadataRequestRT, InfraMetadataRT, @@ -24,23 +23,33 @@ import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata'; import { getNodeInfo } from './lib/get_node_info'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initMetadataRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/metadata', - handler: async req => { + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metadata', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { try { const { nodeId, nodeType, sourceId } = pipe( - InfraMetadataRequestRT.decode(req.payload), + InfraMetadataRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const { configuration } = await libs.sources.getSourceConfiguration(req, sourceId); + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const metricsMetadata = await getMetricMetadata( framework, - req, + requestContext, configuration, nodeId, nodeType @@ -49,35 +58,35 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { nameToFeature('metrics') ); - const info = await getNodeInfo(framework, req, configuration, nodeId, nodeType); + const info = await getNodeInfo(framework, requestContext, configuration, nodeId, nodeType); const cloudInstanceId = get(info, 'cloud.instance.id'); const cloudMetricsMetadata = cloudInstanceId - ? await getCloudMetricsMetadata(framework, req, configuration, cloudInstanceId) + ? await getCloudMetricsMetadata(framework, requestContext, configuration, cloudInstanceId) : { buckets: [] }; const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( nameToFeature('metrics') ); - - const hasAPM = await hasAPMData(framework, req, configuration, nodeId, nodeType); + const hasAPM = await hasAPMData(framework, requestContext, configuration, nodeId, nodeType); const apmMetricFeatures = hasAPM ? [{ name: 'apm.transaction', source: 'apm' }] : []; const id = metricsMetadata.id; const name = metricsMetadata.name || id; - return pipe( - InfraMetadataRT.decode({ + return response.ok({ + body: InfraMetadataRT.encode({ id, name, features: [...metricFeatures, ...cloudMetricsFeatures, ...apmMetricFeatures], info, }), - fold(throwErrors(Boom.badImplementation), identity) - ); + }); } catch (error) { - throw boomify(error); + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; const nameToFeature = (source: string) => (name: string): InfraMetadataFeature => ({ diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts index 58b3beab42886..75ca3ae3caee2 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, InfraMetadataAggregationBucket, InfraMetadataAggregationResponse, } from '../../../lib/adapters/framework'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; @@ -18,8 +18,8 @@ export interface InfraCloudMetricsAdapterResponse { } export const getCloudMetricsMetadata = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, instanceId: string ): Promise => { @@ -51,7 +51,7 @@ export const getCloudMetricsMetadata = async ( { metrics?: InfraMetadataAggregationResponse; } - >(req, 'search', metricQuery); + >(requestContext, 'search', metricQuery); const buckets = response.aggregations && response.aggregations.metrics diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts index 812bc27fffc8a..3bd22062c26a0 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -5,12 +5,12 @@ */ import { get } from 'lodash'; +import { RequestHandlerContext } from 'src/core/server'; import { - InfraFrameworkRequest, InfraMetadataAggregationBucket, - InfraBackendFrameworkAdapter, InfraMetadataAggregationResponse, } from '../../../lib/adapters/framework'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; import { NAME_FIELDS } from '../../../lib/constants'; @@ -22,8 +22,8 @@ export interface InfraMetricsAdapterResponse { } export const getMetricMetadata = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -69,7 +69,7 @@ export const getMetricMetadata = async ( metrics?: InfraMetadataAggregationResponse; nodeName?: InfraMetadataAggregationResponse; } - >(req, 'search', metricQuery); + >(requestContext, 'search', metricQuery); const buckets = response.aggregations && response.aggregations.metrics diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts index 5af25515a42ed..1567b6d1bd1ec 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -5,10 +5,8 @@ */ import { first } from 'lodash'; -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { InfraNodeType } from '../../../graphql/types'; import { InfraMetadataInfo } from '../../../../common/http_api/metadata_api'; @@ -17,8 +15,8 @@ import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; import { getIdFieldName } from './get_id_field_name'; export const getNodeInfo = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -31,7 +29,7 @@ export const getNodeInfo = async ( if (nodeType === InfraNodeType.pod) { const kubernetesNodeName = await getPodNodeName( framework, - req, + requestContext, sourceConfiguration, nodeId, nodeType @@ -39,7 +37,7 @@ export const getNodeInfo = async ( if (kubernetesNodeName) { return getNodeInfo( framework, - req, + requestContext, sourceConfiguration, kubernetesNodeName, InfraNodeType.host @@ -64,7 +62,7 @@ export const getNodeInfo = async ( }, }; const response = await framework.callWithRequest<{ _source: InfraMetadataInfo }, {}>( - req, + requestContext, 'search', params ); diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index 893707a4660ee..47ffc7f83b6bc 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -5,16 +5,14 @@ */ import { first, get } from 'lodash'; -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; export const getPodNodeName = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -40,7 +38,7 @@ export const getPodNodeName = async ( const response = await framework.callWithRequest< { _source: { kubernetes: { node: { name: string } } } }, {} - >(req, 'search', params); + >(requestContext, 'search', params); const firstHit = first(response.hits.hits); if (firstHit) { return get(firstHit, '_source.kubernetes.node.name'); diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts index 3193cf83978b0..ab242804173c0 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts @@ -4,22 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; + +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; export const hasAPMData = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' ) => { - const config = framework.config(req); - const apmIndex = config.get('apm_oss.transactionIndices') || 'apm-*'; + const apmIndices = await framework.plugins.apm.getApmIndices( + requestContext.core.savedObjects.client + ); + const apmIndex = apmIndices['apm_oss.transactionIndices'] || 'apm-*'; + // There is a bug in APM ECS data where host.name is not set. // This will fixed with: https://github.com/elastic/apm-server/issues/2502 const nodeFieldName = @@ -48,6 +50,6 @@ export const hasAPMData = async ( }, }, }; - const response = await framework.callWithRequest<{}, {}>(req, 'search', params); + const response = await framework.callWithRequest<{}, {}>(requestContext, 'search', params); return response.hits.total.value !== 0; }; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts index 6b724f6ac60fd..0c69034c66940 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts @@ -4,42 +4,50 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../lib/infra_types'; import { getGroupings } from './lib/get_groupings'; import { populateSeriesWithTSVBData } from './lib/populate_series_with_tsvb_data'; -import { metricsExplorerSchema } from './schema'; -import { MetricsExplorerResponse, MetricsExplorerWrappedRequest } from './types'; +import { MetricsExplorerRequestBody } from './types'; +// import { metricsExplorerSchema } from './schema'; +// import { MetricsExplorerResponse, MetricsExplorerRequestBody } from './types'; + +// NP_TODO: need to replace all of this with real types or io-ts or something? +const escapeHatch = schema.object({}, { allowUnknowns: true }); export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { const { framework } = libs; const { callWithRequest } = framework; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/metrics_explorer', - options: { + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metrics_explorer', validate: { - payload: metricsExplorerSchema, + body: escapeHatch, }, }, - handler: async req => { + async (requestContext, request, response) => { try { const search = (searchOptions: object) => - callWithRequest<{}, Aggregation>(req, 'search', searchOptions); - const options = req.payload; + callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); + const options = request.body as MetricsExplorerRequestBody; // Need to remove this casting and swap in config-schema demands :( // First we get the groupings from a composite aggregation - const response = await getGroupings(search, options); + const groupings = await getGroupings(search, options); // Then we take the results and fill in the data from TSVB with the // user's custom metrics const seriesWithMetrics = await Promise.all( - response.series.map(populateSeriesWithTSVBData(req, options, framework)) + groupings.series.map( + populateSeriesWithTSVBData(request, options, framework, requestContext) + ) ); - return { ...response, series: seriesWithMetrics }; + return response.ok({ body: { ...groupings, series: seriesWithMetrics } }); } catch (error) { - throw boomify(error); + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts index 6b7f85f7e5952..64b9fba0e7aa2 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts @@ -5,10 +5,10 @@ */ import { InfraMetricModelMetricType } from '../../../lib/adapters/metrics'; -import { MetricsExplorerAggregation, MetricsExplorerRequest } from '../types'; +import { MetricsExplorerAggregation, MetricsExplorerRequestBody } from '../types'; import { InfraMetric } from '../../../graphql/types'; import { TSVBMetricModel } from '../../../../common/inventory_models/types'; -export const createMetricModel = (options: MetricsExplorerRequest): TSVBMetricModel => { +export const createMetricModel = (options: MetricsExplorerRequestBody): TSVBMetricModel => { return { id: InfraMetric.custom, requires: [], diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts index 994de72f8029a..7111d3e7f8ca4 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts @@ -6,7 +6,7 @@ import { isObject, set } from 'lodash'; import { InfraDatabaseSearchResponse } from '../../../lib/adapters/framework'; -import { MetricsExplorerRequest, MetricsExplorerResponse } from '../types'; +import { MetricsExplorerRequestBody, MetricsExplorerResponse } from '../types'; interface GroupingAggregation { groupingsCount: { @@ -27,7 +27,7 @@ const EMPTY_RESPONSE = { export const getGroupings = async ( search: (options: object) => Promise>, - options: MetricsExplorerRequest + options: MetricsExplorerRequestBody ): Promise => { if (!options.groupBy) { return EMPTY_RESPONSE; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 80ccad9567a0f..1a0edb1053730 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -5,25 +5,23 @@ */ import { union } from 'lodash'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, -} from '../../../lib/adapters/framework'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { MetricsExplorerColumnType, - MetricsExplorerRequest, MetricsExplorerRow, MetricsExplorerSeries, - MetricsExplorerWrappedRequest, + MetricsExplorerRequestBody, } from '../types'; import { createMetricModel } from './create_metrics_model'; import { JsonObject } from '../../../../common/typed_json'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; export const populateSeriesWithTSVBData = ( - req: InfraFrameworkRequest, - options: MetricsExplorerRequest, - framework: InfraBackendFrameworkAdapter + request: KibanaRequest, + options: MetricsExplorerRequestBody, + framework: KibanaFramework, + requestContext: RequestHandlerContext ) => async (series: MetricsExplorerSeries) => { // IF there are no metrics selected then we should return an empty result. if (options.metrics.length === 0) { @@ -57,7 +55,7 @@ export const populateSeriesWithTSVBData = ( const model = createMetricModel(options); const calculatedInterval = await calculateMetricInterval( framework, - req, + requestContext, { indexPattern: options.indexPattern, timestampField: options.timerange.field, @@ -78,7 +76,13 @@ export const populateSeriesWithTSVBData = ( } // Get TSVB results using the model, timerange and filters - const tsvbResults = await framework.makeTSVBRequest(req, model, timerange, filters); + const tsvbResults = await framework.makeTSVBRequest( + request, + model, + timerange, + filters, + requestContext + ); // If there is no data `custom` will not exist. if (!tsvbResults.custom) { diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts index b29c41fcbff18..a43e3adbdd184 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraWrappableRequest } from '../../lib/adapters/framework'; - export interface InfraTimerange { field: string; from: number; @@ -27,7 +25,7 @@ export interface MetricsExplorerMetric { field?: string | undefined; } -export interface MetricsExplorerRequest { +export interface MetricsExplorerRequestBody { timerange: InfraTimerange; indexPattern: string; metrics: MetricsExplorerMetric[]; @@ -37,8 +35,6 @@ export interface MetricsExplorerRequest { filterQuery?: string; } -export type MetricsExplorerWrappedRequest = InfraWrappableRequest; - export interface MetricsExplorerPageInfo { total: number; afterKey?: string | null; diff --git a/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts b/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts index a4bc84433a4c1..a9419cd27e684 100644 --- a/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; -import { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; @@ -13,27 +13,34 @@ import { UsageCollector } from '../../usage/usage_collector'; import { InfraMetricsRequestOptions } from '../../lib/adapters/metrics'; import { InfraNodeType, InfraMetric } from '../../graphql/types'; import { - NodeDetailsWrappedRequest, NodeDetailsRequestRT, - NodeDetailsMetricDataResponse, + NodeDetailsMetricDataResponseRT, } from '../../../common/http_api/node_details_api'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/metrics/node_details', - handler: async req => { - const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( - NodeDetailsRequestRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/node_details', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { try { - const source = await libs.sources.getSourceConfiguration(req, sourceId); + const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( + NodeDetailsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); UsageCollector.countNode(nodeType); + const options: InfraMetricsRequestOptions = { nodeIds: { nodeId, @@ -44,13 +51,16 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { metrics: metrics as InfraMetric[], timerange, }; - - return { - metrics: await libs.metrics.getMetrics(req, options), - }; - } catch (e) { - throw boomify(e); + return response.ok({ + body: NodeDetailsMetricDataResponseRT.encode({ + metrics: await libs.metrics.getMetrics(requestContext, options, request), + }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts index 61d2fccf00101..013a261d24831 100644 --- a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; +import { schema } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; @@ -12,37 +13,50 @@ import { InfraSnapshotRequestOptions } from '../../lib/snapshot'; import { UsageCollector } from '../../usage/usage_collector'; import { parseFilterQuery } from '../../utils/serialized_query'; import { InfraNodeType, InfraSnapshotMetricInput } from '../../../public/graphql/types'; -import { - SnapshotRequestRT, - SnapshotWrappedRequest, - SnapshotNodeResponse, -} from '../../../common/http_api/snapshot_api'; +import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initSnapshotRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/metrics/snapshot', - handler: async req => { - const { filterQuery, nodeType, groupBy, sourceId, metric, timerange } = pipe( - SnapshotRequestRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); - const source = await libs.sources.getSourceConfiguration(req, sourceId); - UsageCollector.countNode(nodeType); - const options: InfraSnapshotRequestOptions = { - filterQuery: parseFilterQuery(filterQuery), - // TODO: Use common infra metric and replace graphql type - nodeType: nodeType as InfraNodeType, - groupBy, - sourceConfiguration: source.configuration, - // TODO: Use common infra metric and replace graphql type - metric: metric as InfraSnapshotMetricInput, - timerange, - }; - return { nodes: await libs.snapshot.getNodes(req, options) }; + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/snapshot', + validate: { + body: escapeHatch, + }, }, - }); + async (requestContext, request, response) => { + try { + const { filterQuery, nodeType, groupBy, sourceId, metric, timerange } = pipe( + SnapshotRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + UsageCollector.countNode(nodeType); + const options: InfraSnapshotRequestOptions = { + filterQuery: parseFilterQuery(filterQuery), + // TODO: Use common infra metric and replace graphql type + nodeType: nodeType as InfraNodeType, + groupBy, + sourceConfiguration: source.configuration, + // TODO: Use common infra metric and replace graphql type + metric: metric as InfraSnapshotMetricInput, + timerange, + }; + return response.ok({ + body: SnapshotNodeResponseRT.encode({ + nodes: await libs.snapshot.getNodes(requestContext, options), + }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts index 7696abd2ac250..5eb5d424cdd73 100644 --- a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; interface Options { indexPattern: string; @@ -20,8 +21,8 @@ interface Options { * This is useful for visualizing metric modules like s3 that only send metrics once per day. */ export const calculateMetricInterval = async ( - framework: InfraBackendFrameworkAdapter, - request: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, options: Options, modules: string[] ) => { @@ -64,7 +65,11 @@ export const calculateMetricInterval = async ( }, }; - const resp = await framework.callWithRequest<{}, PeriodAggregationData>(request, 'search', query); + const resp = await framework.callWithRequest<{}, PeriodAggregationData>( + requestContext, + 'search', + query + ); // if ES doesn't return an aggregations key, something went seriously wrong. if (!resp.aggregations) { diff --git a/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts b/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts index a5729b6004dcf..c7ff1b077f685 100644 --- a/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts +++ b/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts @@ -4,25 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; +import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; export const getAllCompositeData = async < Aggregation = undefined, Bucket = {}, Options extends object = {} >( - framework: InfraBackendFrameworkAdapter, - request: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, options: Options, bucketSelector: (response: InfraDatabaseSearchResponse<{}, Aggregation>) => Bucket[], onAfterKey: (options: Options, response: InfraDatabaseSearchResponse<{}, Aggregation>) => Options, previousBuckets: Bucket[] = [] ): Promise => { - const response = await framework.callWithRequest<{}, Aggregation>(request, 'search', options); + const response = await framework.callWithRequest<{}, Aggregation>( + requestContext, + 'search', + options + ); // Nothing available, return the previous buckets. if (response.hits.total.value === 0) { @@ -45,7 +47,7 @@ export const getAllCompositeData = async < const newOptions = onAfterKey(options, response); return getAllCompositeData( framework, - request, + requestContext, newOptions, bucketSelector, onAfterKey, diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index ce05af46ade66..3ffc7bcf275d7 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -106,14 +106,10 @@ describe('Lens App', () => { }, }, }, - }, - dataShim: { indexPatterns: { - indexPatterns: { - get: jest.fn(id => { - return new Promise(resolve => resolve({ id })); - }), - }, + get: jest.fn(id => { + return new Promise(resolve => resolve({ id })); + }), }, }, storage: { @@ -238,7 +234,7 @@ describe('Lens App', () => { await waitForPromises(); expect(args.docStorage.load).toHaveBeenCalledWith('1234'); - expect(args.dataShim.indexPatterns.indexPatterns.get).toHaveBeenCalledWith('1'); + expect(args.data.indexPatterns.get).toHaveBeenCalledWith('1'); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index c29b0df9ee9fa..fda07c9b6bb77 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -11,12 +11,7 @@ import { i18n } from '@kbn/i18n'; import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { AppMountContext, NotificationsStart } from 'src/core/public'; -import { - DataStart, - IndexPattern as IndexPatternInstance, - IndexPatterns as IndexPatternsService, - SavedQuery, -} from 'src/legacy/core_plugins/data/public'; +import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { start as navigation } from '../../../../../../src/legacy/core_plugins/navigation/public/legacy'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; @@ -24,7 +19,11 @@ import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; -import { esFilters } from '../../../../../../src/plugins/data/public'; +import { + esFilters, + IndexPattern as IndexPatternInstance, + IndexPatternsContract, +} from '../../../../../../src/plugins/data/public'; interface State { isLoading: boolean; @@ -46,7 +45,6 @@ interface State { export function App({ editorFrame, data, - dataShim, core, storage, docId, @@ -56,7 +54,6 @@ export function App({ editorFrame: EditorFrameInstance; data: DataPublicPluginStart; core: AppMountContext['core']; - dataShim: DataStart; storage: IStorageWrapper; docId?: string; docStorage: SavedObjectStore; @@ -119,7 +116,7 @@ export function App({ .then(doc => { getAllIndexPatterns( doc.state.datasourceMetaData.filterableIndexPatterns, - dataShim.indexPatterns.indexPatterns, + data.indexPatterns, core.notifications ) .then(indexPatterns => { @@ -286,7 +283,7 @@ export function App({ ) { getAllIndexPatterns( filterableIndexPatterns, - dataShim.indexPatterns.indexPatterns, + data.indexPatterns, core.notifications ).then(indexPatterns => { if (indexPatterns) { @@ -349,7 +346,7 @@ export function App({ export async function getAllIndexPatterns( ids: Array<{ id: string }>, - indexPatternsService: IndexPatternsService, + indexPatternsService: IndexPatternsContract, notifications: NotificationsStart ): Promise { try { diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 6493af28f89ea..b1eac8e287bd8 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -82,7 +82,7 @@ export class AppPlugin { if (this.startDependencies === null) { throw new Error('mounted before start phase'); } - const { data, dataShim, savedObjectsClient, editorFrame } = this.startDependencies; + const { data, savedObjectsClient, editorFrame } = this.startDependencies; addHelpMenuToAppChrome(context.core.chrome); const instance = editorFrame.createInstance({}); @@ -100,7 +100,6 @@ export class AppPlugin { editorFrame.setup(npSetup.core, { - data: dataSetup, + data: npSetup.plugins.data, embeddable: npSetup.plugins.embeddable, expressions: npSetup.plugins.expressions, }); export const editorFrameStart = () => editorFrame.start(npStart.core, { - data: dataStart, + data: npStart.plugins.data, embeddable: npStart.plugins.embeddable, expressions: npStart.plugins.expressions, chrome, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss index a496cccc42a58..ed39beeb7d088 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss @@ -1,5 +1,3 @@ -@import '@elastic/eui/src/components/form/form_control_layout/mixins'; - .lnsInnerIndexPatternDataPanel { width: 100%; height: 100%; @@ -54,4 +52,3 @@ @include euiFormControlLayoutPadding(1, 'right'); @include euiFormControlLayoutPadding(1, 'left'); } - diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap similarity index 77% rename from x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap rename to x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9d5e91c2d5f16..5d6f10b1bda3e 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -130,6 +130,7 @@ exports[`UploadLicense should display a modal when license requires acknowledgem ] } needsAcknowledgement={true} + setBreadcrumb={[Function]} uploadLicense={[Function]} uploadLicenseStatus={[Function]} > @@ -844,99 +845,115 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
- +
- - } - onChange={[Function]} + - -
+
- - - -
- -
+ + } + onChange={[Function]} + > + +
- Select or drag your license file - -
-
+
+ + + +
+ +
+ + Select or drag your license file + +
+
+
+
+ +
-
-
-
+ +
+
- + @@ -1164,6 +1181,7 @@ exports[`UploadLicense should display an error when ES says license is expired 1 errorMessage="The supplied license has expired." isInvalid={false} needsAcknowledgement={false} + setBreadcrumb={[Function]} uploadLicense={[Function]} uploadLicenseStatus={[Function]} > @@ -1291,103 +1309,119 @@ exports[`UploadLicense should display an error when ES says license is expired 1
- +
- - } - onChange={[Function]} + - -
+
- - - -
- -
+ + } + onChange={[Function]} + > + +
- Select or drag your license file - -
-
+
+ + + +
+ +
+ + Select or drag your license file + +
+
+
+
+ +
-
-
-
+ +
+
- + @@ -1615,6 +1649,7 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 errorMessage="The supplied license is not valid for this product." isInvalid={false} needsAcknowledgement={false} + setBreadcrumb={[Function]} uploadLicense={[Function]} uploadLicenseStatus={[Function]} > @@ -1742,103 +1777,119 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
- +
- - } - onChange={[Function]} + - -
+
- - - -
- -
+ + } + onChange={[Function]} + > + +
- Select or drag your license file - -
-
+
+ + + +
+ +
+ + Select or drag your license file + +
+
+
+
+ +
-
-
-
+ +
+
- + @@ -2066,6 +2117,7 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] errorMessage="Error encountered uploading license: Check your license file." isInvalid={false} needsAcknowledgement={false} + setBreadcrumb={[Function]} uploadLicense={[Function]} uploadLicenseStatus={[Function]} > @@ -2193,99 +2245,115 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
- +
- - } - onChange={[Function]} + - -
+
- - - -
- -
+ + } + onChange={[Function]} + > + +
- Select or drag your license file - -
-
+
+ + + +
+ +
+ + Select or drag your license file + +
+
+
+
+ +
-
-
-
+ +
+
- + @@ -2513,6 +2581,7 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` errorMessage="Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled" isInvalid={false} needsAcknowledgement={false} + setBreadcrumb={[Function]} uploadLicense={[Function]} uploadLicenseStatus={[Function]} > @@ -2640,103 +2709,119 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
- +
- - } - onChange={[Function]} + - -
+
- - - -
- -
+ + } + onChange={[Function]} + > + +
- Select or drag your license file - -
-
+
+ + + +
+ +
+ + Select or drag your license file + +
+
+
+
+ +
-
-
-
+ +
+
- + diff --git a/x-pack/legacy/plugins/license_management/__jest__/add_license.test.js b/x-pack/legacy/plugins/license_management/__jest__/add_license.test.js index a0f4ebdd39b0e..1d1b98fb18807 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/add_license.test.js +++ b/x-pack/legacy/plugins/license_management/__jest__/add_license.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AddLicense } from '../public/sections/license_dashboard/add_license'; +import { AddLicense } from '../public/np_ready/application/sections/license_dashboard/add_license'; import { createMockLicense, getComponent } from './util'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/license_management/__jest__/license_status.test.js b/x-pack/legacy/plugins/license_management/__jest__/license_status.test.js index 9a6aaf95afa6b..ff9b2e1c786b5 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/license_status.test.js +++ b/x-pack/legacy/plugins/license_management/__jest__/license_status.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LicenseStatus } from '../public/sections/license_dashboard/license_status'; +import { LicenseStatus } from '../public/np_ready/application/sections/license_dashboard/license_status'; import { createMockLicense, getComponent } from './util'; describe('LicenseStatus component', () => { diff --git a/x-pack/legacy/plugins/license_management/__jest__/request_trial_extension.test.js b/x-pack/legacy/plugins/license_management/__jest__/request_trial_extension.test.js index e990bd48317d1..226b3bac007b0 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/request_trial_extension.test.js +++ b/x-pack/legacy/plugins/license_management/__jest__/request_trial_extension.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestTrialExtension } from '../public/sections/license_dashboard/request_trial_extension'; +import { RequestTrialExtension } from '../public/np_ready/application/sections/license_dashboard/request_trial_extension'; import { createMockLicense, getComponent } from './util'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/license_management/__jest__/revert_to_basic.test.js b/x-pack/legacy/plugins/license_management/__jest__/revert_to_basic.test.js index 76057ff95d41f..3b9ca4487dfb3 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/revert_to_basic.test.js +++ b/x-pack/legacy/plugins/license_management/__jest__/revert_to_basic.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RevertToBasic } from '../public/sections/license_dashboard/revert_to_basic'; +import { RevertToBasic } from '../public/np_ready/application/sections/license_dashboard/revert_to_basic'; import { createMockLicense, getComponent } from './util'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/license_management/__jest__/start_trial.test.js b/x-pack/legacy/plugins/license_management/__jest__/start_trial.test.js index 928c7df70d0b7..903be98f225f2 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/start_trial.test.js +++ b/x-pack/legacy/plugins/license_management/__jest__/start_trial.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StartTrial } from '../public/sections/license_dashboard/start_trial'; +import { StartTrial } from '../public/np_ready/application/sections/license_dashboard/start_trial'; import { createMockLicense, getComponent } from './util'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js b/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js index a92ca384e8a37..70edc67b449bb 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js +++ b/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { setTelemetryEnabled, setTelemetryOptInService } from '../public/lib/telemetry'; -import { TelemetryOptIn } from '../public/components/telemetry_opt_in'; +import { setTelemetryEnabled, setTelemetryOptInService } from '../public/np_ready/application/lib/telemetry'; +import { TelemetryOptIn } from '../public/np_ready/application/components/telemetry_opt_in'; import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; jest.mock('ui/capabilities', () => ({ diff --git a/x-pack/legacy/plugins/license_management/__jest__/upload_license.test.js b/x-pack/legacy/plugins/license_management/__jest__/upload_license.test.tsx similarity index 62% rename from x-pack/legacy/plugins/license_management/__jest__/upload_license.test.js rename to x-pack/legacy/plugins/license_management/__jest__/upload_license.test.tsx index 04da56b7ee82b..b852daf78ca15 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/upload_license.test.js +++ b/x-pack/legacy/plugins/license_management/__jest__/upload_license.test.tsx @@ -4,43 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ +import { httpServiceMock, chromeServiceMock } from '../../../../../src/core/public/mocks'; import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; import React from 'react'; import { Provider } from 'react-redux'; -import { uploadLicense } from '../public/store/actions/upload_license'; -import { licenseManagementStore } from '../public/store/store'; -import { UploadLicense } from '../public/sections/upload_license'; -import { BASE_PATH } from '../common/constants'; + +// @ts-ignore +import { uploadLicense } from '../public/np_ready/application/store/actions/upload_license'; + +// @ts-ignore +import { licenseManagementStore } from '../public/np_ready/application/store/store'; + +// @ts-ignore +import { UploadLicense } from '../public/np_ready/application/sections/upload_license'; + import { UPLOAD_LICENSE_EXPIRED, UPLOAD_LICENSE_REQUIRES_ACK, UPLOAD_LICENSE_SUCCESS, UPLOAD_LICENSE_TLS_NOT_ENABLED, UPLOAD_LICENSE_INVALID, + // @ts-ignore } from './api_responses'; -import sinon from 'sinon'; window.location.reload = () => {}; -let server = null; -let store = null; -let component = null; + +let store: any = null; +let component: any = null; const services = { - kbnUrl: { - change: jest.fn() + legacy: { + xPackInfo: { + refresh: jest.fn(), + get: () => { + return { license: { type: 'basic' } }; + }, + }, + refreshXpack: jest.fn(), + }, + http: httpServiceMock.createSetupContract(), + chrome: chromeServiceMock.createStartContract(), + history: { + replace: jest.fn(), }, - autoLogout: () => {}, - xPackInfo: { - refresh: jest.fn(), - get: () => { - return { license: { type: 'basic' } }; - } - } }; describe('UploadLicense', () => { beforeEach(() => { - server = sinon.fakeServer.create(); - server.respondImmediately = true; store = licenseManagementStore({}, services); component = ( @@ -48,57 +57,60 @@ describe('UploadLicense', () => { ); }); + afterEach(() => { - server.restore(); - services.xPackInfo.refresh.mockReset(); - services.kbnUrl.change.mockReset(); + services.legacy.xPackInfo.refresh.mockReset(); + services.history.replace.mockReset(); + jest.clearAllMocks(); }); + it('should display an error when submitting invalid JSON', async () => { const rendered = mountWithIntl(component); store.dispatch(uploadLicense('INVALID', 'trial')); rendered.update(); expect(rendered).toMatchSnapshot(); }); + it('should display an error when ES says license is invalid', async () => { + services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_INVALID[2])); const rendered = mountWithIntl(component); const invalidLicense = JSON.stringify({ license: { type: 'basic' } }); - server.respond(UPLOAD_LICENSE_INVALID); await uploadLicense(invalidLicense)(store.dispatch, null, services); rendered.update(); expect(rendered).toMatchSnapshot(); }); + it('should display an error when ES says license is expired', async () => { + services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_EXPIRED[2])); const rendered = mountWithIntl(component); const invalidLicense = JSON.stringify({ license: { type: 'basic' } }); - server.respond(UPLOAD_LICENSE_EXPIRED); await uploadLicense(invalidLicense)(store.dispatch, null, services); rendered.update(); expect(rendered).toMatchSnapshot(); }); + it('should display a modal when license requires acknowledgement', async () => { + services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_REQUIRES_ACK[2])); const unacknowledgedLicense = JSON.stringify({ - license: { type: 'basic' } + license: { type: 'basic' }, }); - server.respond(UPLOAD_LICENSE_REQUIRES_ACK); - await uploadLicense(unacknowledgedLicense, 'trial')( - store.dispatch, - null, - services - ); + await uploadLicense(unacknowledgedLicense, 'trial')(store.dispatch, null, services); const rendered = mountWithIntl(component); expect(rendered).toMatchSnapshot(); }); + it('should refresh xpack info and navigate to BASE_PATH when ES accepts new license', async () => { + services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_SUCCESS[2])); const validLicense = JSON.stringify({ license: { type: 'basic' } }); - server.respond(UPLOAD_LICENSE_SUCCESS); await uploadLicense(validLicense)(store.dispatch, null, services); - expect(services.xPackInfo.refresh).toHaveBeenCalled(); - expect(services.kbnUrl.change).toHaveBeenCalledWith(BASE_PATH); + expect(services.legacy.refreshXpack).toHaveBeenCalled(); + expect(services.history.replace).toHaveBeenCalled(); }); + it('should display error when ES returns error', async () => { + services.http.put.mockResolvedValue(JSON.parse(UPLOAD_LICENSE_TLS_NOT_ENABLED[2])); const rendered = mountWithIntl(component); const license = JSON.stringify({ license: { type: 'basic' } }); - server.respond(UPLOAD_LICENSE_TLS_NOT_ENABLED); await uploadLicense(license)(store.dispatch, null, services); rendered.update(); expect(rendered).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/license_management/__jest__/util/util.js b/x-pack/legacy/plugins/license_management/__jest__/util/util.js index 537c8d2856079..57d4bf8b3ebd6 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/util/util.js +++ b/x-pack/legacy/plugins/license_management/__jest__/util/util.js @@ -5,9 +5,10 @@ */ import { Provider } from 'react-redux'; -import { licenseManagementStore } from '../../public/store/store'; +import { licenseManagementStore } from '../../public/np_ready/application/store/store'; import React from 'react'; import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; const highExpirationMillis = new Date('October 13, 2099 00:00:00Z').getTime(); @@ -22,7 +23,10 @@ export const createMockLicense = ( }; }; export const getComponent = (initialState, Component) => { - const store = licenseManagementStore(initialState); + const services = { + http: httpServiceMock.createSetupContract() + }; + const store = licenseManagementStore(initialState, services); return mountWithIntl( diff --git a/x-pack/legacy/plugins/license_management/common/constants/base_path.js b/x-pack/legacy/plugins/license_management/common/constants/base_path.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/common/constants/base_path.js rename to x-pack/legacy/plugins/license_management/common/constants/base_path.ts diff --git a/x-pack/legacy/plugins/license_management/common/constants/external_links.js b/x-pack/legacy/plugins/license_management/common/constants/external_links.ts similarity index 88% rename from x-pack/legacy/plugins/license_management/common/constants/external_links.js rename to x-pack/legacy/plugins/license_management/common/constants/external_links.ts index 66442aaee8d4a..1ba88b2718ae6 100644 --- a/x-pack/legacy/plugins/license_management/common/constants/external_links.js +++ b/x-pack/legacy/plugins/license_management/common/constants/external_links.ts @@ -9,5 +9,5 @@ const ELASTIC_BASE_URL = 'https://www.elastic.co/'; export const EXTERNAL_LINKS = { SUBSCRIPTIONS: `${ELASTIC_BASE_URL}subscriptions`, TRIAL_EXTENSION: `${ELASTIC_BASE_URL}trialextension`, - TRIAL_LICENSE: `${ELASTIC_BASE_URL}legal/trial_license` + TRIAL_LICENSE: `${ELASTIC_BASE_URL}legal/trial_license`, }; diff --git a/x-pack/legacy/plugins/license_management/common/constants/index.js b/x-pack/legacy/plugins/license_management/common/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/common/constants/index.js rename to x-pack/legacy/plugins/license_management/common/constants/index.ts diff --git a/x-pack/legacy/plugins/license_management/common/constants/permissions.js b/x-pack/legacy/plugins/license_management/common/constants/permissions.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/common/constants/permissions.js rename to x-pack/legacy/plugins/license_management/common/constants/permissions.ts diff --git a/x-pack/legacy/plugins/license_management/common/constants/plugin.ts b/x-pack/legacy/plugins/license_management/common/constants/plugin.ts new file mode 100644 index 0000000000000..14b591e3834ef --- /dev/null +++ b/x-pack/legacy/plugins/license_management/common/constants/plugin.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const PLUGIN = { + TITLE: i18n.translate('xpack.licenseMgmt.managementSectionDisplayName', { + defaultMessage: 'License Management', + }), + ID: 'license_management', +}; diff --git a/x-pack/legacy/plugins/license_management/index.js b/x-pack/legacy/plugins/license_management/index.js deleted file mode 100644 index 1c69b5d96aad6..0000000000000 --- a/x-pack/legacy/plugins/license_management/index.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. - */ - -import { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { - registerLicenseRoute, - registerStartTrialRoutes, - registerStartBasicRoute, - registerPermissionsRoute -} from './server/routes/api/license/'; -import { createRouter } from '../../server/lib/create_router'; - -export function licenseManagement(kibana) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.license_management', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: [ - 'plugins/license_management', - ] - }, - init: (server) => { - const xpackInfo = server.plugins.xpack_main.info; - const router = createRouter(server, PLUGIN.ID, '/api/license'); - registerLicenseRoute(router, xpackInfo); - registerStartTrialRoutes(router, xpackInfo); - registerStartBasicRoute(router, xpackInfo); - registerPermissionsRoute(router, xpackInfo); - } - }); -} diff --git a/x-pack/legacy/plugins/license_management/index.ts b/x-pack/legacy/plugins/license_management/index.ts new file mode 100644 index 0000000000000..c621a96945c41 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/index.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 { resolve } from 'path'; +import { PLUGIN } from './common/constants'; +import { Legacy } from '../../../../kibana'; +import { plugin } from './server/np_ready'; + +export function licenseManagement(kibana: any) { + return new kibana.Plugin({ + id: PLUGIN.ID, + configPrefix: 'xpack.license_management', + publicDir: resolve(__dirname, 'public'), + require: ['kibana', 'elasticsearch'], + uiExports: { + styleSheetPaths: resolve(__dirname, 'public/np_ready/application/index.scss'), + managementSections: ['plugins/license_management/legacy'], + }, + init: (server: Legacy.Server) => { + plugin({} as any).setup(server.newPlatform.setup.core, { + ...server.newPlatform.setup.plugins, + __LEGACY: { + xpackMain: server.plugins.xpack_main, + elasticsearch: server.plugins.elasticsearch, + }, + }); + }, + }); +} diff --git a/x-pack/legacy/plugins/license_management/public/index.js b/x-pack/legacy/plugins/license_management/public/legacy.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/public/index.js rename to x-pack/legacy/plugins/license_management/public/legacy.ts diff --git a/x-pack/legacy/plugins/license_management/public/lib/es.js b/x-pack/legacy/plugins/license_management/public/lib/es.js deleted file mode 100644 index 24dbf659ae657..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/lib/es.js +++ /dev/null @@ -1,71 +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 chrome from 'ui/chrome'; - -import $ from 'jquery'; - -export function putLicense(license, acknowledge) { - const options = { - url: `${chrome.addBasePath('/api/license')}${acknowledge ? '?acknowledge=true' : ''}`, - data: license, - contentType: 'application/json', - cache: false, - crossDomain: true, - type: 'PUT', - }; - - return $.ajax(options); -} - -export function startBasic(acknowledge) { - const options = { - url: `${chrome.addBasePath('/api/license/start_basic')}${acknowledge ? '?acknowledge=true' : ''}`, - contentType: 'application/json', - cache: false, - crossDomain: true, - type: 'POST', - }; - - return $.ajax(options); -} - -export function startTrial() { - const options = { - url: chrome.addBasePath('/api/license/start_trial'), - contentType: 'application/json', - cache: false, - crossDomain: true, - type: 'POST', - }; - - return $.ajax(options); -} - -export function canStartTrial() { - const options = { - url: chrome.addBasePath('/api/license/start_trial'), - contentType: 'application/json', - cache: false, - crossDomain: true, - type: 'GET', - }; - - return $.ajax(options); -} - -export function getPermissions() { - const options = { - url: chrome.addBasePath('/api/license/permissions'), - contentType: 'application/json', - cache: false, - crossDomain: true, - type: 'POST', - }; - - return $.ajax(options); -} - diff --git a/x-pack/legacy/plugins/license_management/public/main.html b/x-pack/legacy/plugins/license_management/public/main.html deleted file mode 100644 index 310eda8be763a..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/main.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/legacy/plugins/license_management/public/management_section.js b/x-pack/legacy/plugins/license_management/public/management_section.ts similarity index 62% rename from x-pack/legacy/plugins/license_management/public/management_section.js rename to x-pack/legacy/plugins/license_management/public/management_section.ts index 8f5cc8a2226dc..4f69581fff0df 100644 --- a/x-pack/legacy/plugins/license_management/public/management_section.js +++ b/x-pack/legacy/plugins/license_management/public/management_section.ts @@ -5,15 +5,11 @@ */ import { management } from 'ui/management'; -import { BASE_PATH } from '../common/constants'; -import { i18n } from '@kbn/i18n'; +import { BASE_PATH, PLUGIN } from '../common/constants'; management.getSection('elasticsearch').register('license_management', { visible: true, - display: i18n.translate('xpack.licenseMgmt.managementSectionDisplayName', { - defaultMessage: 'License Management', - }), + display: PLUGIN.TITLE, order: 99, - url: `#${BASE_PATH}home` + url: `#${BASE_PATH}home`, }); - diff --git a/x-pack/legacy/plugins/license_management/public/_license_management.scss b/x-pack/legacy/plugins/license_management/public/np_ready/application/_license_management.scss similarity index 100% rename from x-pack/legacy/plugins/license_management/public/_license_management.scss rename to x-pack/legacy/plugins/license_management/public/np_ready/application/_license_management.scss diff --git a/x-pack/legacy/plugins/license_management/public/app.container.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.container.js similarity index 94% rename from x-pack/legacy/plugins/license_management/public/app.container.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/app.container.js index eac12710aeedc..ab81f7ff14edd 100644 --- a/x-pack/legacy/plugins/license_management/public/app.container.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.container.js @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { App as PresentationComponent } from './app'; -import { getPermission, isPermissionsLoading, getPermissionsError } from './store/reducers/licenseManagement'; +import { getPermission, isPermissionsLoading, getPermissionsError } from './store/reducers/license_management'; import { loadPermissions } from './store/actions/permissions'; const mapStateToProps = state => { diff --git a/x-pack/legacy/plugins/license_management/public/app.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js similarity index 90% rename from x-pack/legacy/plugins/license_management/public/app.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/app.js index ff3a2d6196d76..70e35e0bd8298 100644 --- a/x-pack/legacy/plugins/license_management/public/app.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js @@ -6,9 +6,9 @@ import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { LicenseDashboard, UploadLicense } from './sections/'; +import { LicenseDashboard, UploadLicense } from './sections'; import { Switch, Route } from 'react-router-dom'; -import { BASE_PATH, APP_PERMISSION } from '../common/constants'; +import { APP_PERMISSION } from '../../../common/constants'; import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui'; export class App extends Component { @@ -84,8 +84,10 @@ export class App extends Component { return ( - - + + + {/* Match all */} + ); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx new file mode 100644 index 0000000000000..d0bf5deacfac1 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Provider } from 'react-redux'; +import { HashRouter } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; +import * as history from 'history'; +import { DocLinksStart, HttpSetup, ToastsSetup, ChromeStart } from 'src/core/public'; + +// @ts-ignore +import { App } from './app.container'; +// @ts-ignore +import { licenseManagementStore } from './store'; + +import { setDocLinks } from './lib/docs_links'; +import { BASE_PATH } from '../../../common/constants'; +import { Breadcrumb } from './breadcrumbs'; + +interface AppDependencies { + element: HTMLElement; + chrome: ChromeStart; + + I18nContext: any; + legacy: { + xpackInfo: any; + refreshXpack: () => void; + MANAGEMENT_BREADCRUMB: Breadcrumb; + }; + + toasts: ToastsSetup; + docLinks: DocLinksStart; + http: HttpSetup; +} + +export const boot = (deps: AppDependencies) => { + const { I18nContext, element, legacy, toasts, docLinks, http, chrome } = deps; + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; + const securityDocumentationLink = `${esBase}/security-settings.html`; + + const initialState = { license: legacy.xpackInfo.get('license') }; + + setDocLinks({ securityDocumentationLink }); + + const services = { + legacy: { + refreshXpack: legacy.refreshXpack, + xPackInfo: legacy.xpackInfo, + }, + // So we can imperatively control the hash route + history: history.createHashHistory({ basename: BASE_PATH }), + toasts, + http, + chrome, + MANAGEMENT_BREADCRUMB: legacy.MANAGEMENT_BREADCRUMB, + }; + + const store = licenseManagementStore(initialState, services); + render( + + + + + + + , + element + ); + + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/license_management/public/breadcrumbs.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts similarity index 52% rename from x-pack/legacy/plugins/license_management/public/breadcrumbs.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts index 0de04f4bd70fc..2da04b22c0386 100644 --- a/x-pack/legacy/plugins/license_management/public/breadcrumbs.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/breadcrumbs.ts @@ -5,28 +5,33 @@ */ import { i18n } from '@kbn/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { BASE_PATH } from '../common/constants'; -export function getDashboardBreadcrumbs() { +import { BASE_PATH } from '../../../common/constants'; + +export interface Breadcrumb { + text: string; + href: string; +} + +export function getDashboardBreadcrumbs(root: Breadcrumb) { return [ - MANAGEMENT_BREADCRUMB, + root, { text: i18n.translate('xpack.licenseMgmt.dashboard.breadcrumb', { - defaultMessage: 'License management' + defaultMessage: 'License management', }), - href: `#${BASE_PATH}home` - } + href: `#${BASE_PATH}home`, + }, ]; } -export function getUploadBreadcrumbs() { +export function getUploadBreadcrumbs(root: Breadcrumb) { return [ - ...getDashboardBreadcrumbs(), + ...getDashboardBreadcrumbs(root), { text: i18n.translate('xpack.licenseMgmt.upload.breadcrumb', { - defaultMessage: 'Upload' - }) - } + defaultMessage: 'Upload', + }), + }, ]; } diff --git a/x-pack/legacy/plugins/license_management/public/components/telemetry_opt_in/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/components/telemetry_opt_in/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js diff --git a/x-pack/legacy/plugins/license_management/public/components/telemetry_opt_in/telemetry_opt_in.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/components/telemetry_opt_in/telemetry_opt_in.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js diff --git a/x-pack/legacy/plugins/license_management/public/index.scss b/x-pack/legacy/plugins/license_management/public/np_ready/application/index.scss similarity index 90% rename from x-pack/legacy/plugins/license_management/public/index.scss rename to x-pack/legacy/plugins/license_management/public/np_ready/application/index.scss index 1887a9b48f365..4fb8aafcca93c 100644 --- a/x-pack/legacy/plugins/license_management/public/index.scss +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/index.scss @@ -10,4 +10,4 @@ // licChart__legend--small // licChart__legend-isLoading -@import 'license_management'; \ No newline at end of file +@import 'license_management'; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts similarity index 88% rename from x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts rename to x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts index 4e09b5d0e9e2d..1f963d7f8fcce 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './adapter_types'; +export * from './boot'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts new file mode 100644 index 0000000000000..761fcd2674df6 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/docs_links.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +let docLinks: Record = {}; + +export const setDocLinks = (links: Record) => { + docLinks = links; +}; + +export const getDocLinks = () => docLinks; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/es.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/es.ts new file mode 100644 index 0000000000000..3924de2202d51 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/es.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpSetup } from 'src/core/public'; + +const BASE_PATH = '/api/license'; + +export function putLicense(http: HttpSetup, license: string, acknowledge: boolean) { + return http.put(BASE_PATH, { + query: { + acknowledge: acknowledge ? 'true' : '', + }, + body: license, + headers: { + contentType: 'application/json', + }, + cache: 'no-cache', + }); +} + +export function startBasic(http: HttpSetup, acknowledge: boolean) { + return http.post(`${BASE_PATH}/start_basic`, { + query: { + acknowledge: acknowledge ? 'true' : '', + }, + headers: { + contentType: 'application/json', + }, + body: null, + cache: 'no-cache', + }); +} + +export function startTrial(http: HttpSetup) { + return http.post(`${BASE_PATH}/start_trial`, { + headers: { + contentType: 'application/json', + }, + cache: 'no-cache', + }); +} + +export function canStartTrial(http: HttpSetup) { + return http.get(`${BASE_PATH}/start_trial`, { + headers: { + contentType: 'application/json', + }, + cache: 'no-cache', + }); +} + +export function getPermissions(http: HttpSetup) { + return http.post(`${BASE_PATH}/permissions`, { + headers: { + contentType: 'application/json', + }, + cache: 'no-cache', + }); +} diff --git a/x-pack/legacy/plugins/license_management/public/lib/telemetry.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js similarity index 68% rename from x-pack/legacy/plugins/license_management/public/lib/telemetry.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js index 61d0322227d8e..220807c2a0ac4 100644 --- a/x-pack/legacy/plugins/license_management/public/lib/telemetry.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fetchTelemetry } from '../../../../../../src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry'; -export { PRIVACY_STATEMENT_URL } from '../../../../../../src/legacy/core_plugins/telemetry/common/constants'; -export { TelemetryOptInProvider } from '../../../../../../src/legacy/core_plugins/telemetry/public/services'; -export { OptInExampleFlyout } from '../../../../../../src/legacy/core_plugins/telemetry/public/components'; +import { fetchTelemetry } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry'; +export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/legacy/core_plugins/telemetry/common/constants'; +export { TelemetryOptInProvider } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/services'; +export { OptInExampleFlyout } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/components'; let telemetryEnabled; let httpClient; diff --git a/x-pack/legacy/plugins/license_management/public/sections/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/sections/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/index.js diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/add_license/add_license.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/add_license.js similarity index 94% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/add_license/add_license.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/add_license.js index 3c0402f974f06..bd876ff89b1ad 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/add_license/add_license.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/add_license.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { BASE_PATH } from '../../../../common/constants'; +import { BASE_PATH } from '../../../../../../common/constants'; import { EuiCard, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/add_license/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/add_license/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/add_license/index.js diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/index.js new file mode 100644 index 0000000000000..b8db24e65e3bd --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/index.js @@ -0,0 +1,7 @@ +/* + * 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 { LicenseDashboard } from './license_dashboard.container'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.container.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.container.js new file mode 100644 index 0000000000000..4073c5cd52fbb --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.container.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { connect } from 'react-redux'; +import { LicenseDashboard as PresentationComponent } from './license_dashboard'; +import { setBreadcrumb } from '../../store/actions/set_breadcrumb'; + +const mapDispatchToProps = { + setBreadcrumb, +}; + +export const LicenseDashboard = connect(null, mapDispatchToProps)(PresentationComponent); diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_dashboard.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js similarity index 88% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_dashboard.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js index 4b74211fe8390..e24c167b3b5ad 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_dashboard.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js @@ -18,7 +18,8 @@ import { EuiSpacer } from '@elastic/eui'; -export const LicenseDashboard = () => { +export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} }) => { + setBreadcrumb('dashboard'); return (
diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_status/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_status/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/index.js diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_status/license_status.container.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.container.js similarity index 95% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_status/license_status.container.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.container.js index a282fdc10ddfb..07847a027452f 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_status/license_status.container.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.container.js @@ -6,7 +6,7 @@ import { LicenseStatus as PresentationComponent } from './license_status'; import { connect } from 'react-redux'; -import { getLicense, getExpirationDateFormatted, isExpired } from '../../../store/reducers/licenseManagement'; +import { getLicense, getExpirationDateFormatted, isExpired } from '../../../store/reducers/license_management'; import { i18n } from '@kbn/i18n'; const mapStateToProps = (state) => { diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_status/license_status.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/license_status/license_status.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status.js diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/request_trial_extension/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/request_trial_extension/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/index.js diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js similarity index 92% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js index 96348d4fcb146..e01bcdee2fae6 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.container.js @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { RequestTrialExtension as PresentationComponent } from './request_trial_extension'; import { shouldShowRequestTrialExtension -} from '../../../store/reducers/licenseManagement'; +} from '../../../store/reducers/license_management'; const mapStateToProps = state => { diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js similarity index 96% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/request_trial_extension/request_trial_extension.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js index 372dd5ba5afea..106b1a0d24aec 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/request_trial_extension/request_trial_extension.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexItem, EuiCard, EuiLink, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EXTERNAL_LINKS } from '../../../../common/constants'; +import { EXTERNAL_LINKS } from '../../../../../../common/constants'; export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => { if (!shouldShowRequestTrialExtension) { diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/revert_to_basic/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/revert_to_basic/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/index.js diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js similarity index 95% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js index bd7f7c28be26f..2bb67835c9c31 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.container.js @@ -12,7 +12,7 @@ import { getLicenseType, shouldShowRevertToBasicLicense, getStartBasicMessages -} from '../../../store/reducers/licenseManagement'; +} from '../../../store/reducers/license_management'; import { startBasicLicense, cancelStartBasicLicense } from '../../../store/actions/start_basic'; const mapStateToProps = state => { diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/revert_to_basic/revert_to_basic.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js similarity index 98% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/revert_to_basic/revert_to_basic.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js index 32ab337359a9b..0e3cea9982a9b 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/revert_to_basic/revert_to_basic.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js @@ -16,7 +16,7 @@ import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EXTERNAL_LINKS } from '../../../../common/constants'; +import { EXTERNAL_LINKS } from '../../../../../../common/constants'; export class RevertToBasic extends React.PureComponent { cancel = () => { diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.container.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.container.js similarity index 97% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.container.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.container.js index d8b71688fd537..41a60e38c0932 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.container.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.container.js @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { StartTrial as PresentationComponent } from './start_trial'; import { loadTrialStatus, startLicenseTrial } from '../../../store/actions/start_trial'; -import { shouldShowStartTrial } from '../../../store/reducers/licenseManagement'; +import { shouldShowStartTrial } from '../../../store/reducers/license_management'; const mapStateToProps = (state) => { return { diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js similarity index 96% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js index 20b130d80a211..4cbd64bdea31f 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js @@ -22,13 +22,11 @@ import { EuiModalHeaderTitle } from '@elastic/eui'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; import { TelemetryOptIn } from '../../../components/telemetry_opt_in'; import { optInToTelemetry } from '../../../lib/telemetry'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EXTERNAL_LINKS } from '../../../../common/constants'; -const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; -const securityDocumentationLink = `${esBase}/security-settings.html`; +import { EXTERNAL_LINKS } from '../../../../../../common/constants'; +import { getDocLinks } from '../../../lib/docs_links'; export class StartTrial extends React.PureComponent { @@ -134,7 +132,7 @@ export class StartTrial extends React.PureComponent { authenticationTypeList: 'AD/LDAP, SAML, PKI, SAML/SSO', securityDocumentationLinkText: ( { @@ -148,16 +149,20 @@ export class UploadLicense extends React.PureComponent { - - } - onChange={this.handleFile} - /> - + + + + } + onChange={this.handleFile} + /> + + + { diff --git a/x-pack/legacy/plugins/license_management/public/store/actions/add_error_message.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/add_error_message.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/actions/add_error_message.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/add_error_message.js diff --git a/x-pack/legacy/plugins/license_management/public/store/actions/add_license.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/add_license.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/actions/add_license.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/add_license.js diff --git a/x-pack/legacy/plugins/license_management/public/store/actions/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/actions/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/index.js diff --git a/x-pack/legacy/plugins/license_management/public/store/actions/permissions.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/permissions.js similarity index 86% rename from x-pack/legacy/plugins/license_management/public/store/actions/permissions.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/permissions.js index b9316c0a958c4..1cb55bf334a22 100644 --- a/x-pack/legacy/plugins/license_management/public/store/actions/permissions.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/permissions.js @@ -19,10 +19,10 @@ export const permissionsError = createAction( 'LICENSE_MANAGEMENT_PERMISSIONS_ERROR' ); -export const loadPermissions = () => async dispatch => { +export const loadPermissions = () => async (dispatch, getState, { http }) => { dispatch(permissionsLoading(true)); try { - const permissions = await getPermissions(); + const permissions = await getPermissions(http); dispatch(permissionsLoading(false)); dispatch(permissionsSuccess(permissions.hasPermission)); } catch (e) { diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.ts new file mode 100644 index 0000000000000..bcb4a907bdf88 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/set_breadcrumb.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 { ThunkAction } from 'redux-thunk'; +import { ChromeStart } from 'src/core/public'; +import { getDashboardBreadcrumbs, getUploadBreadcrumbs, Breadcrumb } from '../../breadcrumbs'; + +export const setBreadcrumb = ( + section: 'dashboard' | 'upload' +): ThunkAction => ( + dispatch, + getState, + { chrome, MANAGEMENT_BREADCRUMB } +) => { + if (section === 'upload') { + chrome.setBreadcrumbs(getUploadBreadcrumbs(MANAGEMENT_BREADCRUMB)); + } else { + chrome.setBreadcrumbs(getDashboardBreadcrumbs(MANAGEMENT_BREADCRUMB)); + } +}; diff --git a/x-pack/legacy/plugins/license_management/public/store/actions/start_basic.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_basic.js similarity index 88% rename from x-pack/legacy/plugins/license_management/public/store/actions/start_basic.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_basic.js index b144729c1a317..cc52916a30d8b 100644 --- a/x-pack/legacy/plugins/license_management/public/store/actions/start_basic.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_basic.js @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { createAction } from 'redux-actions'; import { startBasic } from '../../lib/es'; -import { toastNotifications } from 'ui/notify'; -import { i18n } from '@kbn/i18n'; export const startBasicLicenseStatus = createAction( 'LICENSE_MANAGEMENT_START_BASIC_LICENSE_STATUS' @@ -20,17 +19,17 @@ export const cancelStartBasicLicense = createAction( export const startBasicLicense = (currentLicenseType, ack) => async ( dispatch, getState, - { xPackInfo, $injector } + { legacy: { refreshXpack }, toasts, http } ) => { /*eslint camelcase: 0*/ - const { acknowledged, basic_was_started, error_message, acknowledge } = await startBasic(ack); + const { acknowledged, basic_was_started, error_message, acknowledge } = await startBasic(http, ack); if (acknowledged) { if (basic_was_started) { - await xPackInfo.refresh($injector); + await refreshXpack(); // reload necessary to get left nav to refresh with proper links window.location.reload(); } else { - return toastNotifications.addDanger(error_message); + return toasts.addDanger(error_message); } } else { //messages coming back in arrays diff --git a/x-pack/legacy/plugins/license_management/public/store/actions/start_trial.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_trial.js similarity index 74% rename from x-pack/legacy/plugins/license_management/public/store/actions/start_trial.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_trial.js index 0bc274366d97e..e3f33a0cdc977 100644 --- a/x-pack/legacy/plugins/license_management/public/store/actions/start_trial.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/start_trial.js @@ -6,29 +6,28 @@ import { createAction } from 'redux-actions'; import { canStartTrial, startTrial } from '../../lib/es'; -import { toastNotifications } from 'ui/notify'; export const trialStatusLoaded = createAction( 'LICENSE_MANAGEMENT_TRIAL_STATUS_LOADED' ); -export const loadTrialStatus = () => async dispatch => { - const trialOK = await canStartTrial(); +export const loadTrialStatus = () => async (dispatch, getState, { http }) => { + const trialOK = await canStartTrial(http); dispatch(trialStatusLoaded(trialOK)); }; export const startLicenseTrial = () => async ( dispatch, getState, - { xPackInfo, $injector } + { legacy: { refreshXpack }, toasts, http } ) => { /*eslint camelcase: 0*/ - const { trial_was_started, error_message } = await startTrial(); + const { trial_was_started, error_message } = await startTrial(http); if (trial_was_started) { - await xPackInfo.refresh($injector); + await refreshXpack(); // reload necessary to get left nav to refresh with proper links window.location.reload(); } else { - return toastNotifications.addDanger(error_message); + return toasts.addDanger(error_message); } }; diff --git a/x-pack/legacy/plugins/license_management/public/store/actions/upload_license.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/upload_license.js similarity index 90% rename from x-pack/legacy/plugins/license_management/public/store/actions/upload_license.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/upload_license.js index be5b3125a6a12..e93e891f6de93 100644 --- a/x-pack/legacy/plugins/license_management/public/store/actions/upload_license.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/actions/upload_license.js @@ -5,8 +5,7 @@ */ import { createAction } from 'redux-actions'; -import { addLicense } from '../actions/add_license'; -import { BASE_PATH } from '../../../common/constants/base_path'; +import { addLicense } from './add_license'; import { putLicense } from '../../lib/es'; import { addUploadErrorMessage } from './add_error_message'; import { i18n } from '@kbn/i18n'; @@ -17,7 +16,19 @@ const genericUploadError = i18n.translate('xpack.licenseMgmt.uploadLicense.gener defaultMessage: 'Error encountered uploading license:' }); -const dispatchFromResponse = async (response, dispatch, currentLicenseType, newLicenseType, { xPackInfo, kbnUrl, $injector }) => { +const dispatchFromResponse = async ( + response, + dispatch, + currentLicenseType, + newLicenseType, + { + history, + legacy: { + xPackInfo, + refreshXpack, + }, + }, +) => { const { error, acknowledged, license_status: licenseStatus, acknowledge } = response; if (error) { dispatch(uploadLicenseStatus({})); @@ -34,10 +45,10 @@ const dispatchFromResponse = async (response, dispatch, currentLicenseType, newL defaultMessage: 'The supplied license has expired.' }))); } else { - await xPackInfo.refresh($injector); + await refreshXpack(); dispatch(addLicense(xPackInfo.get('license'))); dispatch(uploadLicenseStatus({})); - kbnUrl.change(BASE_PATH); + history.replace('/home'); // reload necessary to get left nav to refresh with proper links window.location.reload(); } @@ -75,7 +86,7 @@ export const uploadLicense = (licenseString, currentLicenseType, acknowledge) => )); } try { - const response = await putLicense(licenseString, acknowledge); + const response = await putLicense(services.http, licenseString, acknowledge); await dispatchFromResponse(response, dispatch, currentLicenseType, newLicenseType, services); } catch (err) { const message = (err.responseJSON && err.responseJSON.error.reason) ? err.responseJSON.error.reason : i18n.translate( diff --git a/x-pack/legacy/plugins/license_management/public/store/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/index.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/index.js diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/index.js similarity index 80% rename from x-pack/legacy/plugins/license_management/public/sections/license_dashboard/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/index.js index 96c727969e2c2..7872202272acf 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/index.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/index.js @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LicenseDashboard } from './license_dashboard'; +export { licenseManagement } from './license_management'; diff --git a/x-pack/legacy/plugins/license_management/public/store/reducers/license.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/license.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/reducers/license.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/license.js diff --git a/x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/license_management.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/license_management.js diff --git a/x-pack/legacy/plugins/license_management/public/store/reducers/permissions.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/permissions.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/reducers/permissions.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/permissions.js diff --git a/x-pack/legacy/plugins/license_management/public/store/reducers/start_basic_license_status.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/start_basic_license_status.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/reducers/start_basic_license_status.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/start_basic_license_status.js diff --git a/x-pack/legacy/plugins/license_management/public/store/reducers/trial_status.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/trial_status.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/reducers/trial_status.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/trial_status.js diff --git a/x-pack/legacy/plugins/license_management/public/store/reducers/upload_error_message.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/upload_error_message.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/reducers/upload_error_message.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/upload_error_message.js diff --git a/x-pack/legacy/plugins/license_management/public/store/reducers/upload_status.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/upload_status.js similarity index 100% rename from x-pack/legacy/plugins/license_management/public/store/reducers/upload_status.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/reducers/upload_status.js diff --git a/x-pack/legacy/plugins/license_management/public/store/store.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/store.js similarity index 93% rename from x-pack/legacy/plugins/license_management/public/store/store.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/store/store.js index 87ccf4e2aba46..b3e6bfbe5db73 100644 --- a/x-pack/legacy/plugins/license_management/public/store/store.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/store/store.js @@ -7,7 +7,7 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; -import { licenseManagement } from './reducers/'; +import { licenseManagement } from './reducers'; export const licenseManagementStore = (initialState = {}, services = {}) => { const enhancers = [ applyMiddleware(thunk.withExtraArgument(services)) ]; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/index.ts b/x-pack/legacy/plugins/license_management/public/np_ready/index.ts new file mode 100644 index 0000000000000..59e2f02d8cb52 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { PluginInitializerContext } from 'src/core/public'; +import { LicenseManagementUIPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => new LicenseManagementUIPlugin(); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts new file mode 100644 index 0000000000000..abd658ff91db0 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts @@ -0,0 +1,50 @@ +/* + * 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 { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { PLUGIN } from '../../common/constants'; +import { Breadcrumb } from './application/breadcrumbs'; + +export interface Plugins { + __LEGACY: { + xpackInfo: XPackMainPlugin; + refreshXpack: () => void; + MANAGEMENT_BREADCRUMB: Breadcrumb; + }; +} + +export class LicenseManagementUIPlugin implements Plugin { + setup({ application, notifications, http }: CoreSetup, { __LEGACY }: Plugins) { + application.register({ + id: PLUGIN.ID, + title: PLUGIN.TITLE, + async mount( + { + core: { + docLinks, + i18n: { Context: I18nContext }, + chrome, + }, + }, + { element } + ) { + const { boot } = await import('./application'); + return boot({ + legacy: { ...__LEGACY }, + I18nContext, + toasts: notifications.toasts, + docLinks, + http, + element, + chrome, + }); + }, + }); + } + start(core: CoreStart, plugins: any) {} + stop() {} +} diff --git a/x-pack/legacy/plugins/license_management/public/register_route.js b/x-pack/legacy/plugins/license_management/public/register_route.js deleted file mode 100644 index fcf1b85f95b8a..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/register_route.js +++ /dev/null @@ -1,109 +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 { render, unmountComponentAtNode } from 'react-dom'; -import { Provider } from 'react-redux'; -import { HashRouter } from 'react-router-dom'; -import { setTelemetryOptInService, setTelemetryEnabled, setHttpClient, TelemetryOptInProvider } from './lib/telemetry'; -import { I18nContext } from 'ui/i18n'; -import chrome from 'ui/chrome'; - -import { App } from './app.container'; -import { BASE_PATH } from '../common/constants/base_path'; - -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import template from './main.html'; -import { licenseManagementStore } from './store'; -import { getDashboardBreadcrumbs, getUploadBreadcrumbs } from './breadcrumbs'; - -const renderReact = (elem, store) => { - render( - - - - - - - , - elem - ); -}; - -/* - This method handles the cleanup needed when route is scope is destroyed. It also prevents Angular - from destroying scope when route changes and both old route and new route are this same route. -*/ -const manageAngularLifecycle = ($scope, $route, elem) => { - const lastRoute = $route.current; - const deregister = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - // if templates are the same we are on the same route - if (lastRoute.$$route.template === currentRoute.$$route.template) { - // update the breadcrumbs by re-running the k7Breadcrumbs function - chrome.breadcrumbs.set(currentRoute.$$route.k7Breadcrumbs($route)); - // this prevents angular from destroying scope - $route.current = lastRoute; - } - }); - $scope.$on('$destroy', () => { - deregister && deregister(); - // manually unmount component when scope is destroyed - elem && unmountComponentAtNode(elem); - }); -}; -const initializeTelemetry = ($injector) => { - const telemetryEnabled = $injector.get('telemetryEnabled'); - const Private = $injector.get('Private'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - setTelemetryOptInService(telemetryOptInProvider); - setTelemetryEnabled(telemetryEnabled); - setHttpClient($injector.get('$http')); -}; -routes - .when(`${BASE_PATH}:view?`, { - template: template, - k7Breadcrumbs($route) { - switch ($route.current.params.view) { - case 'upload_license': - return getUploadBreadcrumbs(); - default: - return getDashboardBreadcrumbs(); - } - }, - controllerAs: 'licenseManagement', - controller: class LicenseManagementController { - - constructor($injector, $rootScope, $scope, $route, kbnUrl) { - initializeTelemetry($injector); - let autoLogout = null; - /* if security is disabled, there will be no autoLogout service, - so just substitute noop function in that case */ - try { - autoLogout = $injector.get('autoLogout'); - } catch (e) { - autoLogout = () => {}; - } - - $scope.$$postDigest(() => { - const elem = document.getElementById('licenseReactRoot'); - const initialState = { license: xpackInfo.get('license') }; - const kbnUrlWrapper = { - change(url) { - kbnUrl.change(url); - $rootScope.$digest(); - } - }; - const services = { autoLogout, xPackInfo: xpackInfo, kbnUrl: kbnUrlWrapper, $injector }; - const store = licenseManagementStore(initialState, services); - renderReact(elem, store); - manageAngularLifecycle($scope, $route, elem); - }); - } - } - }); diff --git a/x-pack/legacy/plugins/license_management/public/register_route.ts b/x-pack/legacy/plugins/license_management/public/register_route.ts new file mode 100644 index 0000000000000..ad84f28c6b6d7 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/register_route.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 { App } from 'src/core/public'; + +/* Legacy Imports */ +import { npSetup, npStart } from 'ui/new_platform'; +import { MANAGEMENT_BREADCRUMB } from 'ui/management'; +import routes from 'ui/routes'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; + +import { plugin } from './np_ready'; + +import { + setTelemetryOptInService, + setTelemetryEnabled, + setHttpClient, + TelemetryOptInProvider, + // @ts-ignore +} from './np_ready/application/lib/telemetry'; + +import { BASE_PATH } from '../common/constants'; + +/* + This method handles the cleanup needed when route is scope is destroyed. It also prevents Angular + from destroying scope when route changes and both old route and new route are this same route. +*/ +const manageAngularLifecycle = ($scope: any, $route: any, unmount: () => void) => { + const lastRoute = $route.current; + const deregister = $scope.$on('$locationChangeSuccess', () => { + const currentRoute = $route.current; + // if templates are the same we are on the same route + if (lastRoute.$$route.template === currentRoute.$$route.template) { + // this prevents angular from destroying scope + $route.current = lastRoute; + } + }); + $scope.$on('$destroy', () => { + if (deregister) { + deregister(); + } + unmount(); + }); +}; + +const initializeTelemetry = ($injector: any) => { + const telemetryEnabled = $injector.get('telemetryEnabled'); + const Private = $injector.get('Private'); + const telemetryOptInProvider = Private(TelemetryOptInProvider); + setTelemetryOptInService(telemetryOptInProvider); + setTelemetryEnabled(telemetryEnabled); + setHttpClient($injector.get('$http')); +}; + +const template = ` +
+
`; + +routes.when(`${BASE_PATH}:view?`, { + template, + controllerAs: 'licenseManagement', + controller: class LicenseManagementController { + constructor($injector: any, $rootScope: any, $scope: any, $route: any) { + initializeTelemetry($injector); + + $scope.$$postDigest(() => { + const element = document.getElementById('licenseReactRoot')!; + + const refreshXpack = async () => { + await xpackInfo.refresh($injector); + }; + + plugin({} as any).setup( + { + ...npSetup.core, + application: { + ...npSetup.core.application, + async register(app: App) { + const unmountApp = await app.mount({ ...npStart } as any, { + element, + appBasePath: '', + }); + manageAngularLifecycle($scope, $route, unmountApp as any); + }, + }, + }, + { + __LEGACY: { xpackInfo, refreshXpack, MANAGEMENT_BREADCRUMB }, + } + ); + }); + } + } as any, +} as any); diff --git a/x-pack/legacy/plugins/license_management/server/lib/license.js b/x-pack/legacy/plugins/license_management/server/lib/license.js deleted file mode 100644 index bc52fe7f56b34..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/lib/license.js +++ /dev/null @@ -1,27 +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. - */ - -const getLicensePath = (acknowledge) => `/_license${ acknowledge ? '?acknowledge=true' : ''}`; - -export async function putLicense(req, xpackInfo) { - const { acknowledge } = req.query; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); - const options = { - method: 'POST', - path: getLicensePath(acknowledge), - body: req.payload - }; - try { - const response = await callWithRequest(req, 'transport.request', options); - const { acknowledged, license_status: licenseStatus } = response; - if (acknowledged && licenseStatus === 'valid') { - await xpackInfo.refreshNow(); - } - return response; - } catch (error) { - return error.body; - } -} diff --git a/x-pack/legacy/plugins/license_management/server/lib/start_basic.js b/x-pack/legacy/plugins/license_management/server/lib/start_basic.js deleted file mode 100644 index 0924146b70280..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/lib/start_basic.js +++ /dev/null @@ -1,28 +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. - */ - -const getStartBasicPath = (acknowledge) => `/_license/start_basic${ acknowledge ? '?acknowledge=true' : ''}`; - - -export async function startBasic(req, xpackInfo) { - const { acknowledge } = req.query; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); - const options = { - method: 'POST', - path: getStartBasicPath(acknowledge) - }; - try { - const response = await callWithRequest(req, 'transport.request', options); - /*eslint camelcase: 0*/ - const { basic_was_started } = response; - if (basic_was_started) { - await xpackInfo.refreshNow(); - } - return response; - } catch (error) { - return error.body; - } -} diff --git a/x-pack/legacy/plugins/license_management/server/lib/start_trial.js b/x-pack/legacy/plugins/license_management/server/lib/start_trial.js deleted file mode 100644 index 19b702e4e43c4..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/lib/start_trial.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. - */ - -export async function canStartTrial(req) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); - const options = { - method: 'GET', - path: '/_license/trial_status' - }; - try { - const response = await callWithRequest(req, 'transport.request', options); - const { eligible_to_start_trial } = response; - return eligible_to_start_trial; - } catch (error) { - return error.body; - } -} -export async function startTrial(req, xpackInfo) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); - const options = { - method: 'POST', - path: '/_license/start_trial?acknowledge=true' - }; - try { - /*eslint camelcase: 0*/ - const response = await callWithRequest(req, 'transport.request', options); - const { trial_was_started } = response; - if (trial_was_started) { - await xpackInfo.refreshNow(); - } - return response; - } catch (error) { - return error.body; - } -} diff --git a/x-pack/legacy/plugins/license_management/server/routes/api/license/register_start_basic_route.js b/x-pack/legacy/plugins/license_management/server/np_ready/index.ts similarity index 53% rename from x-pack/legacy/plugins/license_management/server/routes/api/license/register_start_basic_route.js rename to x-pack/legacy/plugins/license_management/server/np_ready/index.ts index fd7bfcf8d7c2a..2ad4143a94730 100644 --- a/x-pack/legacy/plugins/license_management/server/routes/api/license/register_start_basic_route.js +++ b/x-pack/legacy/plugins/license_management/server/np_ready/index.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { startBasic } from '../../../lib/start_basic'; +import { PluginInitializerContext } from 'src/core/server'; +import { LicenseManagementServerPlugin } from './plugin'; -export function registerStartBasicRoute(router, xpackInfo) { - router.post('/start_basic', (request) => { - return startBasic(request, xpackInfo); - }); -} +export const plugin = (ctx: PluginInitializerContext) => new LicenseManagementServerPlugin(); diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/lib/license.ts b/x-pack/legacy/plugins/license_management/server/np_ready/lib/license.ts new file mode 100644 index 0000000000000..b52c9d50170b9 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/lib/license.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaRequest } from 'src/core/server'; +import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; +const getLicensePath = (acknowledge: boolean) => + `/_license${acknowledge ? '?acknowledge=true' : ''}`; + +export async function putLicense( + req: KibanaRequest, + elasticsearch: ElasticsearchPlugin, + xpackInfo: any +) { + const { acknowledge } = req.query; + const { callWithRequest } = elasticsearch.getCluster('admin'); + const options = { + method: 'POST', + path: getLicensePath(Boolean(acknowledge)), + body: req.body, + }; + try { + const response = await callWithRequest(req as any, 'transport.request', options); + const { acknowledged, license_status: licenseStatus } = response; + if (acknowledged && licenseStatus === 'valid') { + await xpackInfo.refreshNow(); + } + return response; + } catch (error) { + return error.body; + } +} diff --git a/x-pack/legacy/plugins/license_management/server/lib/permissions.js b/x-pack/legacy/plugins/license_management/server/np_ready/lib/permissions.ts similarity index 59% rename from x-pack/legacy/plugins/license_management/server/lib/permissions.js rename to x-pack/legacy/plugins/license_management/server/np_ready/lib/permissions.ts index 11d25a9c2dc24..84cd92821797f 100644 --- a/x-pack/legacy/plugins/license_management/server/lib/permissions.js +++ b/x-pack/legacy/plugins/license_management/server/np_ready/lib/permissions.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { wrapCustomError } from '../../../../server/lib/create_router/error_wrappers'; - -export async function getPermissions(req, xpackInfo) { - if (!xpackInfo) { - // xpackInfo is updated via poll, so it may not be available until polling has begun. - // In this rare situation, tell the client the service is temporarily unavailable. - throw wrapCustomError(new Error('Security info unavailable'), 503); - } +import { KibanaRequest } from 'src/core/server'; +import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; +export async function getPermissions( + req: KibanaRequest, + elasticsearch: ElasticsearchPlugin, + xpackInfo: any +) { const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { // If security isn't enabled, let the user use license management @@ -21,22 +20,21 @@ export async function getPermissions(req, xpackInfo) { }; } - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); + const { callWithRequest } = elasticsearch.getCluster('admin'); const options = { method: 'POST', path: '/_security/user/_has_privileges', body: { cluster: ['manage'], // License management requires "manage" cluster privileges - } + }, }; try { - const response = await callWithRequest(req, 'transport.request', options); + const response = await callWithRequest(req as any, 'transport.request', options); return { hasPermission: response.cluster.manage, }; } catch (error) { return error.body; } - } diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_basic.ts b/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_basic.ts new file mode 100644 index 0000000000000..ba042be132d68 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_basic.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 { KibanaRequest } from 'kibana/server'; +import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; + +const getStartBasicPath = (acknowledge: boolean) => + `/_license/start_basic${acknowledge ? '?acknowledge=true' : ''}`; + +export async function startBasic( + req: KibanaRequest, + elasticsearch: ElasticsearchPlugin, + xpackInfo: any +) { + const { acknowledge } = req.query; + const { callWithRequest } = elasticsearch.getCluster('admin'); + const options = { + method: 'POST', + path: getStartBasicPath(Boolean(acknowledge)), + }; + try { + const response = await callWithRequest(req as any, 'transport.request', options); + const { basic_was_started: basicWasStarted } = response; + if (basicWasStarted) { + await xpackInfo.refreshNow(); + } + return response; + } catch (error) { + return error.body; + } +} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts b/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.ts new file mode 100644 index 0000000000000..3569085d413ca --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/lib/start_trial.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 { KibanaRequest } from 'src/core/server'; +import { ElasticsearchPlugin } from '../../../../../../../src/legacy/core_plugins/elasticsearch'; + +export async function canStartTrial( + req: KibanaRequest, + elasticsearch: ElasticsearchPlugin +) { + const { callWithRequest } = elasticsearch.getCluster('admin'); + const options = { + method: 'GET', + path: '/_license/trial_status', + }; + try { + const response = await callWithRequest(req as any, 'transport.request', options); + return response.eligible_to_start_trial; + } catch (error) { + return error.body; + } +} + +export async function startTrial( + req: KibanaRequest, + elasticsearch: ElasticsearchPlugin, + xpackInfo: any +) { + const { callWithRequest } = elasticsearch.getCluster('admin'); + const options = { + method: 'POST', + path: '/_license/start_trial?acknowledge=true', + }; + try { + const response = await callWithRequest(req as any, 'transport.request', options); + const { trial_was_started: trialWasStarted } = response; + if (trialWasStarted) { + await xpackInfo.refreshNow(); + } + return response; + } catch (error) { + return error.body; + } +} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts new file mode 100644 index 0000000000000..9f065cf98d715 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/plugin.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup } from 'src/core/server'; +import { Dependencies, Server } from './types'; + +import { + registerLicenseRoute, + registerStartTrialRoutes, + registerStartBasicRoute, + registerPermissionsRoute, +} from './routes/api/license'; + +export class LicenseManagementServerPlugin implements Plugin { + setup({ http }: CoreSetup, { __LEGACY }: Dependencies) { + const xpackInfo = __LEGACY.xpackMain.info; + const router = http.createRouter(); + + const server: Server = { + router, + }; + + const legacy = { plugins: __LEGACY }; + + registerLicenseRoute(server, legacy, xpackInfo); + registerStartTrialRoutes(server, legacy, xpackInfo); + registerStartBasicRoute(server, legacy, xpackInfo); + registerPermissionsRoute(server, legacy, xpackInfo); + } + start() {} + stop() {} +} diff --git a/x-pack/legacy/plugins/license_management/server/routes/api/license/index.js b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/server/routes/api/license/index.js rename to x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/index.ts diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts new file mode 100644 index 0000000000000..cdc929a2f3bb3 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.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 { schema } from '@kbn/config-schema'; +import { putLicense } from '../../../lib/license'; +import { Legacy, Server } from '../../../types'; + +export function registerLicenseRoute(server: Server, legacy: Legacy, xpackInfo: any) { + server.router.put( + { + path: '/api/license', + validate: { + query: schema.object({ acknowledge: schema.string() }), + body: schema.object({ + license: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + async (ctx, request, response) => { + try { + return response.ok({ + body: await putLicense(request, legacy.plugins.elasticsearch, xpackInfo), + }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ); +} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts new file mode 100644 index 0000000000000..0f6c343d04fcd --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_permissions_route.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getPermissions } from '../../../lib/permissions'; +import { Legacy, Server } from '../../../types'; + +export function registerPermissionsRoute(server: Server, legacy: Legacy, xpackInfo: any) { + server.router.post( + { path: '/api/license/permissions', validate: false }, + async (ctx, request, response) => { + if (!xpackInfo) { + // xpackInfo is updated via poll, so it may not be available until polling has begun. + // In this rare situation, tell the client the service is temporarily unavailable. + return response.customError({ statusCode: 503, body: 'Security info unavailable' }); + } + + try { + return response.ok({ + body: await getPermissions(request, legacy.plugins.elasticsearch, xpackInfo), + }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ); +} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts new file mode 100644 index 0000000000000..ee7ac8602104b --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_basic_route.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { startBasic } from '../../../lib/start_basic'; +import { Legacy, Server } from '../../../types'; + +export function registerStartBasicRoute(server: Server, legacy: Legacy, xpackInfo: any) { + server.router.post( + { + path: '/api/license/start_basic', + validate: { query: schema.object({ acknowledge: schema.string() }) }, + }, + async (ctx, request, response) => { + try { + return response.ok({ + body: await startBasic(request, legacy.plugins.elasticsearch, xpackInfo), + }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ); +} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.ts new file mode 100644 index 0000000000000..d93f13eba363a --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_start_trial_routes.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 { canStartTrial, startTrial } from '../../../lib/start_trial'; +import { Legacy, Server } from '../../../types'; + +export function registerStartTrialRoutes(server: Server, legacy: Legacy, xpackInfo: any) { + server.router.get( + { path: '/api/license/start_trial', validate: false }, + async (ctx, request, response) => { + try { + return response.ok({ body: await canStartTrial(request, legacy.plugins.elasticsearch) }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ); + + server.router.post( + { path: '/api/license/start_trial', validate: false }, + async (ctx, request, response) => { + try { + return response.ok({ + body: await startTrial(request, legacy.plugins.elasticsearch, xpackInfo), + }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ); +} diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/types.ts b/x-pack/legacy/plugins/license_management/server/np_ready/types.ts new file mode 100644 index 0000000000000..a636481323e29 --- /dev/null +++ b/x-pack/legacy/plugins/license_management/server/np_ready/types.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 { IRouter } from 'src/core/server'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { ElasticsearchPlugin } from '../../../../../../src/legacy/core_plugins/elasticsearch'; + +export interface Dependencies { + __LEGACY: { + xpackMain: XPackMainPlugin; + elasticsearch: ElasticsearchPlugin; + }; +} + +export interface Server { + router: IRouter; +} + +export interface Legacy { + plugins: Dependencies['__LEGACY']; +} diff --git a/x-pack/legacy/plugins/license_management/server/routes/api/license/register_permissions_route.js b/x-pack/legacy/plugins/license_management/server/routes/api/license/register_permissions_route.js deleted file mode 100644 index d8fd4e5abd8f2..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/routes/api/license/register_permissions_route.js +++ /dev/null @@ -1,13 +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 { getPermissions } from '../../../lib/permissions'; - -export function registerPermissionsRoute(router, xpackInfo) { - router.post('/permissions', (request) => { - return getPermissions(request, xpackInfo); - }); -} diff --git a/x-pack/legacy/plugins/license_management/server/routes/api/license/register_start_trial_routes.js b/x-pack/legacy/plugins/license_management/server/routes/api/license/register_start_trial_routes.js deleted file mode 100644 index c13ea680fa0da..0000000000000 --- a/x-pack/legacy/plugins/license_management/server/routes/api/license/register_start_trial_routes.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { canStartTrial, startTrial } from '../../../lib/start_trial'; - -export function registerStartTrialRoutes(router, xpackInfo) { - router.get('/start_trial', (request) => { - return canStartTrial(request); - }); - router.post('/start_trial', (request) => { - return startTrial(request, xpackInfo); - }); -} diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 3b2f887e13c87..77b57e3fe4965 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -57,6 +57,8 @@ export const FIELD_ORIGIN = { }; export const SOURCE_DATA_ID_ORIGIN = 'source'; +export const META_ID_ORIGIN_SUFFIX = 'meta'; +export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_SUFFIX}`; export const GEOJSON_FILE = 'GEOJSON_FILE'; @@ -124,6 +126,11 @@ export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLab export const COUNT_PROP_NAME = 'doc_count'; export const STYLE_TYPE = { - 'STATIC': 'STATIC', - 'DYNAMIC': 'DYNAMIC' + STATIC: 'STATIC', + DYNAMIC: 'DYNAMIC' +}; + +export const LAYER_STYLE_TYPE = { + VECTOR: 'VECTOR', + HEATMAP: 'HEATMAP' }; diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js new file mode 100644 index 0000000000000..ed585e013d06f --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { LAYER_TYPE, STYLE_TYPE } from '../constants'; + +function isVectorLayer(layerDescriptor) { + const layerType = _.get(layerDescriptor, 'type'); + return layerType === LAYER_TYPE.VECTOR; +} + +export function addFieldMetaOptions({ attributes }) { + if (!attributes.layerListJSON) { + return attributes; + } + + const layerList = JSON.parse(attributes.layerListJSON); + layerList.forEach((layerDescriptor) => { + if (isVectorLayer(layerDescriptor) && _.has(layerDescriptor, 'style.properties')) { + Object.values(layerDescriptor.style.properties).forEach(stylePropertyDescriptor => { + if (stylePropertyDescriptor.type === STYLE_TYPE.DYNAMIC) { + stylePropertyDescriptor.options.fieldMetaOptions = { + isEnabled: false, // turn off field metadata to avoid changing behavior of existing saved objects + sigma: 3, + }; + } + }); + } + }); + + return { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }; +} diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js new file mode 100644 index 0000000000000..905f77223b3bc --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js @@ -0,0 +1,121 @@ +/* + * 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 { addFieldMetaOptions } from './add_field_meta_options'; +import { LAYER_TYPE, STYLE_TYPE } from '../constants'; + +describe('addFieldMetaOptions', () => { + + test('Should handle missing layerListJSON attribute', () => { + const attributes = { + title: 'my map', + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + }); + }); + + test('Should ignore non-vector layers', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.HEATMAP, + style: { + type: 'HEATMAP', + colorRampName: 'Greens' + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON + }); + }); + + test('Should ignore static style properties', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + lineColor: { + type: STYLE_TYPE.STATIC, + options: { + color: '#FFFFFF' + } + } + } + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON + }); + }); + + test('Should add field meta options to dynamic style properties', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + field: { + name: 'my_field', + origin: 'source' + }, + color: 'Greys' + } + } + } + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON: JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + field: { + name: 'my_field', + origin: 'source' + }, + color: 'Greys', + fieldMetaOptions: { + isEnabled: false, + sigma: 3, + } + } + } + } + } + } + ]) + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index 39dc58f259961..df19c8425199a 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -8,6 +8,7 @@ import { extractReferences } from './common/migrations/references'; import { emsRasterTileToEmsVectorTile } from './common/migrations/ems_raster_tile_to_ems_vector_tile'; import { topHitsTimeToSort } from './common/migrations/top_hits_time_to_sort'; import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_global_query'; +import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; export const migrations = { 'map': { @@ -37,11 +38,12 @@ export const migrations = { }; }, '7.6.0': (doc) => { - const attributes = moveApplyGlobalQueryToSources(doc); + const attributesPhase1 = moveApplyGlobalQueryToSources(doc); + const attributesPhase2 = addFieldMetaOptions({ attributes: attributesPhase1 }); return { ...doc, - attributes, + attributes: attributesPhase2, }; } }, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index 84d7f8004ad9b..0808885f5c448 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -13,8 +13,12 @@ import { addSpritesheetToMap, } from './utils'; import { getGlyphUrl, isRetina } from '../../../meta'; -import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; -import mapboxgl from 'mapbox-gl'; +import { + DECIMAL_DEGREES_PRECISION, + ZOOM_PRECISION, +} from '../../../../common/constants'; +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; +import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; import chrome from 'ui/chrome'; import { spritesheet } from '@elastic/maki'; import sprites1 from '@elastic/maki/dist/sprite@1.png'; @@ -22,6 +26,8 @@ import sprites2 from '@elastic/maki/dist/sprite@2.png'; import { DrawControl } from './draw_control'; import { TooltipControl } from './tooltip_control'; +mapboxgl.workerUrl = mbWorkerUrl; + export class MBMapContainer extends React.Component { state = { prevLayerList: undefined, diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 7169014542710..609f075e194f8 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -6,12 +6,12 @@ import { getRequestInspectorStats, getResponseInspectorStats } from '../../../../../src/legacy/ui/public/courier'; export { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; import { esFilters } from '../../../../../src/plugins/data/public'; +import { npStart } from 'ui/new_platform'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; export { SearchSource } from '../../../../../src/legacy/ui/public/courier'; -export const indexPatternService = data.indexPatterns.indexPatterns; +export const indexPatternService = npStart.plugins.data.indexPatterns; export async function fetchSearchSourceAndRecordWithInspector({ searchSource, diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js index eb80169e94eab..af78e3a871802 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ - import { AbstractField } from './field'; import { COUNT_AGG_TYPE } from '../../../common/constants'; +import { isMetricCountable } from '../util/is_metric_countable'; import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property'; export class ESAggMetricField extends AbstractField { @@ -36,6 +36,11 @@ export class ESAggMetricField extends AbstractField { return (this.getAggType() === COUNT_AGG_TYPE) ? true : !!this._esDocField; } + async getDataType() { + // aggregations only provide numerical data + return 'number'; + } + getESDocFieldName() { return this._esDocField ? this._esDocField.getName() : ''; } @@ -55,7 +60,6 @@ export class ESAggMetricField extends AbstractField { ); } - makeMetricAggConfig() { const metricAggConfig = { id: this.getName(), @@ -69,4 +73,13 @@ export class ESAggMetricField extends AbstractField { } return metricAggConfig; } + + supportsFieldMeta() { + // count and sum aggregations are not within field bounds so they do not support field meta. + return !isMetricCountable(this.getAggType()); + } + + async getFieldMetaRequest(config) { + return this._esDocField.getFieldMetaRequest(config); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js new file mode 100644 index 0000000000000..65b8c518fa895 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESAggMetricField } from './es_agg_field'; +import { METRIC_TYPE } from '../../../common/constants'; + +describe('supportsFieldMeta', () => { + + test('Non-counting aggregations should support field meta', () => { + const avgMetric = new ESAggMetricField({ aggType: METRIC_TYPE.AVG }); + expect(avgMetric.supportsFieldMeta()).toBe(true); + const maxMetric = new ESAggMetricField({ aggType: METRIC_TYPE.MAX }); + expect(maxMetric.supportsFieldMeta()).toBe(true); + const minMetric = new ESAggMetricField({ aggType: METRIC_TYPE.MIN }); + expect(minMetric.supportsFieldMeta()).toBe(true); + }); + + test('Counting aggregations should not support field meta', () => { + const countMetric = new ESAggMetricField({ aggType: METRIC_TYPE.COUNT }); + expect(countMetric.supportsFieldMeta()).toBe(false); + const sumMetric = new ESAggMetricField({ aggType: METRIC_TYPE.SUM }); + expect(sumMetric.supportsFieldMeta()).toBe(false); + const uniqueCountMetric = new ESAggMetricField({ aggType: METRIC_TYPE.UNIQUE_COUNT }); + expect(uniqueCountMetric.supportsFieldMeta()).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js index 5cc0c9a29ce02..ad15c6249e554 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js @@ -27,4 +27,31 @@ export class ESDocField extends AbstractField { return field.type; } + supportsFieldMeta() { + return true; + } + + async getFieldMetaRequest(/* config */) { + const field = await this._getField(); + + if (field.type !== 'number' && field.type !== 'date') { + return null; + } + + const extendedStats = {}; + if (field.scripted) { + extendedStats.script = { + source: field.script, + lang: field.lang + }; + } else { + extendedStats.field = this._fieldName; + } + return { + [this._fieldName]: { + extended_stats: extendedStats + } + }; + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/field.js b/x-pack/legacy/plugins/maps/public/layers/fields/field.js index b53c6991c6ebe..f1bb116d29c8b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/field.js @@ -42,4 +42,12 @@ export class AbstractField { getOrigin() { return this._origin; } + + supportsFieldMeta() { + return false; + } + + async getFieldMetaRequest(/* config */) { + return null; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js index 184fdc0663bd7..432492973cce0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js @@ -7,6 +7,7 @@ import { ESTermSource } from '../sources/es_term_source'; import { getComputedFieldNamePrefix } from '../styles/vector/style_util'; +import { META_ID_ORIGIN_SUFFIX } from '../../../common/constants'; export class InnerJoin { @@ -36,10 +37,14 @@ export class InnerJoin { // Source request id must be static and unique because the re-fetch logic uses the id to locate the previous request. // Elasticsearch sources have a static and unique id so that requests can be modified in the inspector. // Using the right source id as the source request id because it meets the above criteria. - getSourceId() { + getSourceDataRequestId() { return `join_source_${this._rightSource.getId()}`; } + getSourceMetaDataRequestId() { + return `${this.getSourceDataRequestId()}_${META_ID_ORIGIN_SUFFIX}`; + } + getLeftField() { return this._leftField; } diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 1c2f33df66bf8..b1f3c32f267b9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -80,7 +80,7 @@ export class AbstractLayer { } supportsElasticsearchFilters() { - return this._source.supportsElasticsearchFilters(); + return this._source.isESSource(); } async supportsFitToBounds() { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 413f99480a8c2..f4cb43ad90146 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -15,7 +15,7 @@ import { AggConfigs } from 'ui/agg_types'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { convertToGeoJson } from './convert_to_geojson'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { vectorStyles } from '../../styles/vector/vector_style_defaults'; +import { getDefaultDynamicProperties, VECTOR_STYLES } from '../../styles/vector/vector_style_defaults'; import { RENDER_AS } from './render_as'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; @@ -170,13 +170,15 @@ export class ESGeoGridSource extends AbstractESAggSource { const searchSource = await this._makeSearchSource(searchFilters, 0); const aggConfigs = new AggConfigs(indexPattern, this._makeAggConfigs(searchFilters.geogridPrecision), aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const esResponse = await this._runEsQuery( - layerName, + const esResponse = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, searchSource, registerCancelCallback, - i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { + requestDescription: i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { defaultMessage: 'Elasticsearch geo grid aggregation request' - })); + }), + }); const tabifiedResp = tabifyAggResponse(aggConfigs, esResponse); const { featureCollection } = convertToGeoJson({ @@ -226,10 +228,14 @@ export class ESGeoGridSource extends AbstractESAggSource { sourceDescriptor: this._descriptor, ...options }); + + const defaultDynamicProperties = getDefaultDynamicProperties(); + descriptor.style = VectorStyle.createDescriptor({ - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -238,9 +244,10 @@ export class ESGeoGridSource extends AbstractESAggSource { color: 'Blues' } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js index 1b446e1f2159a..cc1e53dc5cb3f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js @@ -8,13 +8,13 @@ import React, { Fragment, Component } from 'react'; import { RENDER_AS } from './render_as'; import { MetricsEditor } from '../../../components/metrics_editor'; -import { METRIC_TYPE } from '../../../../common/constants'; import { indexPatternService } from '../../../kibana_services'; import { ResolutionEditor } from './resolution_editor'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; +import { isMetricCountable } from '../../util/is_metric_countable'; export class UpdateSourceEditor extends Component { state = { @@ -72,7 +72,7 @@ export class UpdateSourceEditor extends Component { this.props.renderAs === RENDER_AS.HEATMAP ? metric => { //these are countable metrics, where blending heatmap color blobs make sense - return [METRIC_TYPE.COUNT, METRIC_TYPE.SUM, METRIC_TYPE.UNIQUE_COUNT].includes(metric.value); + return isMetricCountable(metric.value); } : null; const allowMultipleMetrics = this.props.renderAs !== RENDER_AS.HEATMAP; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 01220136b14f3..4eb0a952defba 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -12,7 +12,7 @@ import { VectorLayer } from '../../vector_layer'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { vectorStyles } from '../../styles/vector/vector_style_defaults'; +import { getDefaultDynamicProperties, VECTOR_STYLES } from '../../styles/vector/vector_style_defaults'; import { i18n } from '@kbn/i18n'; import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW, COUNT_PROP_NAME, COUNT_PROP_LABEL } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -123,10 +123,12 @@ export class ESPewPewSource extends AbstractESAggSource { } createDefaultLayer(options) { + const defaultDynamicProperties = getDefaultDynamicProperties(); const styleDescriptor = VectorStyle.createDescriptor({ - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -135,9 +137,10 @@ export class ESPewPewSource extends AbstractESAggSource { color: 'Blues' } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -203,13 +206,15 @@ export class ESPewPewSource extends AbstractESAggSource { } }); - const esResponse = await this._runEsQuery( - layerName, + const esResponse = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, searchSource, registerCancelCallback, - i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { + requestDescription: i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { defaultMessage: 'Source-destination connections request' - })); + }), + }); const { featureCollection } = convertToLines(esResponse); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 57a43f924b7e6..453a1851e47aa 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -261,7 +261,13 @@ export class ESSearchSource extends AbstractESSource { } }); - const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document top hits request'); + const resp = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, + searchSource, + registerCancelCallback, + requestDescription: 'Elasticsearch document top hits request', + }); const allHits = []; const entityBuckets = _.get(resp, 'aggregations.entitySplit.buckets', []); @@ -322,7 +328,13 @@ export class ESSearchSource extends AbstractESSource { searchSource.setField('sort', this._buildEsSort()); } - const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document request'); + const resp = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, + searchSource, + registerCancelCallback, + requestDescription: 'Elasticsearch document request', + }); return { hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index c2f4f7e755288..b5d7f7a6f606a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -54,7 +54,7 @@ export class AbstractESSource extends AbstractVectorSource { return []; } - supportsElasticsearchFilters() { + isESSource() { return true; } @@ -73,7 +73,7 @@ export class AbstractESSource extends AbstractVectorSource { return []; } - async _runEsQuery(requestName, searchSource, registerCancelCallback, requestDescription) { + async _runEsQuery({ requestId, requestName, requestDescription, searchSource, registerCancelCallback }) { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); @@ -82,7 +82,7 @@ export class AbstractESSource extends AbstractVectorSource { inspectorAdapters: this._inspectorAdapters, searchSource, requestName, - requestId: this.getId(), + requestId, requestDesc: requestDescription, abortSignal: abortController.signal, }); @@ -271,4 +271,42 @@ export class AbstractESSource extends AbstractVectorSource { return fieldFromIndexPattern.format.getConverterFor('text'); } + async loadStylePropsMeta(layerName, style, dynamicStyleProps, registerCancelCallback, searchFilters) { + const promises = dynamicStyleProps.map(dynamicStyleProp => { + return dynamicStyleProp.getFieldMetaRequest(); + }); + + const fieldAggRequests = await Promise.all(promises); + const aggs = fieldAggRequests.reduce((aggs, fieldAggRequest) => { + return fieldAggRequest ? { ...aggs, ...fieldAggRequest } : aggs; + }, {}); + + const indexPattern = await this.getIndexPattern(); + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('size', 0); + searchSource.setField('aggs', aggs); + if (searchFilters.sourceQuery) { + searchSource.setField('query', searchFilters.sourceQuery); + } + if (style.isTimeAware() && await this.isTimeAware()) { + searchSource.setField('filter', [timefilter.createFilter(indexPattern, searchFilters.timeFilters)]); + } + + const resp = await this._runEsQuery({ + requestId: `${this.getId()}_styleMeta`, + requestName: i18n.translate('xpack.maps.source.esSource.stylePropsMetaRequestName', { + defaultMessage: '{layerName} - metadata', + values: { layerName } + }), + searchSource, + registerCancelCallback, + requestDescription: i18n.translate('xpack.maps.source.esSource.stylePropsMetaRequestDescription', { + defaultMessage: 'Elasticsearch request retrieving field metadata used for calculating symbolization bands.', + }), + }); + + return resp.aggregations; + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index afc402fa81bcb..57366e502d581 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -103,9 +103,13 @@ export class ESTermSource extends AbstractESAggSource { const aggConfigs = new AggConfigs(indexPattern, configStates, aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const requestName = `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`; - const requestDesc = this._getRequestDescription(leftSourceName, leftFieldName); - const rawEsData = await this._runEsQuery(requestName, searchSource, registerCancelCallback, requestDesc); + const rawEsData = await this._runEsQuery({ + requestId: this.getId(), + requestName: `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`, + searchSource, + registerCancelCallback, + requestDescription: this._getRequestDescription(leftSourceName, leftFieldName), + }); const metricPropertyNames = configStates .filter(configState => { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index 78e57f79bbe56..d3b2971dbbb0c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -123,7 +123,7 @@ export class AbstractSource { return AbstractSource.isIndexingSource; } - supportsElasticsearchFilters() { + isESSource() { return false; } @@ -136,6 +136,10 @@ export class AbstractSource { async getFieldFormatter(/* fieldName */) { return null; } + + async loadStylePropsMeta() { + throw new Error(`Source#loadStylePropsMeta not implemented`); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index e4982c86b53bb..ed64f408b2585 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -10,13 +10,14 @@ import { AbstractStyle } from '../abstract_style'; import { HeatmapStyleEditor } from './components/heatmap_style_editor'; import { HeatmapLegend } from './components/legend/heatmap_legend'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants'; +import { LAYER_STYLE_TYPE } from '../../../../common/constants'; import { getColorRampStops } from '../color_utils'; import { i18n } from '@kbn/i18n'; import { EuiIcon } from '@elastic/eui'; export class HeatmapStyle extends AbstractStyle { - static type = 'HEATMAP'; + static type = LAYER_STYLE_TYPE.HEATMAP; constructor(descriptor = {}) { super(); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js new file mode 100644 index 0000000000000..095740abe3dda --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js @@ -0,0 +1,139 @@ +/* + * 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, { Component, Fragment } from 'react'; +import { + EuiButtonIcon, + EuiFormRow, + EuiPopover, + EuiRange, + EuiSwitch, +} from '@elastic/eui'; +import { VECTOR_STYLES } from '../vector_style_defaults'; +import { i18n } from '@kbn/i18n'; + +function getIsEnableToggleLabel(styleName) { + switch (styleName) { + case VECTOR_STYLES.FILL_COLOR: + case VECTOR_STYLES.LINE_COLOR: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel', { + defaultMessage: 'Calculate color ramp range from indices' + }); + case VECTOR_STYLES.LINE_WIDTH: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel', { + defaultMessage: 'Calculate border width range from indices' + }); + case VECTOR_STYLES.ICON_SIZE: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel', { + defaultMessage: 'Calculate symbol size range from indices' + }); + default: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel', { + defaultMessage: 'Calculate symbolization range from indices' + }); + } +} + +export class FieldMetaOptionsPopover extends Component { + + state = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + } + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + } + + _onIsEnabledChange = event => { + this.props.onChange({ + ...this.props.styleProperty.getFieldMetaOptions(), + isEnabled: event.target.checked, + }); + }; + + _onSigmaChange = event => { + this.props.onChange({ + ...this.props.styleProperty.getFieldMetaOptions(), + sigma: event.target.value, + }); + } + + _renderButton() { + return ( + + ); + } + + _renderContent() { + return ( + + + + + + + + + + ); + } + + render() { + if (!this.props.styleProperty.supportsFieldMeta()) { + return null; + } + + return ( + + {this._renderContent()} + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js index 0984b0189558d..b21577d214bb5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js @@ -6,27 +6,27 @@ import { i18n } from '@kbn/i18n'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; export function getVectorStyleLabel(styleName) { switch (styleName) { - case vectorStyles.FILL_COLOR: + case VECTOR_STYLES.FILL_COLOR: return i18n.translate('xpack.maps.styles.vector.fillColorLabel', { defaultMessage: 'Fill color' }); - case vectorStyles.LINE_COLOR: + case VECTOR_STYLES.LINE_COLOR: return i18n.translate('xpack.maps.styles.vector.borderColorLabel', { defaultMessage: 'Border color' }); - case vectorStyles.LINE_WIDTH: + case VECTOR_STYLES.LINE_WIDTH: return i18n.translate('xpack.maps.styles.vector.borderWidthLabel', { defaultMessage: 'Border width' }); - case vectorStyles.ICON_SIZE: + case VECTOR_STYLES.ICON_SIZE: return i18n.translate('xpack.maps.styles.vector.symbolSizeLabel', { defaultMessage: 'Symbol size' }); - case vectorStyles.ICON_ORIENTATION: + case VECTOR_STYLES.ICON_ORIENTATION: return i18n.translate('xpack.maps.styles.vector.orientationLabel', { defaultMessage: 'Symbol orientation' }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js index 35c7066b7fd0f..dc5098c4d6d4d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js @@ -81,18 +81,24 @@ export class StylePropertyLegendRow extends Component { } render() { - const { range, style } = this.props; if (this._excludeFromHeader()) { return null; } const header = style.renderHeader(); + + const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE)); + const minLabel = this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange ? `< ${min}` : min; + + const max = this._formatValue(_.get(range, 'max', EMPTY_VALUE)); + const maxLabel = this.props.style.isFieldMetaEnabled() && range && range.isMaxOutsideStdRange ? `> ${max}` : max; + return ( diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js index d1de8e0fe6b4a..9686214fec9fe 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component, Fragment } from 'react'; import { VectorStyle } from '../vector_style'; import { i18n } from '@kbn/i18n'; +import { FieldMetaOptionsPopover } from './field_meta_options_popover'; import { getVectorStyleLabel } from './get_vector_style_label'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiFormRow, EuiButtonToggle } from '@elastic/eui'; -export class StaticDynamicStyleRow extends React.Component { +export class StaticDynamicStyleRow extends Component { // Store previous options locally so when type is toggled, // previous style options can be used. prevStaticStyleOptions = this.props.defaultStaticStyleOptions; @@ -29,6 +30,17 @@ export class StaticDynamicStyleRow extends React.Component { return this.props.styleProperty.getOptions(); } + _onFieldMetaOptionsChange = fieldMetaOptions => { + const styleDescriptor = { + type: VectorStyle.STYLE_TYPE.DYNAMIC, + options: { + ...this._getStyleOptions(), + fieldMetaOptions + } + }; + this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor); + } + _onStaticStyleChange = options => { const styleDescriptor = { type: VectorStyle.STYLE_TYPE.STATIC, @@ -64,11 +76,17 @@ export class StaticDynamicStyleRow extends React.Component { if (this._isDynamic()) { const DynamicSelector = this.props.DynamicSelector; return ( - + + + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 3043d57c04037..d848b9274d071 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -22,7 +22,7 @@ import { SYMBOLIZE_AS_ICON } from '../vector_constants'; import { i18n } from '@kbn/i18n'; import { SYMBOL_OPTIONS } from '../symbol_utils'; -import { EuiSpacer, EuiButtonGroup } from '@elastic/eui'; +import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui'; export class VectorStyleEditor extends Component { state = { @@ -117,6 +117,14 @@ export class VectorStyleEditor extends Component { return [...this.state.dateFields, ...this.state.numberFields]; } + _handleSelectedFeatureChange = selectedFeature => { + this.setState({ selectedFeature }); + }; + + _onIsTimeAwareChange = event => { + this.props.onIsTimeAwareChange(event.target.checked); + }; + _renderFillColor() { return ( { - this.setState({ selectedFeature }); - }; - - render() { + _renderProperties() { const { supportedFeatures, selectedFeature } = this.state; if (!supportedFeatures) { @@ -302,4 +306,34 @@ export class VectorStyleEditor extends Component { ); } + + _renderIsTimeAwareSwitch() { + if (!this.props.showIsTimeAware) { + return null; + } + + return ( + + + + ); + } + + render() { + return ( + + {this._renderProperties()} + {this._renderIsTimeAwareSwitch()} + + ); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 4b4b853c274cb..d56db31d17067 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -50,7 +50,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { } isCustomColorRamp() { - return !!this._options.customColorRamp; + return this._options.useCustomColorRamp; } supportsFeatureState() { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js index fb4ffd8cce4b4..afbe924e1afb8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js @@ -7,14 +7,14 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { getComputedFieldName } from '../style_util'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; export class DynamicOrientationProperty extends DynamicStyleProperty { syncIconRotationWithMb(symbolLayerId, mbMap) { if (this._options.field && this._options.field.name) { - const targetName = getComputedFieldName(vectorStyles.ICON_ORIENTATION, this._options.field.name); + const targetName = getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this._options.field.name); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]); } else { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index bd011b27d81c8..b4e6cf7be1701 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -8,7 +8,7 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { getComputedFieldName } from '../style_util'; import { HALF_LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE, SMALL_MAKI_ICON_SIZE } from '../symbol_utils'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; import _ from 'lodash'; import { CircleIcon } from '../components/legend/circle_icon'; import React, { Fragment } from 'react'; @@ -55,7 +55,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { mbMap.setLayoutProperty(symbolLayerId, 'icon-image', `${symbolId}-${iconPixels}`); const halfIconPixels = iconPixels / 2; - const targetName = getComputedFieldName(vectorStyles.ICON_SIZE, this._options.field.name); + const targetName = getComputedFieldName(VECTOR_STYLES.ICON_SIZE, this._options.field.name); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-size', [ 'interpolate', @@ -112,9 +112,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty { renderHeader() { let icons; - if (this.getStyleName() === vectorStyles.LINE_WIDTH) { + if (this.getStyleName() === VECTOR_STYLES.LINE_WIDTH) { icons = getLineWidthIcons(); - } else if (this.getStyleName() === vectorStyles.ICON_SIZE) { + } else if (this.getStyleName() === VECTOR_STYLES.ICON_SIZE) { icons = getSymbolSizeIcons(); } else { return null; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index e87bcc12c99be..a72502f9f17fb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ - +import _ from 'lodash'; import { AbstractStyleProperty } from './style_property'; +import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { STYLE_TYPE } from '../../../../../common/constants'; export class DynamicStyleProperty extends AbstractStyleProperty { @@ -32,6 +33,22 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return this._field.getOrigin(); } + isFieldMetaEnabled() { + const fieldMetaOptions = this.getFieldMetaOptions(); + return this.supportsFieldMeta() && _.get(fieldMetaOptions, 'isEnabled', true); + } + + supportsFieldMeta() { + return this.isComplete() && this.isScaled() && this._field.supportsFieldMeta(); + } + + async getFieldMetaRequest() { + const fieldMetaOptions = this.getFieldMetaOptions(); + return this._field.getFieldMetaRequest({ + sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA), + }); + } + supportsFeatureState() { return true; } @@ -39,4 +56,8 @@ export class DynamicStyleProperty extends AbstractStyleProperty { isScaled() { return true; } + + getFieldMetaOptions() { + return _.get(this.getOptions(), 'fieldMetaOptions', {}); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js index 69caaca080138..699955fe6542a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ - export function getComputedFieldName(styleName, fieldName) { return `${getComputedFieldNamePrefix(fieldName)}__${styleName}`; } @@ -12,3 +11,19 @@ export function getComputedFieldName(styleName, fieldName) { export function getComputedFieldNamePrefix(fieldName) { return `__kbn__dynamic__${fieldName}`; } + +export function scaleValue(value, range) { + if (isNaN(value) || !range) { + return -1; //Nothing to scale, put outside scaled range + } + + if (range.delta === 0 || value >= range.max) { + return 1; //snap to end of scaled range + } + + if (value <= range.min) { + return 0; //snap to beginning of scaled range + } + + return (value - range.min) / range.delta; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js new file mode 100644 index 0000000000000..a25e3bf8684c9 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js @@ -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 { scaleValue } from './style_util'; + +describe('scaleValue', () => { + test('Should scale value between 0 and 1', () => { + expect(scaleValue(5, { min: 0, max: 10, delta: 10 })).toBe(0.5); + }); + + test('Should snap value less then range min to 0', () => { + expect(scaleValue(-1, { min: 0, max: 10, delta: 10 })).toBe(0); + }); + + test('Should snap value greater then range max to 1', () => { + expect(scaleValue(11, { min: 0, max: 10, delta: 10 })).toBe(1); + }); + + test('Should snap value to 1 when tere is not range delta', () => { + expect(scaleValue(10, { min: 10, max: 10, delta: 0 })).toBe(1); + }); + + test('Should put value as -1 when value is not provided', () => { + expect(scaleValue(undefined, { min: 0, max: 10, delta: 10 })).toBe(-1); + }); + + test('Should put value as -1 when range is not provided', () => { + expect(scaleValue(5, undefined)).toBe(-1); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 45a1636e5c033..53794f2043aad 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -7,15 +7,21 @@ import _ from 'lodash'; import React from 'react'; import { VectorStyleEditor } from './components/vector_style_editor'; -import { getDefaultProperties, vectorStyles } from './vector_style_defaults'; +import { getDefaultProperties, VECTOR_STYLES } from './vector_style_defaults'; import { AbstractStyle } from '../abstract_style'; -import { GEO_JSON_TYPE, FIELD_ORIGIN, STYLE_TYPE } from '../../../../common/constants'; +import { + GEO_JSON_TYPE, + FIELD_ORIGIN, + STYLE_TYPE, + SOURCE_META_ID_ORIGIN, + LAYER_STYLE_TYPE, +} from '../../../../common/constants'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from './vector_constants'; import { getMakiSymbolAnchor } from './symbol_utils'; -import { getComputedFieldName } from './style_util'; +import { getComputedFieldName, scaleValue } from './style_util'; import { StaticStyleProperty } from './properties/static_style_property'; import { DynamicStyleProperty } from './properties/dynamic_style_property'; import { DynamicSizeProperty } from './properties/dynamic_size_property'; @@ -31,12 +37,13 @@ const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]; export class VectorStyle extends AbstractStyle { - static type = 'VECTOR'; + static type = LAYER_STYLE_TYPE.VECTOR; static STYLE_TYPE = STYLE_TYPE; - static createDescriptor(properties = {}) { + static createDescriptor(properties = {}, isTimeAware = true) { return { type: VectorStyle.type, - properties: { ...getDefaultProperties(), ...properties } + properties: { ...getDefaultProperties(), ...properties }, + isTimeAware, }; } @@ -50,15 +57,15 @@ export class VectorStyle extends AbstractStyle { this._layer = layer; this._descriptor = { ...descriptor, - ...VectorStyle.createDescriptor(descriptor.properties), + ...VectorStyle.createDescriptor(descriptor.properties, descriptor.isTimeAware), }; - this._lineColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.LINE_COLOR], vectorStyles.LINE_COLOR); - this._fillColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.FILL_COLOR], vectorStyles.FILL_COLOR); - this._lineWidthStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.LINE_WIDTH], vectorStyles.LINE_WIDTH); - this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.ICON_SIZE], vectorStyles.ICON_SIZE); + this._lineColorStyleProperty = this._makeColorProperty(this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], VECTOR_STYLES.LINE_COLOR); + this._fillColorStyleProperty = this._makeColorProperty(this._descriptor.properties[VECTOR_STYLES.FILL_COLOR], VECTOR_STYLES.FILL_COLOR); + this._lineWidthStyleProperty = this._makeSizeProperty(this._descriptor.properties[VECTOR_STYLES.LINE_WIDTH], VECTOR_STYLES.LINE_WIDTH); + this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[VECTOR_STYLES.ICON_SIZE], VECTOR_STYLES.ICON_SIZE); // eslint-disable-next-line max-len - this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[vectorStyles.ICON_ORIENTATION], vectorStyles.ICON_ORIENTATION); + this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[VECTOR_STYLES.ICON_ORIENTATION], VECTOR_STYLES.ICON_ORIENTATION); } _getAllStyleProperties() { @@ -72,13 +79,22 @@ export class VectorStyle extends AbstractStyle { } renderEditor({ layer, onStyleDescriptorChange }) { - const styleProperties = { ...this.getRawProperties() }; + const rawProperties = this.getRawProperties(); const handlePropertyChange = (propertyName, settings) => { - styleProperties[propertyName] = settings;//override single property, but preserve the rest - const vectorStyleDescriptor = VectorStyle.createDescriptor(styleProperties); + rawProperties[propertyName] = settings;//override single property, but preserve the rest + const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, this.isTimeAware()); onStyleDescriptorChange(vectorStyleDescriptor); }; + const onIsTimeAwareChange = isTimeAware => { + const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, isTimeAware); + onStyleDescriptorChange(vectorStyleDescriptor); + }; + + const propertiesWithFieldMeta = this.getDynamicPropertiesArray().filter(dynamicStyleProp => { + return dynamicStyleProp.isFieldMetaEnabled(); + }); + return ( 0} /> ); } @@ -156,7 +175,7 @@ export class VectorStyle extends AbstractStyle { nextStyleDescriptor: VectorStyle.createDescriptor({ ...originalProperties, ...updatedProperties, - }) + }, this.isTimeAware()) }; } @@ -239,6 +258,10 @@ export class VectorStyle extends AbstractStyle { return fieldNames; } + isTimeAware() { + return this._descriptor.isTimeAware; + } + getRawProperties() { return this._descriptor.properties || {}; } @@ -277,7 +300,56 @@ export class VectorStyle extends AbstractStyle { } _getFieldRange = (fieldName) => { - return _.get(this._descriptor, ['__styleMeta', fieldName]); + const fieldRangeFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); + const dynamicProps = this.getDynamicPropertiesArray(); + const dynamicProp = dynamicProps.find(dynamicProp => { return fieldName === dynamicProp.getField().getName(); }); + + if (!dynamicProp || !dynamicProp.isFieldMetaEnabled()) { + return fieldRangeFromLocalFeatures; + } + + let dataRequestId; + if (dynamicProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { + dataRequestId = SOURCE_META_ID_ORIGIN; + } else { + const join = this._layer.getValidJoins().find(join => { + const matchingField = join.getRightJoinSource().getMetricFieldForName(fieldName); + return !!matchingField; + }); + if (join) { + dataRequestId = join.getSourceMetaDataRequestId(); + } + } + + if (!dataRequestId) { + return fieldRangeFromLocalFeatures; + } + + const styleMetaDataRequest = this._layer._findDataRequestForSource(dataRequestId); + if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { + return fieldRangeFromLocalFeatures; + } + + const data = styleMetaDataRequest.getData(); + const field = dynamicProp.getField(); + const realFieldName = field.getESDocFieldName ? field.getESDocFieldName() : field.getName(); + const stats = data[realFieldName]; + if (!stats) { + return fieldRangeFromLocalFeatures; + } + + const sigma = _.get(dynamicProp.getFieldMetaOptions(), 'sigma', 3); + const stdLowerBounds = stats.avg - (stats.std_deviation * sigma); + const stdUpperBounds = stats.avg + (stats.std_deviation * sigma); + const min = Math.max(stats.min, stdLowerBounds); + const max = Math.min(stats.max, stdUpperBounds); + return { + min, + max, + delta: max - min, + isMinOutsideStdRange: stats.min < stdLowerBounds, + isMaxOutsideStdRange: stats.max > stdUpperBounds, + }; } getIcon = () => { @@ -289,8 +361,8 @@ export class VectorStyle extends AbstractStyle { ); @@ -321,7 +393,7 @@ export class VectorStyle extends AbstractStyle { // To work around this limitation, some styling values must fall back to geojson property values. let supportsFeatureState; let isScaled; - if (styleProperty.getStyleName() === vectorStyles.ICON_SIZE + if (styleProperty.getStyleName() === VECTOR_STYLES.ICON_SIZE && this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON) { supportsFeatureState = false; isScaled = true; @@ -380,13 +452,7 @@ export class VectorStyle extends AbstractStyle { const value = parseFloat(feature.properties[name]); let styleValue; if (isScaled) { - if (isNaN(value) || !range) {//cannot scale - styleValue = -1;//put outside range - } else if (range.delta === 0) {//values are identical - styleValue = 1;//snap to end of color range - } else { - styleValue = (value - range.min) / range.delta; - } + styleValue = scaleValue(value, range); } else { if (isNaN(value)) { styleValue = 0; @@ -450,7 +516,6 @@ export class VectorStyle extends AbstractStyle { } _makeField(fieldDescriptor) { - if (!fieldDescriptor || !fieldDescriptor.name) { return null; } @@ -473,8 +538,6 @@ export class VectorStyle extends AbstractStyle { } else { throw new Error(`Unknown origin-type ${fieldDescriptor.origin}`); } - - } _makeSizeProperty(descriptor, styleName) { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index ea4228430d13d..b834fb842389e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -16,8 +16,9 @@ const DEFAULT_ICON = 'airfield'; export const DEFAULT_MIN_SIZE = 1; export const DEFAULT_MAX_SIZE = 64; +export const DEFAULT_SIGMA = 3; -export const vectorStyles = { +export const VECTOR_STYLES = { SYMBOL: 'symbol', FILL_COLOR: 'fillColor', LINE_COLOR: 'lineColor', @@ -29,7 +30,7 @@ export const vectorStyles = { export function getDefaultProperties(mapColors = []) { return { ...getDefaultStaticProperties(mapColors), - [vectorStyles.SYMBOL]: { + [VECTOR_STYLES.SYMBOL]: { options: { symbolizeAs: SYMBOLIZE_AS_CIRCLE, symbolId: DEFAULT_ICON, @@ -48,31 +49,31 @@ export function getDefaultStaticProperties(mapColors = []) { return { - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { color: nextFillColor, } }, - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { color: nextLineColor } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { size: 1 } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { size: DEFAULT_ICON_SIZE } }, - [vectorStyles.ICON_ORIENTATION]: { + [VECTOR_STYLES.ICON_ORIENTATION]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { orientation: 0 @@ -83,40 +84,60 @@ export function getDefaultStaticProperties(mapColors = []) { export function getDefaultDynamicProperties() { return { - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { minSize: DEFAULT_MIN_SIZE, maxSize: DEFAULT_MAX_SIZE, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { minSize: DEFAULT_MIN_SIZE, maxSize: DEFAULT_MAX_SIZE, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.ICON_ORIENTATION]: { + [VECTOR_STYLES.ICON_ORIENTATION]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js index 610c704b34ec6..557a2bf869987 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -128,3 +128,22 @@ export async function canSkipSourceUpdate({ source, prevDataRequest, nextMeta }) && !updateDueToPrecisionChange && !updateDueToSourceMetaChange; } + +export function canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }) { + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + const updateDueToFields = !_.isEqual(prevMeta.dynamicStyleFields, nextMeta.dynamicStyleFields); + + const updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + + const updateDueToIsTimeAware = nextMeta.isTimeAware !== prevMeta.isTimeAware; + const updateDueToTime = nextMeta.isTimeAware ? !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters) : false; + + return !updateDueToFields && !updateDueToSourceQuery && !updateDueToIsTimeAware && !updateDueToTime; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js index 77359a6def48f..24728f2ac95fd 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -126,7 +126,8 @@ describe('canSkipSourceUpdate', () => { applyGlobalQuery: prevApplyGlobalQuery, filters: prevFilters, query: prevQuery, - } + }, + data: {} }); it('can skip update when filter changes', async () => { @@ -210,7 +211,8 @@ describe('canSkipSourceUpdate', () => { applyGlobalQuery: prevApplyGlobalQuery, filters: prevFilters, query: prevQuery, - } + }, + data: {} }); it('can not skip update when filter changes', async () => { diff --git a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js b/x-pack/legacy/plugins/maps/public/layers/util/data_request.js index 95b82aa292884..12d57afbe1c87 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/data_request.js @@ -22,7 +22,7 @@ export class DataRequest { } getMeta() { - return _.get(this._descriptor, 'dataMeta', {}); + return this.hasData() ? _.get(this._descriptor, 'dataMeta', {}) : _.get(this._descriptor, 'dataMetaAtStart', {}); } hasData() { diff --git a/x-pack/legacy/plugins/license_management/server/routes/api/license/register_license_route.js b/x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js similarity index 56% rename from x-pack/legacy/plugins/license_management/server/routes/api/license/register_license_route.js rename to x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js index 8e66ff4891643..54d8794b1e3cf 100644 --- a/x-pack/legacy/plugins/license_management/server/routes/api/license/register_license_route.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { putLicense } from '../../../lib/license'; +import { METRIC_TYPE } from '../../../common/constants'; -export function registerLicenseRoute(router, xpackInfo) { - router.put('', (request) => { - return putLicense(request, xpackInfo); - }); +export function isMetricCountable(aggType) { + return [METRIC_TYPE.COUNT, METRIC_TYPE.SUM, METRIC_TYPE.UNIQUE_COUNT].includes(aggType); } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 57126bb7681b8..7e831115e6dba 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -12,16 +12,19 @@ import { InnerJoin } from './joins/inner_join'; import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, + SOURCE_META_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, - LAYER_TYPE + LAYER_TYPE, + FIELD_ORIGIN, + LAYER_STYLE_TYPE, } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; -import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { canSkipSourceUpdate, canSkipStyleMetaUpdate } from './util/can_skip_fetch'; import { assignFeatureIds } from './util/assign_feature_ids'; import { getFillFilterExpression, @@ -88,7 +91,7 @@ export class VectorLayer extends AbstractLayer { const joins = this.getValidJoins(); for (let i = 0; i < joins.length; i++) { - const joinDataRequest = this.getDataRequest(joins[i].getSourceId()); + const joinDataRequest = this.getDataRequest(joins[i].getSourceDataRequestId()); if (!joinDataRequest || !joinDataRequest.hasData()) { return false; } @@ -229,12 +232,10 @@ export class VectorLayer extends AbstractLayer { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } - - async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); - const sourceDataId = join.getSourceId(); + const sourceDataId = join.getSourceDataRequestId(); const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); const searchFilters = { ...dataFilters, @@ -287,6 +288,7 @@ export class VectorLayer extends AbstractLayer { async _syncJoins(syncContext) { const joinSyncs = this.getValidJoins().map(async join => { + await this._syncJoinStyleMeta(syncContext, join); return this._syncJoin({ join, ...syncContext }); }); @@ -350,7 +352,7 @@ export class VectorLayer extends AbstractLayer { startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { - const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); + const requestToken = Symbol(`layer-source-data:${this.getId()}`); const searchFilters = this._getSearchFilters(dataFilters); const prevDataRequest = this.getSourceDataRequest(); @@ -389,11 +391,89 @@ export class VectorLayer extends AbstractLayer { } } + async _syncSourceStyleMeta(syncContext) { + if (this._style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + return; + } + + return this._syncStyleMeta({ + source: this._source, + sourceQuery: this.getQuery(), + dataRequestId: SOURCE_META_ID_ORIGIN, + dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE && dynamicStyleProp.isFieldMetaEnabled(); + }), + ...syncContext + }); + } + + async _syncJoinStyleMeta(syncContext, join) { + const joinSource = join.getRightJoinSource(); + return this._syncStyleMeta({ + source: joinSource, + sourceQuery: joinSource.getWhereQuery(), + dataRequestId: join.getSourceMetaDataRequestId(), + dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { + const matchingField = joinSource.getMetricFieldForName(dynamicStyleProp.getField().getName()); + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN + && !!matchingField + && dynamicStyleProp.isFieldMetaEnabled(); + }), + ...syncContext + }); + } + + async _syncStyleMeta({ + source, + sourceQuery, + dataRequestId, + dynamicStyleProps, + dataFilters, + startLoading, + stopLoading, + onLoadError, + registerCancelCallback + }) { + + if (!source.isESSource() || dynamicStyleProps.length === 0) { + return; + } + + const dynamicStyleFields = dynamicStyleProps.map(dynamicStyleProp => { + return dynamicStyleProp.getField().getName(); + }); + + const nextMeta = { + dynamicStyleFields: _.uniq(dynamicStyleFields).sort(), + sourceQuery, + isTimeAware: this._style.isTimeAware() && await source.isTimeAware(), + timeFilters: dataFilters.timeFilters, + }; + const prevDataRequest = this._findDataRequestForSource(dataRequestId); + const canSkipFetch = canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }); + if (canSkipFetch) { + return; + } + + const requestToken = Symbol(`layer-${this.getId()}-style-meta`); + try { + startLoading(dataRequestId, requestToken, nextMeta); + const layerName = await this.getDisplayName(); + const styleMeta = await source.loadStylePropsMeta(layerName, this._style, dynamicStyleProps, registerCancelCallback, nextMeta); + stopLoading(dataRequestId, requestToken, styleMeta, nextMeta); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + onLoadError(dataRequestId, requestToken, error.message); + } + } + } + async syncData(syncContext) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } + await this._syncSourceStyleMeta(syncContext); const sourceResult = await this._syncSource(syncContext); if ( !sourceResult.featureCollection || diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 90e1e748492cb..9b42998c814fd 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -7,7 +7,10 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; import KbnServer, { Server } from 'src/legacy/server/kbn_server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { plugin } from './server/new_platform'; +import { CloudSetup } from '../../../plugins/cloud/server'; + import { MlInitializerContext, MlCoreSetup, @@ -80,13 +83,14 @@ export const ml = (kibana: any) => { http: mlHttpService, savedObjects: server.savedObjects, }; - + const { usageCollection, cloud } = kbnServer.newPlatform.setup.plugins; const plugins = { elasticsearch: server.plugins.elasticsearch, security: server.plugins.security, xpackMain: server.plugins.xpack_main, spaces: server.plugins.spaces, - usageCollection: kbnServer.newPlatform.setup.plugins.usageCollection, + usageCollection: usageCollection as UsageCollectionSetup, + cloud: cloud as CloudSetup, ml: this, }; diff --git a/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss index ea5c245d9e39f..25bf3597c3466 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss +++ b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss @@ -1,6 +1,3 @@ -@import '@elastic/eui/src/components/tool_tip/variables'; -@import '@elastic/eui/src/components/tool_tip/mixins'; - .mlChartTooltip { @include euiToolTipStyle('s'); @include euiFontSizeXS; @@ -50,5 +47,4 @@ &--hidden { opacity: 0; } - } diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx index aedb8b6d17d06..5fb3ab95e4ea0 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; -import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; +import { EuiSpacer, EuiInMemoryTable } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; import { i18n } from '@kbn/i18n'; @@ -35,11 +35,13 @@ export const JobMessages: FC = ({ messages, loading, error }) width: `${theme.euiSizeL}`, }, { + field: 'timestamp', name: i18n.translate('xpack.ml.jobMessages.timeLabel', { defaultMessage: 'Time', }), - render: (message: any) => formatDate(message.timestamp, TIME_FORMAT), + render: (timestamp: number) => formatDate(timestamp, TIME_FORMAT), width: '120px', + sortable: true, }, { field: 'node_name', @@ -57,13 +59,22 @@ export const JobMessages: FC = ({ messages, loading, error }) }, ]; + const defaultSorting = { + sort: { + field: 'timestamp', + direction: 'asc', + }, + }; + return ( <> - ('globalState'); globalState.fetch(); - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = $injector.get('indexPatterns'); const kibanaConfig = $injector.get('config'); const $route = $injector.get('$route'); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx index 66a2452bfdf96..1f4ad65bd1879 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx @@ -171,8 +171,6 @@ export const getColumns = ( truncateText: true, 'data-test-subj': 'mlAnalyticsTableColumnId', }, - // Description is not supported yet by API - /* { field: DataFrameAnalyticsListColumn.description, name: i18n.translate('xpack.ml.dataframe.analyticsList.description', { @@ -181,7 +179,6 @@ export const getColumns = ( sortable: true, truncateText: true, }, - */ { field: DataFrameAnalyticsListColumn.configSourceIndex, name: i18n.translate('xpack.ml.dataframe.analyticsList.sourceIndex', { @@ -240,7 +237,7 @@ export const getColumns = ( defaultMessage: 'Actions', }), actions, - width: isManagementTable === true ? '100px' : '200px', + width: isManagementTable === true ? '100px' : '150px', }, ]; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 098c239e4f6ba..ff7da8d67852f 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -105,8 +105,7 @@ export enum DataFrameAnalyticsListColumn { configDestIndex = 'config.dest.index', configSourceIndex = 'config.source.index', configCreateTime = 'config.create_time', - // Description attribute is not supported yet by API - // description = 'config.description', + description = 'config.description', id = 'id', } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 2ad81a05741c1..a650a867aea60 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -133,7 +133,11 @@ export const ExpandedRow: FC = ({ item }) => { } }, [jobIsCompleted]); - const stateValues = { ...item.stats }; + const stateValues: any = { ...item.stats }; + + if (item.config?.description) { + stateValues.description = item.config.description; + } delete stateValues.progress; const state: SectionConfig = { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx index 592b53dcecba0..49381e3b1c031 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx @@ -45,7 +45,7 @@ describe('Data Frame Analytics: ', () => { ); const euiFormRows = wrapper.find('EuiFormRow'); - expect(euiFormRows.length).toBe(7); + expect(euiFormRows.length).toBe(8); const row1 = euiFormRows.at(0); expect(row1.find('label').text()).toBe('Job type'); @@ -56,6 +56,6 @@ describe('Data Frame Analytics: ', () => { expect(options.at(2).props().value).toBe('regression'); const row2 = euiFormRows.at(1); - expect(row2.find('p').text()).toBe('Enable advanced editor'); + expect(row2.find('EuiSwitch').text()).toBe('Enable advanced editor'); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 951639856c02a..e2106e56e0346 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -36,6 +36,7 @@ import { import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; import { Messages } from './messages'; import { JobType } from './job_type'; +import { JobDescriptionInput } from './job_description'; import { mmlUnitInvalidErrorMessage } from '../../hooks/use_create_analytics_form/reducer'; // based on code used by `ui/index_patterns` internally @@ -70,6 +71,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta dependentVariable, dependentVariableFetchFail, dependentVariableOptions, + description, destinationIndex, destinationIndexNameEmpty, destinationIndexNameExists, @@ -322,6 +324,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta data-test-subj="mlAnalyticsCreateJobFlyoutJobIdInput" /> + >; +} + +export const JobDescriptionInput: FC = ({ description, setFormState }) => ( + + { + const value = e.target.value; + setFormState({ description: value }); + }} + data-test-subj="mlDFAnalyticsJobCreationJobDescription" + /> + +); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx index 8299ff53393bb..5d97ed6dfcd3d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx @@ -11,10 +11,10 @@ import ReactDOM from 'react-dom'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml', ['react']); -import { IndexPatterns } from 'ui/index_patterns'; import { I18nContext } from 'ui/i18n'; import { InjectorService } from '../../../../../common/types/angular'; import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; +import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; @@ -25,7 +25,7 @@ module.directive('mlDataFrameAnalyticsManagement', ($injector: InjectorService) scope: {}, restrict: 'E', link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = $injector.get('indexPatterns'); const kibanaConfig = $injector.get('config'); const $route = $injector.get('$route'); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 64bd3681afce9..d29291f8795af 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -46,6 +46,7 @@ export interface State { dependentVariable: DependentVariable; dependentVariableFetchFail: boolean; dependentVariableOptions: Array<{ label: DependentVariable }> | []; + description: string; destinationIndex: EsIndexName; destinationIndexNameExists: boolean; destinationIndexNameEmpty: boolean; @@ -89,6 +90,7 @@ export const getInitialState = (): State => ({ dependentVariable: '', dependentVariableFetchFail: false, dependentVariableOptions: [], + description: '', destinationIndex: '', destinationIndexNameExists: false, destinationIndexNameEmpty: true, @@ -131,6 +133,7 @@ export const getJobConfigFromFormState = ( formState: State['form'] ): DeepPartial => { const jobConfig: DeepPartial = { + description: formState.description, source: { // If a Kibana index patterns includes commas, we need to split // the into an array of indices to be in the correct format for diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss index 6137a21d0bd8d..39a87ece68ac9 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss @@ -41,12 +41,13 @@ background-color: #920000; } - .type-other, .unknown { + .type-other, + .unknown { background-color: #bfa180; } // Use euiPanel styling - @include euiPanel($selector: 'card-contents'); + @include euiPanel($selector: '.card-contents'); .card-contents { height: 378px; @@ -68,12 +69,16 @@ padding-bottom: 0px; } - .stat.min, .stat.max, .stat.median { + .stat.min, + .stat.max, + .stat.median { width: 30%; display: inline-block; } - .stat.min.value, .stat.max.value, .stat.median.value { + .stat.min.value, + .stat.max.value, + .stat.median.value { font-size: $euiFontSizeS; @include euiTextTruncate; } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 3776245d90c81..99e61d5937c1d 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -5,8 +5,8 @@ */ import React, { FC, Fragment } from 'react'; -import { IndexPatterns } from 'ui/index_patterns'; import { timefilter } from 'ui/timefilter'; +import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; import { KibanaConfigTypeFix } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; @@ -15,7 +15,7 @@ import { NavigationMenu } from '../../components/navigation_menu'; import { FileDataVisualizerView } from './components/file_datavisualizer_view/index'; export interface FileDataVisualizerPageProps { - indexPatterns: IndexPatterns; + indexPatterns: IndexPatternsContract; kibanaConfig: KibanaConfigTypeFix; } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx index 291e03a96e85f..7ca2db041da29 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx @@ -14,7 +14,6 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml', ['react']); import uiRoutes from 'ui/routes'; -import { IndexPatterns } from 'ui/index_patterns'; import { KibanaConfigTypeFix } from '../../contexts/kibana'; import { getFileDataVisualizerBreadcrumbs } from './breadcrumbs'; import { InjectorService } from '../../../../common/types/angular'; @@ -24,6 +23,7 @@ import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { loadIndexPatterns } from '../../util/index_utils'; import { FileDataVisualizerPage, FileDataVisualizerPageProps } from './file_datavisualizer'; +import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; const template = `
@@ -47,7 +47,7 @@ module.directive('fileDatavisualizerPage', function($injector: InjectorService) scope: {}, restrict: 'E', link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = $injector.get('indexPatterns'); const kibanaConfig = $injector.get('config'); const props: FileDataVisualizerPageProps = { diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss index ca7d8e3f31c58..b4fd521f21bec 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss @@ -2,7 +2,6 @@ height: 420px; width: 360px; - // Note the names of these styles need to match the type of the field they are displaying. .boolean { background-color: $euiColorVis5; @@ -36,13 +35,13 @@ background-color: $euiColorVis9; } - .type-other, .unknown { + .type-other, + .unknown { background-color: $euiColorVis6; } - // Use euiPanel styling - @include euiPanel($selector: 'mlFieldDataCard__content'); + @include euiPanel($selector: '.mlFieldDataCard__content'); .mlFieldDataCard__content { @include euiFontSizeS; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx index 58cd1c2c6fd0c..5de7cb6b71acb 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx @@ -12,7 +12,7 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml', ['react']); import { I18nContext } from 'ui/i18n'; -import { IndexPatterns } from 'ui/index_patterns'; +import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; import { InjectorService } from '../../../../common/types/angular'; import { KibanaConfigTypeFix, KibanaContext } from '../../contexts/kibana/kibana_context'; @@ -25,7 +25,7 @@ module.directive('mlDataVisualizer', ($injector: InjectorService) => { scope: {}, restrict: 'E', link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = $injector.get('indexPatterns'); const kibanaConfig = $injector.get('config'); const $route = $injector.get('$route'); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap index 6e768cc301852..d98dcb26ee238 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap @@ -40,6 +40,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe > +
- +
`; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx index a7308514de182..98acb1cfa8045 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx @@ -228,6 +228,7 @@ export const CustomUrlEditor: FC = ({ onChange={onLabelChange} isInvalid={isInvalidLabel} compressed + data-test-subj="mlJobCustomUrlLabelInput" /> = ({ job, customUrls, setCust : []; return ( - + = ({ job, customUrls, setCust value={label} isInvalid={isInvalidLabel} onChange={e => onLabelChange(e, index)} + data-test-subj={`mlJobEditCustomUrlLabelInput_${index}`} /> @@ -266,5 +267,5 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust ); }); - return <>{customUrlRows}; + return
{customUrlRows}
; }; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index ed2c880c1b65f..c36b4ceed7d57 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -223,7 +223,11 @@ export class CustomUrls extends Component { : true; const addButton = ( - + { ) : ( - + { <> {(!editorOpen || editMode === 'modal') && ( - + = ({ additionalExpanded, setAdditional buttonContent={ButtonContent} onToggle={setAdditionalExpanded} initialIsOpen={additionalExpanded} + data-test-subj="mlJobWizardToggleAdditionalSettingsSection" > - +
+ - - - - - + + + + + - - - - - - + + + + + + +
); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index c441d1fe6270c..919972186761a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -67,7 +67,7 @@ export const CalendarsSelection: FC = () => { - + { return { @@ -29,7 +29,7 @@ module.directive('mlJobTypePage', ($injector: InjectorService) => { timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = $injector.get('indexPatterns'); const kibanaConfig = $injector.get('config'); const $route = $injector.get('$route'); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx index db4078ba1bbc8..d152dfc488ff8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx @@ -11,9 +11,9 @@ import ReactDOM from 'react-dom'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml', ['react']); import { timefilter } from 'ui/timefilter'; -import { IndexPatterns } from 'ui/index_patterns'; - import { I18nContext } from 'ui/i18n'; +import { IndexPatternsContract } from '../../../../../../../../../../src/plugins/data/public'; + import { InjectorService } from '../../../../../../common/types/angular'; import { createSearchItems } from '../../utils/new_job_utils'; import { Page, PageProps } from './page'; @@ -29,7 +29,7 @@ module.directive('mlNewJobPage', ($injector: InjectorService) => { timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = $injector.get('indexPatterns'); const kibanaConfig = $injector.get('config'); const $route = $injector.get('$route'); const existingJobsAndGroups = $route.current.locals.existingJobsAndGroups; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx index 2d08a1da07459..4ed12dfff4c20 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx @@ -11,7 +11,6 @@ import ReactDOM from 'react-dom'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml', ['react']); import { timefilter } from 'ui/timefilter'; -import { IndexPatterns } from 'ui/index_patterns'; import { I18nContext } from 'ui/i18n'; import { InjectorService } from '../../../../../common/types/angular'; @@ -20,6 +19,7 @@ import { createSearchItems } from '../utils/new_job_utils'; import { Page } from './page'; import { KibanaContext, KibanaConfigTypeFix } from '../../../contexts/kibana'; +import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; module.directive('mlRecognizePage', ($injector: InjectorService) => { return { @@ -30,7 +30,7 @@ module.directive('mlRecognizePage', ($injector: InjectorService) => { timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = $injector.get('indexPatterns'); const kibanaConfig = $injector.get('config'); const $route = $injector.get('$route'); diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx index 156e53b19874f..ff81f0e87aca8 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx @@ -26,6 +26,8 @@ import { import { AnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions'; import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; +const MlInMemoryTable = mlInMemoryTableFactory(); + interface Props { items: DataFrameAnalyticsListRow[]; } @@ -113,8 +115,6 @@ export const AnalyticsTable: FC = ({ items }) => { }, }; - const MlInMemoryTable = mlInMemoryTableFactory(); - return ( (); + // Used to pass on attribute names to table columns export enum AnomalyDetectionListColumns { id = 'id', @@ -195,8 +197,6 @@ export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData }, }; - const MlInMemoryTable = mlInMemoryTableFactory(); - return ( diff --git a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts index aeec71462308e..f79515c80556a 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; import { SavedSearchLoader } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { @@ -15,13 +14,17 @@ import { NewJobCaps, EVENT_RATE_FIELD_ID, } from '../../../common/types/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { + ES_FIELD_TYPES, + IndexPattern, + IndexPatternsContract, +} from '../../../../../../../src/plugins/data/public'; import { ml } from './ml_api_service'; // called in the angular routing resolve block to initialize the // newJobCapsService with the currently selected index pattern export function loadNewJobCapabilities( - indexPatterns: IndexPatterns, + indexPatterns: IndexPatternsContract, savedSearches: SavedSearchLoader, $route: Record ) { diff --git a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts index f25821e8ca1ca..99882b0243be8 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts @@ -6,19 +6,19 @@ import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { SavedSearchLoader } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; -import { start as data } from '../../../../../../../src/legacy/core_plugins/data/public/legacy'; +import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; type IndexPatternSavedObject = SimpleSavedObject; let indexPatternCache: IndexPatternSavedObject[] = []; -let fullIndexPatterns: IndexPatterns | null = null; +let fullIndexPatterns: IndexPatternsContract | null = null; export function loadIndexPatterns() { - fullIndexPatterns = data.indexPatterns.indexPatterns; + fullIndexPatterns = npStart.plugins.data.indexPatterns; const savedObjectsClient = chrome.getSavedObjectsClient(); return savedObjectsClient .find({ @@ -49,7 +49,10 @@ export function getIndexPatternIdFromName(name: string) { return null; } -export function loadCurrentIndexPattern(indexPatterns: IndexPatterns, $route: Record) { +export function loadCurrentIndexPattern( + indexPatterns: IndexPatternsContract, + $route: Record +) { fullIndexPatterns = indexPatterns; return fullIndexPatterns.get($route.current.params.index); } diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss index c3216773c1a32..ac3f3fef97c70 100644 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ b/x-pack/legacy/plugins/ml/public/index.scss @@ -1,10 +1,6 @@ // Should import both the EUI constants and any Kibana ones that are considered global @import 'src/legacy/ui/public/styles/styling_constants'; -// ML needs EUI card styling till it fully adopts React components -@import '@elastic/eui/src/components/panel/variables'; -@import '@elastic/eui/src/components/panel/mixins'; - // ML has it's own variables for coloring @import 'application/variables'; @@ -16,7 +12,6 @@ // Protect the rest of Kibana from ML generic namespacing // SASSTODO: Prefix ml selectors instead #ml-app { - // App level @import 'application/app'; diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts index 7a9766f36a6ed..293480b2aa5dc 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts @@ -16,9 +16,13 @@ import { import { UsageInitialization } from '../../new_platform/plugin'; export function makeMlUsageCollector( - usageCollection: UsageCollectionSetup, + usageCollection: UsageCollectionSetup | undefined, { elasticsearchPlugin, savedObjects }: UsageInitialization ): void { + if (!usageCollection) { + return; + } + const mlUsageCollector = usageCollection.makeUsageCollector({ type: 'ml', isReady: () => true, diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 3b666cad7d8eb..16328a9cd5e93 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -90,7 +90,7 @@ export function jobAuditMessagesProvider(callWithRequest) { body: { sort: [ - { timestamp: { order: 'asc' } }, + { timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } } ], query diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index b789121beebfc..727d05605614f 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -11,6 +11,7 @@ import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_s import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CloudSetup } from '../../../../../plugins/cloud/server'; import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; import { checkLicense } from '../lib/check_license'; @@ -79,7 +80,8 @@ export interface PluginsSetup { xpackMain: MlXpackMainPlugin; security: any; spaces: any; - usageCollection: UsageCollectionSetup; + usageCollection?: UsageCollectionSetup; + cloud?: CloudSetup; // TODO: this is temporary for `mirrorPluginStatus` ml: any; } @@ -91,6 +93,7 @@ export interface RouteInitialization { xpackMainPlugin?: MlXpackMainPlugin; savedObjects?: SavedObjectsLegacyService; spacesPlugin: any; + cloud?: CloudSetup; } export interface UsageInitialization { elasticsearchPlugin: ElasticsearchPlugin; @@ -190,6 +193,7 @@ export class Plugin { xpackMainPlugin: plugins.xpackMain, savedObjects: core.savedObjects, spacesPlugin: plugins.spaces, + cloud: plugins.cloud, }; const usageInitializationDeps: UsageInitialization = { elasticsearchPlugin: plugins.elasticsearch, diff --git a/x-pack/legacy/plugins/ml/server/routes/system.js b/x-pack/legacy/plugins/ml/server/routes/system.js index a686971672c58..60ea6a10827e2 100644 --- a/x-pack/legacy/plugins/ml/server/routes/system.js +++ b/x-pack/legacy/plugins/ml/server/routes/system.js @@ -21,10 +21,10 @@ import { isSecurityDisabled } from '../lib/security_utils'; export function systemRoutes({ commonRouteConfig, elasticsearchPlugin, - config, route, xpackMainPlugin, - spacesPlugin + spacesPlugin, + cloud, }) { const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); @@ -174,8 +174,7 @@ export function systemRoutes({ try { const info = await callWithRequest('ml.info'); - const cloudIdKey = 'xpack.cloud.id'; - const cloudId = config.has(cloudIdKey) && config.get(cloudIdKey); + const cloudId = cloud && cloud.cloudId; return { ...info, cloudId }; } catch (error) { return wrapError(error); diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js b/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js index f2f27499d113c..e9de0da7aabbd 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js +++ b/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js @@ -43,7 +43,7 @@ function NoDataMessage(props) { export function NoData(props) { const [isLoading, setIsLoading] = useState(false); - const [useInternalCollection, setUseInternalCollection] = useState(props.isOnCloud); + const [useInternalCollection, setUseInternalCollection] = useState(props.isCloudEnabled); async function startSetup() { setIsLoading(true); @@ -64,7 +64,7 @@ export function NoData(props) { - { !props.isOnCloud ? ( + { !props.isCloudEnabled ? ( setUseInternalCollection(false)}> diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js index 239c6e3fd775a..41aae01307617 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js @@ -12,6 +12,7 @@ import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; +import { npSetup } from 'ui/new_platform'; function isOnPage(hash) { return contains(window.location.hash, hash); @@ -82,10 +83,10 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - - const isCloud = chrome.getInjected('isOnCloud'); + const { cloud } = npSetup.plugins; + const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const hasPermissions = get(data, '_meta.hasPermissions', false); - if (isCloud || !hasPermissions) { + if (isCloudEnabled || !hasPermissions) { let text = null; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { @@ -163,7 +164,9 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const enabled = !globalState.inSetupMode && !chrome.getInjected('isOnCloud'); + const { cloud } = npSetup.plugins; + const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); + const enabled = !globalState.inSetupMode && !isCloudEnabled; render( , diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js index 1a9fdfeb920da..70313d45d096b 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -69,12 +69,15 @@ function setModules() { describe('setup_mode', () => { beforeEach(async () => { - jest.doMock('ui/chrome', () => ({ - getInjected: key => { - if (key === 'isOnCloud') { - return false; + jest.doMock('ui/new_platform', () => ({ + npSetup: { + plugins: { + cloud: { + cloudId: undefined, + isCloudEnabled: false, + } } - }, + } })); setModules(); }); @@ -122,12 +125,15 @@ describe('setup_mode', () => { it('should not fetch data if on cloud', async done => { const addDanger = jest.fn(); - jest.doMock('ui/chrome', () => ({ - getInjected: key => { - if (key === 'isOnCloud') { - return true; + jest.doMock('ui/new_platform', () => ({ + npSetup: { + plugins: { + cloud: { + cloudId: 'test', + isCloudEnabled: true, + } } - }, + } })); data = { _meta: { diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js b/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js index f364b70fd934f..9634f9872dd59 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js +++ b/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js @@ -5,7 +5,6 @@ */ import React from 'react'; -import chrome from 'ui/chrome'; import { ClusterSettingsChecker, NodeSettingsChecker, @@ -18,6 +17,7 @@ import { I18nContext } from 'ui/i18n'; import { CODE_PATH_LICENSE } from '../../../common/constants'; import { MonitoringViewBaseController } from '../base_controller'; import { i18n } from '@kbn/i18n'; +import { npSetup } from 'ui/new_platform'; export class NoDataController extends MonitoringViewBaseController { @@ -97,6 +97,8 @@ export class NoDataController extends MonitoringViewBaseController { render(enabler) { const props = this; + const { cloud } = npSetup.plugins; + const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); this.renderReact( @@ -104,7 +106,7 @@ export class NoDataController extends MonitoringViewBaseController { {...props} enabler={enabler} changePath={this.changePath} - isOnCloud={chrome.getInjected('isOnCloud')} + isCloudEnabled={isCloudEnabled} /> ); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/cluster.js b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/cluster.js index 768cba0e3e350..2db348f2a7751 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/cluster.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/cluster.js @@ -21,6 +21,15 @@ describe('Elasticsearch Cluster Settings', () => { const getReq = response => { return { server: { + newPlatform: { + setup: { + plugins: { + cloud: { + isCloudEnabled: false, + } + } + } + }, plugins: { elasticsearch: { getCluster() { @@ -49,7 +58,7 @@ describe('Elasticsearch Cluster Settings', () => { reason: { context: `cluster ${source}`, data: '-1', - isCloud: false, + isCloudEnabled: false, property: 'xpack.monitoring.collection.interval' } }); @@ -79,7 +88,7 @@ describe('Elasticsearch Cluster Settings', () => { reason: { context: `cluster ${source}`, data: 'Remote exporters indicate a possible misconfiguration: myCoolExporter', - isCloud: false, + isCloudEnabled: false, property: 'xpack.monitoring.exporters' } }); @@ -109,7 +118,7 @@ describe('Elasticsearch Cluster Settings', () => { reason: { context: `cluster ${source}`, data: 'false', - isCloud: false, + isCloudEnabled: false, property: 'xpack.monitoring.enabled' } }); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js index e8c508ecd5f97..596195d7ae915 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js @@ -7,14 +7,14 @@ import { get } from 'lodash'; import { findReason } from './find_reason'; -export function handleResponse(response, isCloud) { +export function handleResponse(response, isCloudEnabled) { const sources = ['persistent', 'transient', 'defaults']; for (const source of sources) { const monitoringSettings = get(response[source], 'xpack.monitoring'); if (monitoringSettings !== undefined) { const check = findReason(monitoringSettings, { context: `cluster ${source}`, - isCloud: isCloud + isCloudEnabled, }); if (check.found) { @@ -28,7 +28,8 @@ export function handleResponse(response, isCloud) { export async function checkClusterSettings(req) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); - const isCloud = get(req.server.plugins, 'cloud.config.isCloudEnabled', false); + const { cloud } = req.server.newPlatform.setup.plugins; + const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const response = await callWithRequest(req, 'transport.request', { method: 'GET', path: '/_cluster/settings?include_defaults', @@ -39,5 +40,5 @@ export async function checkClusterSettings(req) { ] }); - return handleResponse(response, isCloud); + return handleResponse(response, isCloudEnabled); } diff --git a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.js b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.js index 07c8402b0da7a..f5aac00d8b0a5 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.js @@ -14,7 +14,7 @@ const isEnabledOrDefault = property => { return property === undefined || (Boolean(property) && property !== 'false'); }; -export function findReason(settingsSource, context, isCloud) { +export function findReason(settingsSource, context, isCloudEnabled) { const iterateReasons = () => { // PluginEnabled: check for `monitoring.enabled: false` const monitoringEnabled = get(settingsSource, 'enabled'); @@ -91,7 +91,7 @@ export function findReason(settingsSource, context, isCloud) { }); if (allEnabledRemote.length > 0 && allEnabledLocal.length === 0) { let ret = {}; - if (isCloud) { + if (isCloudEnabled) { ret = { found: true, reason: { diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js index 0976292be576b..f8a8c9ff51213 100644 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ b/x-pack/legacy/plugins/monitoring/ui_exports.js @@ -5,7 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; import { resolve } from 'path'; /** @@ -30,7 +29,6 @@ export const getUiExports = () => ({ const config = server.config(); return { monitoringUiEnabled: config.get('xpack.monitoring.ui.enabled'), - isOnCloud: get(server.plugins, 'cloud.config.isCloudEnabled', false) }; }, hacks: [ 'plugins/monitoring/hacks/toggle_app_link_in_nav' ], diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index bf309c65556a8..6ebc43bb92737 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/legacy/plugins/remote_clusters/public/app/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -40,6 +40,7 @@ Array [
-

Skip if unavailable -

+
@@ -367,6 +370,7 @@ Array [
-

Skip if unavailable -

+
, diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss index d36a587b9257f..5c35e9a23b8a1 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss @@ -1,5 +1,4 @@ @import '@elastic/eui/src/components/header/variables'; -@import '@elastic/eui/src/components/panel/mixins'; @import 'mixins'; @@ -16,7 +15,6 @@ } } - .prfDevTool__page { height: 100%; display: flex; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_main.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_main.scss index 09bcddef02cc3..6e2ef4a129397 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_main.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/containers/_main.scss @@ -1,4 +1,3 @@ - @include euiBreakpoint('xs', 's') { .prfDevTool__shardDetailsWrapper { flex-direction: column; @@ -34,7 +33,8 @@ font-size: $euiSize; color: $euiColorMediumShade; } - h1, p { + h1, + p { cursor: default; user-select: none; } @@ -44,7 +44,7 @@ } } -@include euiPanel('prfDevTool__main'); +@include euiPanel('.prfDevTool__main'); @include euiBreakpoint('xs', 's') { .prfDevTool__container { @@ -59,4 +59,3 @@ margin: $euiSize 0; } } - diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index fc83dc64aafab..1d798a4a2bc40 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -110,7 +110,8 @@ export const security = (kibana) => new kibana.Plugin({ } watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => { - if (securityPlugin.__legacyCompat.license.getFeatures().allowRbac) { + const xpackInfo = server.plugins.xpack_main.info; + if (xpackInfo.isAvailable() && xpackInfo.feature('security').isEnabled()) { await securityPlugin.__legacyCompat.registerPrivilegesWithCluster(); } }); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js index 24e304b0010d0..1d93b9251bbd6 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js @@ -9,11 +9,11 @@ import routes from 'ui/routes'; import { capabilities } from 'ui/capabilities'; import { kfetch } from 'ui/kfetch'; import { fatalError, toastNotifications } from 'ui/notify'; +import { npStart } from 'ui/new_platform'; import template from 'plugins/security/views/management/edit_role/edit_role.html'; import 'plugins/security/services/shield_user'; import 'plugins/security/services/shield_role'; import 'plugins/security/services/shield_indices'; -import { start as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { SpacesManager } from '../../../../../spaces/public/lib'; import { ROLES_PATH, CLONE_ROLES_PATH, EDIT_ROLES_PATH } from '../management_urls'; @@ -75,8 +75,7 @@ const routeDefinition = (action) => ({ .then(users => _.map(users, 'username')); }, indexPatterns() { - const { indexPatterns } = data.indexPatterns; - return indexPatterns.getTitles(); + return npStart.plugins.data.indexPatterns.getTitles(); }, spaces(spacesEnabled) { if (spacesEnabled) { diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index e5d1fc83dac26..0924b6c6eb5e6 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -15,7 +15,11 @@ export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; + +// DEPRECATED: THIS WILL BE REMOVED VERY SOON AND IS NO LONGER USED ON THE BACKEND +// TODO: Remove this as soon as no code is left that is pulling data from it. export const DEFAULT_SIGNALS_INDEX_KEY = 'siem:defaultSignalsIndex'; + export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; export const DEFAULT_MAX_SIGNALS = 100; export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; @@ -32,12 +36,20 @@ export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; /** - * Id for the SIGNALS alerting type + * Id for the signals alerting type */ export const SIGNALS_ID = `${APP_ID}.signals`; /** - * Detection engine route + * Detection engine routes */ export const DETECTION_ENGINE_URL = '/api/detection_engine'; export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`; +export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; + +/** + * Default signals index key for kibana.dev.yml + */ +export const SIGNALS_INDEX_KEY = 'signalsIndex'; +export const DETECTION_ENGINE_SIGNALS_URL = `${DETECTION_ENGINE_URL}/signals`; +export const DETECTION_ENGINE_SIGNALS_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/status`; diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 72b4ec588a5a4..fca4a13db8cb5 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; import { Server } from 'hapi'; +import { Root } from 'joi'; import { PluginInitializerContext } from 'src/core/server'; import { plugin } from './server'; @@ -24,6 +25,7 @@ import { DEFAULT_FROM, DEFAULT_TO, DEFAULT_SIGNALS_INDEX, + SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX_KEY, } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; @@ -103,6 +105,8 @@ export const siem = (kibana: any) => { category: ['siem'], requiresPageReload: true, }, + // DEPRECATED: This should be removed once the front end is no longer using any parts of it. + // TODO: Remove this as soon as no code is left that is pulling data from it. [DEFAULT_SIGNALS_INDEX_KEY]: { name: i18n.translate('xpack.siem.uiSettings.defaultSignalsIndexLabel', { defaultMessage: 'Elasticsearch signals index', @@ -155,7 +159,11 @@ export const siem = (kibana: any) => { getInjectedUiAppVars, indexPatternsServiceFactory, injectUiAppVars, - plugins: { alerting: plugins.alerting, xpack_main: plugins.xpack_main }, + plugins: { + alerting: plugins.alerting, + xpack_main: plugins.xpack_main, + spaces: plugins.spaces, + }, route: route.bind(server), savedObjects, }; @@ -166,5 +174,13 @@ export const siem = (kibana: any) => { serverFacade ); }, + config(Joi: Root) { + return Joi.object() + .keys({ + enabled: Joi.boolean().default(true), + [SIGNALS_INDEX_KEY]: Joi.string().default(DEFAULT_SIGNALS_INDEX), + }) + .default(); + }, }); }; diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx index 3548fb7c0e671..be449e3d422d9 100644 --- a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx @@ -6,7 +6,6 @@ import { EuiBadge } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import * as i18n from './translations'; @@ -39,7 +38,7 @@ export type AndOr = 'and' | 'or'; /** Displays AND / OR in a round badge */ // Ref: https://github.com/elastic/eui/issues/1655 -export const AndOrBadge = pure<{ type: AndOr }>(({ type }) => { +export const AndOrBadge = React.memo<{ type: AndOr }>(({ type }) => { return ( {type === 'and' ? i18n.AND : i18n.OR} diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap index 408bcac756f47..7702695520790 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap @@ -1,9 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`arrows ArrowBody renders correctly against snapshot 1`] = ` - - + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx index 8be0e7c267ec0..10d3c899562e8 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -15,12 +15,12 @@ import { ArrowBody, ArrowHead } from '.'; describe('arrows', () => { describe('ArrowBody', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow( + const wrapper = mount( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('ArrowBody'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx b/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx index 6d5b464e0e886..dfc7645c564d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx @@ -6,7 +6,6 @@ import { EuiIcon } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; /** Renders the body (non-pointy part) of an arrow */ @@ -21,7 +20,7 @@ ArrowBody.displayName = 'ArrowBody'; export type ArrowDirection = 'arrowLeft' | 'arrowRight'; /** Renders the head of an arrow */ -export const ArrowHead = pure<{ +export const ArrowHead = React.memo<{ direction: ArrowDirection; }>(({ direction }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx index 37ec256ccd8c0..f8db7d754aab1 100644 --- a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx @@ -6,7 +6,6 @@ import { EuiText } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DraggableBadge } from '../draggables'; @@ -36,7 +35,7 @@ FingerprintLabel.displayName = 'FingerprintLabel'; * 'tls.client_certificate.fingerprint.sha1' * 'tls.server_certificate.fingerprint.sha1' */ -export const CertificateFingerprint = pure<{ +export const CertificateFingerprint = React.memo<{ eventId: string; certificateType: CertificateType; contextId: string; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index 7218d7a497f19..99ad995e48852 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; - import { Chart, BarSeries, @@ -63,6 +62,7 @@ export const BarChartBaseComponent = ({ ...chartDefaultSettings, ...get('configs.settings', chartConfigs), }; + return chartConfigs.width && chartConfigs.height ? ( @@ -116,6 +116,7 @@ export const BarChartComponent = ({ }) => { const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); + return checkIfAnyValidSeriesExist(barChart) ? ( {({ measureRef, content: { height, width } }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap index 03a04983f9f86..f082dc4023e7a 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap @@ -1,32 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBar it renders 1`] = ` - - - - - - Test text - - - - - Test action - - - - - - - Test action - - - - - + + + + + Test text + + + + + Test action + + + + + + + Test action + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap index 470b40cd1d960..eb20ac217b300 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap @@ -1,11 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarAction it renders 1`] = ` - - - Test action - - + + Test action + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap index 62ff1b17dd55f..8ef7ee1cfe842 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap @@ -1,11 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarGroup it renders 1`] = ` - - - - Test text - - - + + + Test text + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap index f81717c892755..2fe3b8ac5c7aa 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap @@ -1,13 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarSection it renders 1`] = ` - - - - - Test text - - - - + + + + Test text + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap index 446b5556945d8..cf635ffa49c4c 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarText it renders 1`] = ` - - - Test text - - + + Test text + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx index 27688ec24530e..68522377bd847 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx @@ -47,7 +47,7 @@ describe('UtilityBar', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBar'))).toMatchSnapshot(); }); test('it applies border styles when border is true', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx index f226e0e055391..524769361ea9d 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx @@ -8,11 +8,12 @@ import React from 'react'; import { Bar, BarProps } from './styles'; -export interface UtilityBarProps extends BarProps { +interface UtilityBarProps extends BarProps { children: React.ReactNode; } export const UtilityBar = React.memo(({ border, children }) => ( {children} )); + UtilityBar.displayName = 'UtilityBar'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx index f71bdfda705d0..7921c1ef42200 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx @@ -22,7 +22,7 @@ describe('UtilityBarAction', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarAction'))).toMatchSnapshot(); }); test('it renders a popover', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx index 2ad48bc9b9c92..f695c33a37447 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx @@ -37,6 +37,7 @@ const Popover = React.memo( ); } ); + Popover.displayName = 'Popover'; export interface UtilityBarActionProps extends LinkIconProps { @@ -71,4 +72,5 @@ export const UtilityBarAction = React.memo( ) ); + UtilityBarAction.displayName = 'UtilityBarAction'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx index 84ad96c5a1e5e..294d27fa95b3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx @@ -24,6 +24,6 @@ describe('UtilityBarGroup', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarGroup'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx index 1e23fd3498199..723035df672a9 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx @@ -15,4 +15,5 @@ export interface UtilityBarGroupProps { export const UtilityBarGroup = React.memo(({ children }) => ( {children} )); + UtilityBarGroup.displayName = 'UtilityBarGroup'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx index 2dfc1d3b8d193..e0e0acc3a71c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx @@ -26,6 +26,6 @@ describe('UtilityBarSection', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarSection'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx index c457e6bc3dee0..42532c0355607 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx @@ -15,4 +15,5 @@ export interface UtilityBarSectionProps { export const UtilityBarSection = React.memo(({ children }) => ( {children} )); + UtilityBarSection.displayName = 'UtilityBarSection'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx index 0743e5cab02b4..29e1844bb2d4f 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx @@ -22,6 +22,6 @@ describe('UtilityBarText', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarText'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx index f8eb25f03d4ad..6195e008dbe27 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx @@ -15,4 +15,5 @@ export interface UtilityBarTextProps { export const UtilityBarText = React.memo(({ children }) => ( {children} )); + UtilityBarText.displayName = 'UtilityBarText'; diff --git a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx b/x-pack/legacy/plugins/siem/public/components/direction/index.tsx index b5d6fcfc6cef7..9295e055f918d 100644 --- a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/direction/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { NetworkDirectionEcs } from '../../graphql/types'; import { DraggableBadge } from '../draggables'; @@ -56,7 +55,7 @@ export const getDirectionIcon = ( /** * Renders a badge containing the value of `network.direction` */ -export const DirectionBadge = pure<{ +export const DirectionBadge = React.memo<{ contextId: string; direction?: string | null; eventId: string; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap index 22c7b62711795..666a8249c27d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap @@ -1,426 +1,419 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] = ` - - - - Drag drop context wrapper children - - - + "client.bytes": Object { + "aggregatable": true, + "category": "client", + "description": "Bytes sent from the client to the server.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.bytes", + "searchable": true, + "type": "number", + }, + "client.domain": Object { + "aggregatable": true, + "category": "client", + "description": "Client domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.domain", + "searchable": true, + "type": "string", + }, + "client.geo.country_iso_code": Object { + "aggregatable": true, + "category": "client", + "description": "Country ISO code.", + "example": "CA", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.geo.country_iso_code", + "searchable": true, + "type": "string", + }, + }, + }, + "cloud": Object { + "fields": Object { + "cloud.account.id": Object { + "aggregatable": true, + "category": "cloud", + "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.", + "example": "666777888999", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.account.id", + "searchable": true, + "type": "string", + }, + "cloud.availability_zone": Object { + "aggregatable": true, + "category": "cloud", + "description": "Availability zone in which this host is running.", + "example": "us-east-1c", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.availability_zone", + "searchable": true, + "type": "string", + }, + }, + }, + "container": Object { + "fields": Object { + "container.id": Object { + "aggregatable": true, + "category": "container", + "description": "Unique container id.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.id", + "searchable": true, + "type": "string", + }, + "container.image.name": Object { + "aggregatable": true, + "category": "container", + "description": "Name of the image the container was built on.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.name", + "searchable": true, + "type": "string", + }, + "container.image.tag": Object { + "aggregatable": true, + "category": "container", + "description": "Container image tag.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.tag", + "searchable": true, + "type": "string", + }, + }, + }, + "destination": Object { + "fields": Object { + "destination.address": Object { + "aggregatable": true, + "category": "destination", + "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.address", + "searchable": true, + "type": "string", + }, + "destination.bytes": Object { + "aggregatable": true, + "category": "destination", + "description": "Bytes sent from the destination to the source.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.bytes", + "searchable": true, + "type": "number", + }, + "destination.domain": Object { + "aggregatable": true, + "category": "destination", + "description": "Destination domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.domain", + "searchable": true, + "type": "string", + }, + "destination.ip": Object { + "aggregatable": true, + "category": "destination", + "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.ip", + "searchable": true, + "type": "ip", + }, + "destination.port": Object { + "aggregatable": true, + "category": "destination", + "description": "Port of the destination.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.port", + "searchable": true, + "type": "long", + }, + }, + }, + "event": Object { + "fields": Object { + "event.end": Object { + "aggregatable": true, + "category": "event", + "description": "event.end contains the date when the event ended or when the activity was last observed.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat-*", + "endgame-*", + "filebeat-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.end", + "searchable": true, + "type": "date", + }, + }, + }, + "source": Object { + "fields": Object { + "source.ip": Object { + "aggregatable": true, + "category": "source", + "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.ip", + "searchable": true, + "type": "ip", + }, + "source.port": Object { + "aggregatable": true, + "category": "source", + "description": "Port of the source.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.port", + "searchable": true, + "type": "long", + }, + }, + }, + } + } +> + Drag drop context wrapper children + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap index a240d5122ac9c..aa8214938c2b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap @@ -1,443 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DraggableWrapper rendering it renders against the snapshot 1`] = ` - - - - - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap index 23a540f0ce3b3..7c6e321395fa5 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap @@ -1,430 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DroppableWrapper rendering it renders against the snapshot 1`] = ` - - - - - draggable wrapper content - - - - + + draggable wrapper content + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx index b8fba6fe2f6d8..1a8af9d99193a 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx @@ -28,7 +28,7 @@ describe('DragDropContextWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DragDropContextWrapper'))).toMatchSnapshot(); }); test('it renders the children', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index a3528158a0317..f9e6bfcf7c236 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -114,6 +114,8 @@ const mapStateToProps = (state: State) => { export const DragDropContextWrapper = connect(mapStateToProps)(DragDropContextWrapperComponent); +DragDropContextWrapper.displayName = 'DragDropContextWrapper'; + const onBeforeCapture = (before: BeforeCapture) => { const x = window.pageXOffset !== undefined diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index d9b78836b450e..008ece5c7e69c 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -30,7 +30,7 @@ describe('DraggableWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DraggableWrapper'))).toMatchSnapshot(); }); test('it renders the children passed to the render prop', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index c314785511201..809c46f7b53bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -275,6 +275,8 @@ export const DraggableWrapper = connect(null, { unRegisterProvider: dragAndDropActions.unRegisterProvider, })(DraggableWrapperComponent); +DraggableWrapper.displayName = 'DraggableWrapper'; + /** * Conditionally wraps children in an EuiPortal to ensure drag offsets are correct when dragging * from containers that have css transforms diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx index 859b30d2164dd..39abbdd4d4e38 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx @@ -30,7 +30,7 @@ describe('DroppableWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DroppableWrapper'))).toMatchSnapshot(); }); test('it renders the children when a render prop is not provided', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index 3f789a39832f1..2b013a665af16 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -7,7 +7,6 @@ import { rgba } from 'polished'; import * as React from 'react'; import { Droppable } from 'react-beautiful-dnd'; -import { pure } from 'recompose'; import styled from 'styled-components'; interface Props { @@ -87,7 +86,7 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string `; ReactDndDropTarget.displayName = 'ReactDndDropTarget'; -export const DroppableWrapper = pure( +export const DroppableWrapper = React.memo( ({ children = null, droppableId, diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap index 1e9e89ad66641..63ba13306ecd8 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap @@ -1,29 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`draggables rendering it renders the default Badge 1`] = ` - - - A child of this - - + + + A child of this + + + `; exports[`draggables rendering it renders the default DefaultDraggable 1`] = ` - - - A child of this - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx index 5bff59494b9ad..90d8ad463b476 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx @@ -6,7 +6,6 @@ import { rgba } from 'polished'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; const Field = styled.div` @@ -28,11 +27,12 @@ Field.displayName = 'Field'; // Passing the styles directly to the component because the width is // being calculated and is recommended by Styled Components for performance // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 -export const DraggableFieldBadge = pure<{ fieldId: string; fieldWidth?: string }>( +export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: string }>( ({ fieldId, fieldWidth }) => ( {fieldId} ) ); + DraggableFieldBadge.displayName = 'DraggableFieldBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx index fb49329ba1501..d3dcba9526bdd 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx @@ -108,19 +108,15 @@ describe('draggables', () => { }); test('it returns null if value is undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if value is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); @@ -218,31 +214,27 @@ describe('draggables', () => { }); test('it returns null if value is undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if value is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index 2f91cdc43b797..5b219dad9c841 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -6,7 +6,6 @@ import { EuiBadge, EuiBadgeProps, EuiToolTip, IconType } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; @@ -50,7 +49,7 @@ export const getDefaultWhenTooltipIsUnspecified = ({ /** * Renders the content of the draggable, wrapped in a tooltip */ -const Content = pure<{ +const Content = React.memo<{ children?: React.ReactNode; field: string; tooltipContent?: React.ReactNode; @@ -83,7 +82,7 @@ Content.displayName = 'Content'; * prevent a tooltip from being displayed, or pass arbitrary content * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ -export const DefaultDraggable = pure( +export const DefaultDraggable = React.memo( ({ id, field, value, name, children, tooltipContent, queryValue }) => value != null ? ( & { * prevent a tooltip from being displayed, or pass arbitrary content * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ -export const DraggableBadge = pure( +export const DraggableBadge = React.memo( ({ contextId, eventId, diff --git a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx b/x-pack/legacy/plugins/siem/public/components/duration/index.tsx index 06446a152bea8..15e6246f1f1ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/duration/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DefaultDraggable } from '../draggables'; import { FormattedDuration } from '../formatted_duration'; @@ -16,7 +15,7 @@ export const EVENT_DURATION_FIELD_NAME = 'event.duration'; * Renders draggable text containing the value of a field representing a * duration of time, (e.g. `event.duration`) */ -export const Duration = pure<{ +export const Duration = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index d7da585966758..ede0d3f394789 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -147,6 +147,10 @@ export const mockLineLayer = { }, minSize: 1, maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, }, }, iconSize: { type: 'STATIC', options: { size: 10 } }, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap index f343316d88c46..b03670b2b1cd4 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap @@ -1,11 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Embeddable it renders 1`] = ` - - +
+

Test content

- - +
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap index e88693b292a5d..6d02ccb1c6eb9 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap @@ -1,9 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EmbeddableHeader it renders 1`] = ` - - - +
+ + + +
+ Test title +
+
+
+
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx index 49f5306dc1b60..c0d70754e78bd 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx @@ -9,7 +9,6 @@ import toJson from 'enzyme-to-json'; import React from 'react'; import '../../mock/ui_settings'; -import { TestProviders } from '../../mock'; import { Embeddable } from './embeddable'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -17,11 +16,9 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('Embeddable', () => { test('it renders', () => { const wrapper = shallow( - - -

{'Test content'}

-
-
+ +

{'Test content'}

+
); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx index 4536da3ba7b97..6387de30aa265 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx @@ -16,11 +16,7 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('EmbeddableHeader', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts index fd17e6eaeac64..637251eb64f70 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts @@ -210,6 +210,10 @@ export const getLineLayer = (indexPatternTitle: string, indexPatternId: string) }, minSize: 1, maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, }, }, iconSize: { type: 'STATIC', options: { size: 10 } }, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap index 2ef4d9df89a1b..9d39b6e59365f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap @@ -1,18 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PointToolTipContent renders correctly against snapshot 1`] = ` - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 1733fb3aa7480..5e1eae1649b41 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -46,7 +46,7 @@ describe('PointToolTipContent', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('PointToolTipContentComponent'))).toMatchSnapshot(); }); test('renders array filter correctly', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap index 7e1da6ae7ace3..9b6bfb1752a20 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap @@ -1,9 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly 1`] = ` - + + + Do Something + + + + } + title={ +

+ My Super Title +

+ } /> `; diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx index 9c3dd462de153..ef2b76c9aad1c 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx @@ -6,7 +6,6 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, IconType } from '@elastic/eui'; import React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; const EmptyPrompt = styled(EuiEmptyPrompt)` @@ -29,7 +28,7 @@ interface EmptyPageProps { title: string; } -export const EmptyPage = pure( +export const EmptyPage = React.memo( ({ actionPrimaryIcon, actionPrimaryLabel, diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap index bfb10fc385c08..4cf7cbb43cdc7 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -1,692 +1,1544 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EventDetails rendering should match snapshot 1`] = ` - - + , + "id": "table-view", + "name": "Table", } } - columnHeaders={ + tabs={ Array [ Object { - "aggregatable": true, - "category": "base", - "columnHeaderType": "not-filtered", - "description": "Date/time when the event originated. + "content": , + "id": "table-view", + "name": "Table", }, Object { - "field": "destination.port", - "originalValue": 902, - "values": Array [ - "902", - ], + "content": , + "id": "json-view", + "name": "JSON View", }, ] } - id="Y-6TfmcB0WOhS6qyMv3s" - onUpdateColumns={[MockFunction]} - onViewSelected={[MockFunction]} - timelineId="test" - toggleColumn={[MockFunction]} - view="table-view" /> - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap index a788b60afd6b3..caa7853fd9ec0 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -1,150 +1,52 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`JSON View rendering should match snapshot 1`] = ` - + +}" + width="100%" + /> + `; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx index fb1f9f0cd4e64..d8c0e46d8480b 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx @@ -21,19 +21,17 @@ describe('EventDetails', () => { describe('rendering', () => { test('should match snapshot', () => { const wrapper = shallow( - - - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx index 05690a0d20d92..519f56adff2d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx @@ -7,7 +7,6 @@ import { EuiCodeEditor } from '@elastic/eui'; import { set } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DetailItem } from '../../graphql/types'; @@ -23,7 +22,7 @@ const JsonEditor = styled.div` JsonEditor.displayName = 'JsonEditor'; -export const JsonView = pure(({ data }) => ( +export const JsonView = React.memo(({ data }) => ( ({ uiSettings: mockUiSettings, })); +const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; +jest.mock('../../containers/detection_engine/rules/fetch_index_patterns'); +mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + }, +]); + const from = 1566943856794; const to = 1566857456791; @@ -33,7 +45,12 @@ describe('EventsViewer', () => { const wrapper = mount( - + ); @@ -53,7 +70,12 @@ describe('EventsViewer', () => { const wrapper = mount( - + ); @@ -73,7 +95,12 @@ describe('EventsViewer', () => { const wrapper = mount( - + ); @@ -94,7 +121,12 @@ describe('EventsViewer', () => { const wrapper = mount( - + ); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index ee7853d092784..9878194a17826 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -26,13 +26,13 @@ import { Footer, footerHeight } from '../timeline/footer'; import { combineQueries } from '../timeline/helpers'; import { TimelineRefetch } from '../timeline/refetch_timeline'; import { isCompactFooter } from '../timeline/timeline'; -import { ManageTimelineContext } from '../timeline/timeline_context'; +import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context'; import * as i18n from './translations'; import { - IIndexPattern, - Query, esFilters, esQuery, + IIndexPattern, + Query, } from '../../../../../../../src/plugins/data/public'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; @@ -48,6 +48,7 @@ interface Props { dataProviders: DataProvider[]; end: number; filters: esFilters.Filter[]; + headerFilterGroup?: React.ReactNode; height?: number; id: string; indexPattern: IIndexPattern; @@ -60,7 +61,9 @@ interface Props { showInspect: boolean; start: number; sort: Sort; + timelineTypeContext: TimelineTypeContextProps; toggleColumn: (column: ColumnHeader) => void; + utilityBar?: (totalCount: number) => React.ReactNode; } export const EventsViewer = React.memo( @@ -70,6 +73,7 @@ export const EventsViewer = React.memo( dataProviders, end, filters, + headerFilterGroup, height = DEFAULT_EVENTS_VIEWER_HEIGHT, id, indexPattern, @@ -82,7 +86,9 @@ export const EventsViewer = React.memo( showInspect, start, sort, + timelineTypeContext, toggleColumn, + utilityBar, }) => { const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const core = useKibanaCore(); @@ -116,6 +122,7 @@ export const EventsViewer = React.memo( fields={columnsHeader.map(c => c.id)} filterQuery={combinedQueries.filterQuery} id={id} + indexPattern={indexPattern} limit={itemsPerPage} sortField={{ sortFieldId: sort.columnId, @@ -137,17 +144,29 @@ export const EventsViewer = React.memo( + subtitle={ + utilityBar + ? undefined + : `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT( + totalCount + )}` + } + title={timelineTypeContext?.title ?? i18n.EVENTS} + > + {headerFilterGroup} + + + {utilityBar?.(totalCount)}
- + ({ uiSettings: mockUiSettings, })); +const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; +jest.mock('../../containers/detection_engine/rules/fetch_index_patterns'); +mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + }, +]); + const from = 1566943856794; const to = 1566857456791; @@ -32,7 +44,12 @@ describe('StatefulEventsViewer', () => { const wrapper = mount( - + ); @@ -52,7 +69,12 @@ describe('StatefulEventsViewer', () => { const wrapper = mount( - + ); @@ -72,7 +94,12 @@ describe('StatefulEventsViewer', () => { const wrapper = mount( - + ); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 3514fda4efe29..21292e4ac3254 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -5,26 +5,35 @@ */ import { isEqual } from 'lodash/fp'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; -import { WithSource } from '../../containers/source'; +import chrome from 'ui/chrome'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { timelineActions, inputsActions } from '../../store/actions'; -import { KqlMode, TimelineModel } from '../../store/timeline/model'; +import { inputsActions, timelineActions } from '../../store/actions'; +import { KqlMode, SubsetTimelineModel, TimelineModel } from '../../store/timeline/model'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { Sort } from '../timeline/body/sort'; import { OnChangeItemsPerPage } from '../timeline/events'; -import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; +import { esFilters, Query } from '../../../../../../../src/plugins/data/public'; import { EventsViewer } from './events_viewer'; import { InputsModelId } from '../../store/inputs/constants'; +import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; +import { TimelineTypeContextProps } from '../timeline/timeline_context'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; export interface OwnProps { + defaultIndices?: string[]; + defaultFilters?: esFilters.Filter[]; + defaultModel: SubsetTimelineModel; end: number; id: string; start: number; + headerFilterGroup?: React.ReactNode; + timelineTypeContext?: TimelineTypeContextProps; + utilityBar?: (totalCount: number) => React.ReactNode; } interface StateReduxProps { @@ -74,9 +83,13 @@ const StatefulEventsViewerComponent = React.memo( createTimeline, columns, dataProviders, + defaultFilters = [], + defaultModel, + defaultIndices, deleteEventQuery, end, filters, + headerFilterGroup, id, isLive, itemsPerPage, @@ -86,10 +99,18 @@ const StatefulEventsViewerComponent = React.memo( removeColumn, start, sort, + timelineTypeContext = { + showCheckboxes: false, + showRowRenderers: true, + }, updateItemsPerPage, upsertColumn, + utilityBar, }) => { const [showInspect, setShowInspect] = useState(false); + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + defaultIndices ?? chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY) + ); useEffect(() => { if (createTimeline != null) { @@ -131,31 +152,30 @@ const StatefulEventsViewerComponent = React.memo( const handleOnMouseLeave = useCallback(() => setShowInspect(false), []); return ( - - {({ indexPattern, browserFields }) => ( -
- -
- )} -
+
+ +
); }, (prevProps, nextProps) => @@ -182,15 +202,15 @@ const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getEvents = timelineSelectors.getEventsByIdSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { + const mapStateToProps = (state: State, { id, defaultFilters = [], defaultModel }: OwnProps) => { const input: inputsModel.InputsRange = getInputsTimeline(state); - const events: TimelineModel = getEvents(state, id); + const events: TimelineModel = getEvents(state, id) ?? defaultModel; const { columns, dataProviders, itemsPerPage, itemsPerPageOptions, kqlMode, sort } = events; return { columns, dataProviders, - filters: getGlobalFiltersQuerySelector(state), + filters: [...getGlobalFiltersQuerySelector(state), ...defaultFilters], id, isLive: input.policy.kind === 'interval', itemsPerPage, diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts b/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts index 8512d9535cc69..352b0b95c6dd4 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts @@ -5,7 +5,6 @@ */ import { noop } from 'lodash/fp'; -import { defaultIndexPattern } from '../../../default_index_pattern'; import { timelineQuery } from '../../containers/timeline/index.gql_query'; export const mockEventViewerResponse = [ @@ -31,7 +30,7 @@ export const mockEventViewerResponse = [ sourceId: 'default', pagination: { limit: 25, cursor: null, tiebreaker: null }, sortField: { sortFieldId: '@timestamp', direction: 'desc' }, - defaultIndex: defaultIndexPattern, + defaultIndex: ['filebeat-*', 'auditbeat-*', 'packetbeat-*'], inspect: false, }, }, diff --git a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx b/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx index a54e0803d02ea..bba32e72abc37 100644 --- a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx @@ -6,7 +6,6 @@ import { EuiIcon } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; const LinkIcon = styled(EuiIcon)` @@ -30,7 +29,7 @@ const iconType = 'popout'; * Renders an icon that indicates following the hyperlink will navigate to * content external to the app */ -export const ExternalLinkIcon = pure<{ +export const ExternalLinkIcon = React.memo<{ leftMargin?: boolean; }>(({ leftMargin = true }) => leftMargin ? ( diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap index 6ae9268966480..2ff93b2ecada4 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap @@ -1,220 +1,126 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Field Renderers #autonomousSystemRenderer it renders correctly against snapshot 1`] = ` - - + + + + - - - - - / - - - - - - + / + + + + +
`; exports[`Field Renderers #dateRenderer it renders correctly against snapshot 1`] = ` - - + - + `; exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1`] = ` - - - - raspberrypi - - - + `; exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot 1`] = ` - - - - raspberrypi - - - + `; exports[`Field Renderers #locationRenderer it renders correctly against snapshot 1`] = ` - - + + + + ,  + - - - - ,  - - - - - + + +
`; exports[`Field Renderers #reputationRenderer it renders correctly against snapshot 1`] = ` - talosIntelligence.com - + `; exports[`Field Renderers #whoisRenderer it renders correctly against snapshot 1`] = ` - - - iana.org - - + iana.org + `; diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx index 0fd63bc3f2bf2..2d69db82405ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx @@ -32,9 +32,7 @@ describe('Field Renderers', () => { describe('#locationRenderer', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - - {locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete)} - + locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete) ); expect(toJson(wrapper)).toMatchSnapshot(); @@ -59,9 +57,7 @@ describe('Field Renderers', () => { describe('#dateRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {dateRenderer(mockData.complete.source!.firstSeen)} - ); + const wrapper = shallow(dateRenderer(mockData.complete.source!.firstSeen)); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -78,9 +74,7 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - - {autonomousSystemRenderer(mockData.complete.source!.autonomousSystem!, FlowTarget.source)} - + autonomousSystemRenderer(mockData.complete.source!.autonomousSystem!, FlowTarget.source) ); expect(toJson(wrapper)).toMatchSnapshot(); @@ -113,9 +107,7 @@ describe('Field Renderers', () => { ip: null, }; test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {hostNameRenderer(mockData.complete.host, '10.10.10.10')} - ); + const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -158,9 +150,7 @@ describe('Field Renderers', () => { ip: ['10.10.10.10'], }; test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {hostNameRenderer(mockData.complete.host, '10.10.10.10')} - ); + const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -194,9 +184,7 @@ describe('Field Renderers', () => { describe('#whoisRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallowWithIntl( - {whoisRenderer('10.10.10.10')} - ); + const wrapper = shallowWithIntl(whoisRenderer('10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -208,7 +196,7 @@ describe('Field Renderers', () => { {reputationRenderer('10.10.10.10')} ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DragDropContext'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx index 5df961dfceeb5..80d68dfe1b731 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx @@ -8,9 +8,8 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiText } from ' import { FormattedMessage } from '@kbn/i18n/react'; import { getOr } from 'lodash/fp'; import React, { Fragment, useState } from 'react'; -import { pure } from 'recompose'; - import styled from 'styled-components'; + import { AutonomousSystem, FlowTarget, HostEcsFields, IpOverviewData } from '../../graphql/types'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { DefaultDraggable } from '../draggables'; @@ -151,7 +150,7 @@ interface DefaultFieldRendererProps { // TODO: This causes breaks between elements until the ticket below is fixed // https://github.com/elastic/ingest-dev/issues/474 -export const DefaultFieldRenderer = pure( +export const DefaultFieldRenderer = React.memo( ({ attrName, displayCount = 1, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx index 8d4e3b3928492..7b8451db2212f 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx @@ -5,7 +5,6 @@ */ import { EuiInMemoryTable } from '@elastic/eui'; -import { pure } from 'recompose'; import * as React from 'react'; import styled from 'styled-components'; @@ -33,7 +32,7 @@ interface Props { width: number; } -export const Category = pure( +export const Category = React.memo( ({ categoryId, filteredBrowserFields, fieldItems, timelineId, width }) => ( <> (({ filteredBrowserFields, categoryId, timelineId }) => ( - - - -
{categoryId}
-
-
- - - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - -
-)); +export const CategoryTitle = React.memo( + ({ filteredBrowserFields, categoryId, timelineId }) => ( + + + +
{categoryId}
+
+
+ + + + + {getFieldCount(filteredBrowserFields[categoryId])} + + + +
+ ) +); CategoryTitle.displayName = 'CategoryTitle'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx index 4cc5537bec343..170cf324ca6d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx @@ -5,7 +5,6 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { pure } from 'recompose'; import * as React from 'react'; import styled from 'styled-components'; @@ -59,7 +58,7 @@ type Props = Pick void; }; -export const FieldsPane = pure( +export const FieldsPane = React.memo( ({ columnHeaders, filteredBrowserFields, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx index ae9109bffe0db..8acb19970c268 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx @@ -13,7 +13,6 @@ import { EuiTitle, } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -65,7 +64,7 @@ interface Props { timelineId: string; } -const CountRow = pure>(({ filteredBrowserFields }) => ( +const CountRow = React.memo>(({ filteredBrowserFields }) => ( >(({ filteredBrowserFi CountRow.displayName = 'CountRow'; -const TitleRow = pure<{ +const TitleRow = React.memo<{ isEventViewer?: boolean; onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns; @@ -121,7 +120,7 @@ const TitleRow = pure<{ TitleRow.displayName = 'TitleRow'; -export const Header = pure( +export const Header = React.memo( ({ isEventViewer, isSearching, diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap index 56432cb25c189..35fe74abff284 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap @@ -1,9 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendering renders correctly 1`] = ` - -

- Additional filters here. -

-
+ + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx index adbd904c5c325..7f377a57c3e9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx @@ -9,7 +9,7 @@ import toJson from 'enzyme-to-json'; import React from 'react'; import '../../mock/match_media'; -import { FiltersGlobal } from './index'; +import { FiltersGlobal } from './filters_global'; describe('rendering', () => { test('renders correctly', () => { @@ -18,6 +18,7 @@ describe('rendering', () => {

{'Additional filters here.'}

); + expect(toJson(wrapper)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx index bdda8497a8bcb..edf6f7f01ab2e 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx @@ -7,7 +7,6 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; import { Sticky } from 'react-sticky'; -import { pure } from 'recompose'; import styled, { css } from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; @@ -42,7 +41,7 @@ export interface FiltersGlobalProps { children: React.ReactNode; } -export const FiltersGlobal = pure(({ children }) => ( +export const FiltersGlobal = React.memo(({ children }) => ( {({ style, isSticky }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap index 9553ec5b7654e..ee76657c8d27a 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap @@ -1,8 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Select Flow Direction rendering it renders the basic group button for uni-direction and bi-direction 1`] = ` - + + + Unidirectional + + + Bidirectional + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap index 46053008ea09c..a9b48c8ee16be 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap @@ -1,11 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FlowTargetSelect Component rendering it renders the FlowTargetSelect 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx index d5370c218a2de..2b826164063be 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx @@ -7,7 +7,6 @@ import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; import React from 'react'; -import { pure } from 'recompose'; import { FlowDirection } from '../../graphql/types'; import * as i18n from './translations'; @@ -17,7 +16,7 @@ interface Props { onChangeDirection: (value: FlowDirection) => void; } -export const FlowDirectionSelect = pure(({ onChangeDirection, selectedDirection }) => ( +export const FlowDirectionSelect = React.memo(({ onChangeDirection, selectedDirection }) => ( ( +export const FlowTargetSelect = React.memo( ({ id, isLoading = false, diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap index 3aa9fd1b962b5..abdc4f4681294 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap @@ -1,16 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Flyout rendering it renders correctly against snapshot 1`] = ` - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx index 22c57c8c8a00e..e1075c89ca350 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx @@ -25,7 +25,7 @@ import { Properties } from '../../timeline/properties'; import { appActions, appModel } from '../../../store/app'; import { inputsActions } from '../../../store/inputs'; import { timelineActions } from '../../../store/actions'; -import { TimelineModel } from '../../../store/timeline/model'; +import { timelineDefaults, TimelineModel } from '../../../store/timeline/model'; import { DEFAULT_TIMELINE_WIDTH } from '../../timeline/body/helpers'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -129,7 +129,7 @@ const makeMapStateToProps = () => { const getNotesByIds = appSelectors.notesByIdsSelector(); const getGlobalInput = inputsSelectors.globalSelector(); const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId); + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; const globalInput: inputsModel.InputsRange = getGlobalInput(state); const { dataProviders, diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index ddc3e4f15938a..86a8952a10efa 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -37,7 +37,7 @@ describe('Flyout', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Flyout'))).toMatchSnapshot(); }); test('it renders the default flyout state as a button', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index aae8f67997156..2d347830d5b1b 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -124,3 +124,5 @@ const mapStateToProps = (state: State, { timelineId }: OwnProps) => { export const Flyout = connect(mapStateToProps, { showTimeline: timelineActions.showTimeline, })(FlyoutComponent); + +Flyout.displayName = 'Flyout'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap index 31eaf4f56d7bc..efa682cd4d18e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -1,22 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Pane renders correctly against snapshot 1`] = ` - - - - I am a child of flyout - - - + + + I am a child of flyout + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 65233e55901ff..acea2d1cce468 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -44,7 +44,7 @@ describe('Pane', () => { ); - expect(toJson(EmptyComponent)).toMatchSnapshot(); + expect(toJson(EmptyComponent.find('Pane'))).toMatchSnapshot(); }); test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index 4b5ceb25befa4..f2f0cf4f980f3 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -182,3 +182,5 @@ FlyoutPaneComponent.displayName = 'FlyoutPaneComponent'; export const Pane = connect(null, { applyDeltaToWidth: timelineActions.applyDeltaToWidth, })(FlyoutPaneComponent); + +Pane.displayName = 'Pane'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx index 71820c62dd528..a517820361f9f 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx @@ -21,6 +21,10 @@ jest.mock('../../lib/settings/use_kibana_ui_setting', () => ({ describe('formatted_bytes', () => { describe('PreferenceFormattedBytes', () => { describe('rendering', () => { + beforeEach(() => { + mockUseKibanaUiSetting.mockClear(); + }); + const bytes = '2806422'; test('renders correctly against snapshot', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap index 0f9cf1ba89f9c..d196a23bff5bf 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`formatted_date PreferenceFormattedDate rendering renders correctly against snapshot 1`] = ` - +> + 2019-02-25T22:27:05.000Z + `; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx index bb0b947f149f4..df361a06d3805 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx @@ -38,7 +38,8 @@ describe('formatted_date', () => { .format(config.dateFormat); test('renders correctly against snapshot', () => { - const wrapper = shallow(); + mockUseKibanaUiSetting.mockImplementation(() => [null]); + const wrapper = mount(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx index 32c064096fcf9..37bf3653f3b62 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx @@ -7,7 +7,6 @@ import moment from 'moment-timezone'; import * as React from 'react'; import { FormattedRelative } from '@kbn/i18n/react'; -import { pure } from 'recompose'; import { DEFAULT_DATE_FORMAT, @@ -19,7 +18,7 @@ import { getOrEmptyTagFromValue } from '../empty_value'; import { LocalizedDateTooltip } from '../localized_date_tooltip'; import { getMaybeDate } from './maybe_date'; -export const PreferenceFormattedDate = pure<{ value: Date }>(({ value }) => { +export const PreferenceFormattedDate = React.memo<{ value: Date }>(({ value }) => { const [dateFormat] = useKibanaUiSetting(DEFAULT_DATE_FORMAT); const [dateFormatTz] = useKibanaUiSetting(DEFAULT_DATE_FORMAT_TZ); const [timezone] = useKibanaUiSetting(DEFAULT_TIMEZONE_BROWSER); @@ -43,7 +42,7 @@ PreferenceFormattedDate.displayName = 'PreferenceFormattedDate'; * - a long representation of the date that includes the day of the week (e.g. Thursday, March 21, 2019 6:47pm) * - the raw date value (e.g. 2019-03-22T00:47:46Z) */ -export const FormattedDate = pure<{ +export const FormattedDate = React.memo<{ fieldName: string; value?: string | number | null; }>( diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx index c97fc7bdc2428..8afbafe57af4a 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx @@ -5,12 +5,11 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { getFormattedDurationString } from './helpers'; import { FormattedDurationTooltip } from './tooltip'; -export const FormattedDuration = pure<{ +export const FormattedDuration = React.memo<{ maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; }>(({ maybeDurationNanoseconds, tooltipTitle }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx index 08f4a412caf51..1372b3ef10920 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx @@ -6,7 +6,6 @@ import { EuiToolTip } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; @@ -18,7 +17,7 @@ const P = styled.p` P.displayName = 'P'; -export const FormattedDurationTooltipContent = pure<{ +export const FormattedDurationTooltipContent = React.memo<{ maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; }>(({ maybeDurationNanoseconds, tooltipTitle }) => ( @@ -35,7 +34,7 @@ export const FormattedDurationTooltipContent = pure<{ FormattedDurationTooltipContent.displayName = 'FormattedDurationTooltipContent'; -export const FormattedDurationTooltip = pure<{ +export const FormattedDurationTooltip = React.memo<{ children: JSX.Element; maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx index 81f5cbfe2308b..8dcb558122d01 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx @@ -6,7 +6,6 @@ import { isArray, isEmpty, isString, uniq } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; @@ -60,7 +59,7 @@ const getDataProvider = ({ and: [], }); -const NonDecoratedIp = pure<{ +const NonDecoratedIp = React.memo<{ contextId: string; eventId: string; fieldName: string; @@ -92,7 +91,7 @@ const NonDecoratedIp = pure<{ NonDecoratedIp.displayName = 'NonDecoratedIp'; -const AddressLinks = pure<{ +const AddressLinks = React.memo<{ addresses: string[]; contextId: string; eventId: string; @@ -128,7 +127,7 @@ const AddressLinks = pure<{ AddressLinks.displayName = 'AddressLinks'; -export const FormattedIp = pure<{ +export const FormattedIp = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap index 665a5c75f3684..849f3616524cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap @@ -1,7 +1,107 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderGlobal it renders 1`] = ` - - - + + + + + + + + + + + + + + + + + + + + + + Add data + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx index ebd1da634ed1a..b3eb599af9407 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx @@ -8,7 +8,6 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import React from 'react'; -import { TestProviders } from '../../mock'; import '../../mock/match_media'; import '../../mock/ui_settings'; import { HeaderGlobal } from './index'; @@ -23,11 +22,7 @@ jest.mock('../search_bar', () => ({ describe('HeaderGlobal', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap index 0fe2890dc9f24..a91d8fce87dac 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap @@ -1,23 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderPage it renders 1`] = ` - - + -

- Test supplement -

-
-
+ + +

+ Test title + + +

+
+ + +
+ +

+ Test supplement +

+
+ + `; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx index 9c50a915b7ba8..c20f3c7185e66 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx @@ -18,17 +18,15 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('HeaderPage', () => { test('it renders', () => { const wrapper = shallow( - - -

{'Test supplement'}

-
-
+ +

{'Test supplement'}

+
); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap index ecd2b15a841f6..d4c3763f51460 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap @@ -1,9 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderSection it renders 1`] = ` - - - +
+ + + + + +

+ Test title +

+
+
+
+
+
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx index 4a6da9c80968f..8606758c68d2c 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx @@ -17,11 +17,7 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('HeaderSection', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx index 43fd8e653f3d8..d42ee08e86407 100644 --- a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect } from 'react'; -import { pure } from 'recompose'; +import React, { useEffect } from 'react'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -export const HelpMenu = pure<{}>(() => { +export const HelpMenu = React.memo(() => { useEffect(() => { chrome.helpExtension.set({ appName: i18n.translate('xpack.siem.chrome.help.appName', { diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index 56bd86310acad..6908aba542e4c 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -11,7 +11,6 @@ import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import styled from 'styled-components'; -import { pure } from 'recompose'; import { inputsModel, inputsSelectors, State } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { inputsActions } from '../../store/inputs'; @@ -58,7 +57,7 @@ interface InspectButtonDispatch { type InspectButtonProps = OwnProps & InspectButtonReducer & InspectButtonDispatch; -const InspectButtonComponent = pure( +const InspectButtonComponent = React.memo( ({ compact = false, inputId = 'global', diff --git a/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap index d75a0f054775a..0199742242e59 100644 --- a/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap @@ -1,10 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Port renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx b/x-pack/legacy/plugins/siem/public/components/ip/index.tsx index ceec48951a198..8c327989963b4 100644 --- a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ip/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; @@ -18,7 +17,7 @@ const IP_FIELD_TYPE = 'ip'; * Renders text containing a draggable IP address (e.g. `source.ip`, * `destination.ip`) that contains a hyperlink */ -export const Ip = pure<{ +export const Ip = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx index 3148efbb3050a..950ab252ad0bd 100644 --- a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DraggableBadge } from '../draggables'; @@ -27,7 +26,7 @@ Ja3FingerprintLabel.displayName = 'Ja3FingerprintLabel'; * using TLS traffic to be identified, which is possible because SSL * negotiations happen in the clear */ -export const Ja3Fingerprint = pure<{ +export const Ja3Fingerprint = React.memo<{ eventId: string; contextId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap index 5902768383cb0..c5086c8cde285 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap @@ -1,14 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LinkIcon it renders 1`] = ` - - + + Test link - - + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx index 451db49028ee1..7f9133a0de7c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx @@ -17,11 +17,9 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('LinkIcon', () => { test('it renders', () => { const wrapper = shallow( - - - {'Test link'} - - + + {'Test link'} + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 0125b52e3ad33..5a7f6ef1274c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; -import { pure } from 'recompose'; import { SiemPageName } from '../../pages/home/types'; import { HostsTableType } from '../../store/hosts/model'; @@ -26,7 +25,7 @@ interface LinkToPageProps { match: RouteMatch<{}>; } -export const LinkToPage = pure(({ match }) => ( +export const LinkToPage = React.memo(({ match }) => ( ( +export const HostDetailsLink = React.memo<{ children?: React.ReactNode; hostName: string }>( ({ children, hostName }) => ( {children ? children : hostName} @@ -22,7 +21,7 @@ export const HostDetailsLink = pure<{ children?: React.ReactNode; hostName: stri HostDetailsLink.displayName = 'HostDetailsLink'; -export const IPDetailsLink = pure<{ children?: React.ReactNode; ip: string }>( +export const IPDetailsLink = React.memo<{ children?: React.ReactNode; ip: string }>( ({ children, ip }) => ( {children ? children : ip} @@ -33,7 +32,7 @@ export const IPDetailsLink = pure<{ children?: React.ReactNode; ip: string }>( IPDetailsLink.displayName = 'IPDetailsLink'; // External Links -export const GoogleLink = pure<{ children?: React.ReactNode; link: string }>( +export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( {children ? children : link} @@ -43,7 +42,7 @@ export const GoogleLink = pure<{ children?: React.ReactNode; link: string }>( GoogleLink.displayName = 'GoogleLink'; -export const PortOrServiceNameLink = pure<{ +export const PortOrServiceNameLink = React.memo<{ children?: React.ReactNode; portOrServiceName: number | string; }>(({ children, portOrServiceName }) => ( @@ -60,21 +59,22 @@ export const PortOrServiceNameLink = pure<{ PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; -export const Ja3FingerprintLink = pure<{ children?: React.ReactNode; ja3Fingerprint: string }>( - ({ children, ja3Fingerprint }) => ( - - {children ? children : ja3Fingerprint} - - ) -); +export const Ja3FingerprintLink = React.memo<{ + children?: React.ReactNode; + ja3Fingerprint: string; +}>(({ children, ja3Fingerprint }) => ( + + {children ? children : ja3Fingerprint} + +)); Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; -export const CertificateFingerprintLink = pure<{ +export const CertificateFingerprintLink = React.memo<{ children?: React.ReactNode; certificateFingerprint: string; }>(({ children, certificateFingerprint }) => ( @@ -91,7 +91,7 @@ export const CertificateFingerprintLink = pure<{ CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; -export const ReputationLink = pure<{ children?: React.ReactNode; domain: string }>( +export const ReputationLink = React.memo<{ children?: React.ReactNode; domain: string }>( ({ children, domain }) => ( ( +export const VirusTotalLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( VirusTotalLink.displayName = 'VirusTotalLink'; -export const WhoIsLink = pure<{ children?: React.ReactNode; domain: string }>( +export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( ({ children, domain }) => ( {children ? children : domain} diff --git a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap index 440193c9e0dfd..0885f15b1efba 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap @@ -1,11 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendering renders correctly 1`] = ` - - Loading - + + + + + + +

+ Loading +

+
+
+
+ `; diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx index 55628fe2e8d33..be2ce3dde951c 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import { rgba } from 'polished'; import React from 'react'; -import { pure } from 'recompose'; import styled, { css } from 'styled-components'; const Aside = styled.aside<{ overlay?: boolean; overlayBackground?: string }>` @@ -56,9 +55,10 @@ export interface LoaderProps { overlay?: boolean; overlayBackground?: string; size?: EuiLoadingSpinnerSize; + children?: React.ReactChild; } -export const Loader = pure(({ children, overlay, overlayBackground, size }) => ( +export const Loader = React.memo(({ children, overlay, overlayBackground, size }) => (
+
@@ -678,80 +650,66 @@ exports[`Stat Items Component rendering kpis with charts it renders the default show={false} title="KPI UNIQUE_PRIVATE_IPS" > - - -
- - - - -
-
-
+ viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + + + +
+ +
+
diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap index 2522d4d1de084..1c6ff628df1e6 100644 --- a/x-pack/legacy/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap @@ -1,9 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Subtitle it renders 1`] = ` - - - + + + Test subtitle + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx index 77506f8a466a5..b54f3133de472 100644 --- a/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx @@ -16,11 +16,7 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('Subtitle', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap index 1e6c5eb89352b..82f447a55ec07 100644 --- a/x-pack/legacy/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap @@ -1,33 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Table Helpers #getRowItemDraggable it returns correctly against snapshot 1`] = ` - - - + } + key="idPrefix-attrName-item1" + render={[Function]} +/> `; exports[`Table Helpers #getRowItemDraggables it returns correctly against snapshot 1`] = ` - - + , - , - - + `; exports[`Table Helpers #getRowItemOverflow it returns correctly against snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx index eb06fe8a01d79..9d9087d34a765 100644 --- a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx @@ -27,7 +27,7 @@ describe('Table Helpers', () => { idPrefix: 'idPrefix', }); const wrapper = shallow({rowItem}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DraggableWrapper'))).toMatchSnapshot(); }); test('it returns empty value when rowItem is undefined', () => { @@ -38,7 +38,7 @@ describe('Table Helpers', () => { displayCount: 0, }); const wrapper = mount({rowItem}); - expect(wrapper.text()).toBe(getEmptyValue()); + expect(wrapper.find('DragDropContext').text()).toBe(getEmptyValue()); }); test('it returns empty string value when rowItem is empty', () => { @@ -95,7 +95,7 @@ describe('Table Helpers', () => { idPrefix: 'idPrefix', }); const wrapper = shallow({rowItems}); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DragDropContext'))).toMatchSnapshot(); }); test('it returns empty value when rowItems is undefined', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx index d16e446a879a6..6c793126efa72 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx @@ -11,7 +11,6 @@ import { EuiGlobalToastListToast as Toast, } from '@elastic/eui'; import { getOr } from 'lodash/fp'; -import { pure } from 'recompose'; import * as React from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -47,7 +46,7 @@ interface DispatchProps { type OwnProps = ReduxProps & DispatchProps; -const AutoSaveWarningMsgComponent = pure( +const AutoSaveWarningMsgComponent = React.memo( ({ newTimelineModel, setTimelineRangeDatePicker, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx index 03c8fd1951725..b57b343d614a8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx @@ -6,7 +6,6 @@ import { EuiButtonIcon } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { OnColumnRemoved } from '../../../events'; import { EventsHeadingExtra, EventsLoading } from '../../../styles'; @@ -23,7 +22,7 @@ interface Props { /** Given a `header`, returns the `SortDirection` applicable to it */ -export const CloseButton = pure<{ +export const CloseButton = React.memo<{ columnId: string; onColumnRemoved: OnColumnRemoved; }>(({ columnId, onColumnRemoved }) => ( @@ -40,6 +39,7 @@ export const CloseButton = pure<{ }} /> )); + CloseButton.displayName = 'CloseButton'; export const Actions = React.memo(({ header, onColumnRemoved, sort }) => { @@ -58,4 +58,5 @@ export const Actions = React.memo(({ header, onColumnRemoved, sort }) => ); }); + Actions.displayName = 'Actions'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx index 1de227a9a675b..15911f522032a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx @@ -17,6 +17,7 @@ export interface ColumnHeader { example?: string; format?: string; id: ColumnId; + label?: string; placeholder?: string; type?: string; width: number; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx index 1298760cff73a..057f751f451ac 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx @@ -6,7 +6,6 @@ import { EuiText } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { Pin } from '../../../../pin'; @@ -47,7 +46,7 @@ export interface EventsSelectOption { dropdownDisplay: JSX.Element | string; } -export const DropdownDisplay = pure<{ text: string }>(({ text }) => ( +export const DropdownDisplay = React.memo<{ text: string }>(({ text }) => ( {text} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx index 4f414af74a914..634db2dc52676 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx @@ -7,7 +7,6 @@ import { EuiCheckbox, EuiSuperSelect } from '@elastic/eui'; import { noop } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled, { createGlobalStyle } from 'styled-components'; import { getEventsSelectOptions } from './helpers'; @@ -50,7 +49,7 @@ interface Props { timelineId: string; } -export const EventsSelect = pure(({ checkState, timelineId }) => { +export const EventsSelect = React.memo(({ checkState, timelineId }) => { return (
`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx index 53d94716ba3f3..c56322cc69e0c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx @@ -6,7 +6,6 @@ import { noop } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { OnFilterChange } from '../../../events'; import { ColumnHeader } from '../column_header'; @@ -18,7 +17,7 @@ interface Props { } /** Renders a header's filter, based on the `columnHeaderType` */ -export const Filter = pure(({ header, onFilterChange = noop }) => { +export const Filter = React.memo(({ header, onFilterChange = noop }) => { switch (header.columnHeaderType) { case 'text-filter': return ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx index 35a4f4a74ae20..bfcf3cd639799 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx @@ -67,6 +67,31 @@ describe('Header', () => { ).toEqual(columnHeader.id); }); + test('it renders the header text alias when label is provided', () => { + const label = 'Timestamp'; + const headerWithLabel = { ...columnHeader, label }; + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="header-text-${columnHeader.id}"]`) + .first() + .text() + ).toEqual(label); + }); + test('it renders a sort indicator', () => { const headerSortable = { ...columnHeader, aggregatable: true }; const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx index 474de01020497..311b4bfda60fe 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx @@ -50,7 +50,7 @@ const HeaderComp = React.memo( data-test-subj="header-tooltip" content={} > - <>{header.id} + <>{header.label ?? header.id} @@ -66,7 +66,7 @@ const HeaderComp = React.memo( data-test-subj="header-tooltip" content={} > - <>{header.id} + <>{header.label ?? header.id} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap index a7c1f67196451..945a9a7aee698 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap @@ -1,20 +1,66 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderToolTipContent it renders the expected table content 1`] = ` - +

+ + Category + : + + + base + +

+

+ + Field + : + + + @timestamp + +

+

+ + Type + : + + + + + date + + +

+

+ + Description + : + + + Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. -Required field for all events.", - "example": "2016-05-23T08:05:34.853Z", - "id": "@timestamp", - "type": "date", - "width": 190, - } - } -/> +Required field for all events. + +

+ `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx index 4b56b9589f19e..a63ec2bf840a6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx @@ -7,7 +7,6 @@ import { EuiIcon } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { getIconFromType } from '../../../../event_details/helpers'; @@ -36,7 +35,7 @@ const ToolTipTableValue = styled.span` `; ToolTipTableValue.displayName = 'ToolTipTableValue'; -export const HeaderToolTipContent = pure<{ header: ColumnHeader }>(({ header }) => ( +export const HeaderToolTipContent = React.memo<{ header: ColumnHeader }>(({ header }) => ( <> {!isEmpty(header.category) && (

diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx index ae6f8757f98a9..c221829f92d04 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx @@ -6,7 +6,6 @@ import { EuiSelect } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { OnRangeSelected } from '../../../events'; @@ -29,7 +28,7 @@ const SelectContainer = styled.div` SelectContainer.displayName = 'SelectContainer'; /** Renders a time range picker for the MiniMap (e.g. 1 Day, 1 Week...) */ -export const RangePicker = pure(({ selected, onRangeSelected }) => { +export const RangePicker = React.memo(({ selected, onRangeSelected }) => { const onChange = (event: React.ChangeEvent): void => { onRangeSelected(event.target.value); }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap index 698572d05e30a..fc4bd7bbd6148 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap @@ -1,8 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TextFilter rendering renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx index 995e5d8f58df3..09672eb9a38fe 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx @@ -7,7 +7,6 @@ import { EuiFieldText } from '@elastic/eui'; import { noop } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { OnFilterChange } from '../../../events'; @@ -30,7 +29,7 @@ const FieldText = styled(EuiFieldText)<{ minwidth: string }>` FieldText.displayName = 'FieldText'; /** Renders a text-based column filter */ -export const TextFilter = pure( +export const TextFilter = React.memo( ({ columnId, minWidth, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 366cd5d1766db..d370a24cc1d4d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -1,124 +1,240 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Columns it renders the expected columns 1`] = ` - - + + + + + + + + + + + - + > + + + + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx index 3ca17f4ca1ac6..f5b33296561c7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx @@ -8,7 +8,7 @@ import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mockTimelineData, TestProviders } from '../../../../mock'; +import { mockTimelineData } from '../../../../mock'; import { defaultHeaders } from '../column_headers/default_headers'; import { columnRenderers } from '../renderers'; @@ -19,16 +19,14 @@ describe('Columns', () => { test('it renders the expected columns', () => { const wrapper = shallow( - - - + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx index 96cb9f754f525..72f45bdb27fae 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx @@ -17,6 +17,7 @@ import { ColumnHeader } from '../column_headers/column_header'; import { DataDrivenColumns } from '../data_driven_columns'; import { eventHasNotes, getPinOnClick } from '../helpers'; import { ColumnRenderer } from '../renderers/column_renderer'; +import { useTimelineTypeContext } from '../../timeline_context'; interface Props { id: string; @@ -67,44 +68,48 @@ export const EventColumnView = React.memo( timelineId, toggleShowNotes, updateNote, - }) => ( - - + }) => { + const timelineTypeContext = useTimelineTypeContext(); - - - ), + return ( + + + + + + ); + }, (prevProps, nextProps) => { return ( prevProps.id === nextProps.id && diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts index 21a2e54aa1949..602b88b7ae6d6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts @@ -17,6 +17,7 @@ import { getColumnWidthFromType, getPinTooltip, stringifyEvent, + SHOW_CHECK_BOXES_COLUMN_WIDTH, } from './helpers'; describe('helpers', () => { @@ -202,28 +203,22 @@ describe('helpers', () => { }); describe('getPinTooltip', () => { - test('it informs the user the event may not be unpinned when the event is pinned and has notes', () => { + test('it indicates the event may NOT be unpinned when `isPinned` is `true` and the event has notes', () => { expect(getPinTooltip({ isPinned: true, eventHasNotes: true })).toEqual( 'This event cannot be unpinned because it has notes' ); }); - test('it tells the user the event is persisted when the event is pinned, but has no notes', () => { - expect(getPinTooltip({ isPinned: true, eventHasNotes: false })).toEqual( - 'This event is persisted with the timeline' - ); + test('it indicates the event is pinned when `isPinned` is `true` and the event does NOT have notes', () => { + expect(getPinTooltip({ isPinned: true, eventHasNotes: false })).toEqual('Pinned event'); }); - test('it tells the user the event is NOT persisted when the event is not pinned, but it has notes', () => { - expect(getPinTooltip({ isPinned: false, eventHasNotes: true })).toEqual( - 'This is event is NOT persisted with the timeline' - ); + test('it indicates the event is NOT pinned when `isPinned` is `false` and the event has notes', () => { + expect(getPinTooltip({ isPinned: false, eventHasNotes: true })).toEqual('Unpinned event'); }); - test('it tells the user the event is NOT persisted when the event is not pinned, and has no notes', () => { - expect(getPinTooltip({ isPinned: false, eventHasNotes: false })).toEqual( - 'This is event is NOT persisted with the timeline' - ); + test('it indicates the event is NOT pinned when `isPinned` is `false` and the event does NOT have notes', () => { + expect(getPinTooltip({ isPinned: false, eventHasNotes: false })).toEqual('Unpinned event'); }); }); @@ -258,8 +253,20 @@ describe('helpers', () => { expect(getActionsColumnWidth(false)).toEqual(DEFAULT_ACTIONS_COLUMN_WIDTH); }); + test('returns the default actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => { + expect(getActionsColumnWidth(false, true)).toEqual( + DEFAULT_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH + ); + }); + test('returns the events viewer actions column width when isEventViewer is true', () => { expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH); }); + + test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { + expect(getActionsColumnWidth(true, true)).toEqual( + EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH + ); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts index 0b1d21b2371ee..5fdc2fddfbfb1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts @@ -18,6 +18,8 @@ export const DEFAULT_ACTIONS_COLUMN_WIDTH = 115; // px; * an events viewer, which has fewer actions than a regular events viewer */ export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 32; // px; +/** Additional column width to include when checkboxes are shown **/ +export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 32; // px; /** The default minimum width of a column (when a width for the column type is not specified) */ export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px /** The default minimum width of a column of type `date` */ @@ -93,5 +95,6 @@ export const getColumnHeaders = ( }; /** Returns the (fixed) width of the Actions column */ -export const getActionsColumnWidth = (isEventViewer: boolean): number => - isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH; +export const getActionsColumnWidth = (isEventViewer: boolean, showCheckboxes = false): number => + (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) + + (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index 07e37346ac968..0aed68a0e4ad7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -27,6 +27,7 @@ import { getActionsColumnWidth } from './helpers'; import { ColumnRenderer } from './renderers/column_renderer'; import { RowRenderer } from './renderers/row_renderer'; import { Sort } from './sort'; +import { useTimelineTypeContext } from '../timeline_context'; export interface BodyProps { addNoteToEvent: AddNoteToEvent; @@ -80,9 +81,11 @@ export const Body = React.memo( toggleColumn, updateNote, }) => { + const timelineTypeContext = useTimelineTypeContext(); + const columnWidths = columnHeaders.reduce( (totalWidth, header) => totalWidth + header.width, - getActionsColumnWidth(isEventViewer) + getActionsColumnWidth(isEventViewer, timelineTypeContext.showCheckboxes) ); return ( @@ -94,7 +97,10 @@ export const Body = React.memo( style={{ minWidth: columnWidths + 'px' }} > ( /> - `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap index 03254490a2ce5..b867e0a788449 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap @@ -2,7 +2,7 @@ exports[`get_column_renderer renders correctly against snapshot 1`] = ` - + + + + + + in + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap index 2271514b20faa..e2eaf1d872b26 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap @@ -2,7 +2,7 @@ exports[`plain_column_renderer rendering renders correctly against snapshot 1`] = ` - +

+ + +
`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap index 595dfd486281c..d01bb85f565dc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap @@ -1,12 +1,48 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UserHostWorkingDir rendering it renders against shallow snapshot 1`] = ` - + + + + + + \\ + + + + + + @ + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap index f777029f2b48d..cf953ccef8f8e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap @@ -1,114 +1,130 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`GenericDetails rendering it renders the default AuditAcquiredCredsDetails 1`] = ` - + + + + timelineId="test" + /> + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap index f18bfcc612ea8..8e806fadb7bf8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap @@ -1,10 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`GenericFileDetails rendering it renders the default GenericFileDetails 1`] = ` - - + + + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap index 037ec582ca1a0..bf73f545ce3d5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap @@ -1,11 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UserPrimarySecondary rendering it renders the default PrimarySecondaryUserInfo 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx index 90698cc3bf5c9..585a6527423f0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx @@ -49,16 +49,14 @@ describe('GenericDetails', () => { }); test('it returns null for text if the data contains no auditd data', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx index 127d88a2a2c6d..b60d89c857421 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; @@ -38,7 +37,7 @@ interface Props { session: string | null | undefined; } -export const AuditdGenericLine = pure( +export const AuditdGenericLine = React.memo( ({ id, contextId, @@ -112,7 +111,7 @@ interface GenericDetailsProps { timelineId: string; } -export const AuditdGenericDetails = pure( +export const AuditdGenericDetails = React.memo( ({ data, contextId, text, timelineId }) => { const id = data._id; const session: string | null | undefined = get('auditd.session[0]', data); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx index 7630202293f8f..63050d3e7dc9e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx @@ -20,16 +20,14 @@ describe('GenericFileDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy const browserFields: BrowserFields = {}; const wrapper = shallow( - - - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -53,17 +51,15 @@ describe('GenericFileDetails', () => { }); test('it returns null for text if the data contains no auditd data', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx index 076c605ecb89f..be84696033d69 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiSpacer, IconType } from '@elastic/eui'; import { get } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; @@ -40,7 +39,7 @@ interface Props { session: string | null | undefined; } -export const AuditdGenericFileLine = pure( +export const AuditdGenericFileLine = React.memo( ({ id, contextId, @@ -131,7 +130,7 @@ interface GenericDetailsProps { timelineId: string; } -export const AuditdGenericFileDetails = pure( +export const AuditdGenericFileDetails = React.memo( ({ data, contextId, text, fileIcon = 'document', timelineId }) => { const id = data._id; const session: string | null | undefined = get('auditd.session[0]', data); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx index a895f68db1d5a..bd350a599bb47 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx @@ -6,7 +6,6 @@ import { EuiFlexGroup } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { DraggableBadge } from '../../../../draggables'; @@ -23,7 +22,7 @@ interface Props { secondary: string | null | undefined; } -export const PrimarySecondary = pure(({ contextId, eventId, primary, secondary }) => { +export const PrimarySecondary = React.memo(({ contextId, eventId, primary, secondary }) => { if (nilOrUnSet(primary) && nilOrUnSet(secondary)) { return null; } else if (!nilOrUnSet(primary) && nilOrUnSet(secondary)) { @@ -95,7 +94,7 @@ interface PrimarySecondaryUserInfoProps { secondary: string | null | undefined; } -export const PrimarySecondaryUserInfo = pure( +export const PrimarySecondaryUserInfo = React.memo( ({ contextId, eventId, userName, primary, secondary }) => { if (nilOrUnSet(userName) && nilOrUnSet(primary) && nilOrUnSet(secondary)) { return null; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx index 6d11709cdb63e..3c825e6c931be 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DraggableBadge } from '../../../../draggables'; @@ -25,7 +24,7 @@ interface Props { session: string | null | undefined; } -export const SessionUserHostWorkingDir = pure( +export const SessionUserHostWorkingDir = React.memo( ({ eventId, contextId, hostName, userName, primary, secondary, workingDirectory, session }) => ( <> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx index aaa252fd651f8..7529c0623ef47 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { isNumber, isString } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { Bytes, BYTES_FORMAT } from '../../../bytes'; import { Duration, EVENT_DURATION_FIELD_NAME } from '../../../duration'; @@ -24,7 +23,7 @@ import { MESSAGE_FIELD_NAME, } from './constants'; -export const FormattedFieldValue = pure<{ +export const FormattedFieldValue = React.memo<{ contextId: string; eventId: string; fieldFormat?: string; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx index 9b25d246f7780..0bdecfecd6c59 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DraggableBadge } from '../../../draggables'; @@ -19,8 +18,8 @@ interface Props { workingDirectory: string | null | undefined; } -export const HostWorkingDir = pure(({ contextId, eventId, hostName, workingDirectory }) => { - return ( +export const HostWorkingDir = React.memo( + ({ contextId, eventId, hostName, workingDirectory }) => ( <> (({ contextId, eventId, hostName, worki /> - ); -}); + ) +); HostWorkingDir.displayName = 'HostWorkingDir'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx index 6bdb3463949d6..0904c836c2f30 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx @@ -6,7 +6,6 @@ import { get } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { Ecs } from '../../../../graphql/types'; import { asArrayIfExists } from '../../../../lib/helpers'; @@ -51,7 +50,12 @@ import { NETWORK_TRANSPORT_FIELD_NAME, } from '../../../source_destination/field_names'; -export const NetflowRenderer = pure<{ data: Ecs; timelineId: string }>(({ data, timelineId }) => ( +interface NetflowRendererProps { + data: Ecs; + timelineId: string; +} + +export const NetflowRenderer = React.memo(({ data, timelineId }) => ( { }); test('it returns null if everything is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if everything is undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx index 440feca8f3e56..29d6c0e7d59c2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DraggableBadge } from '../../../draggables'; @@ -22,7 +21,7 @@ interface Props { processName: string | undefined | null; } -export const ProcessDraggable = pure( +export const ProcessDraggable = React.memo( ({ contextId, endgamePid, @@ -94,7 +93,7 @@ export const ProcessDraggable = pure( ProcessDraggable.displayName = 'ProcessDraggable'; -export const ProcessDraggableWithNonExistentProcess = pure( +export const ProcessDraggableWithNonExistentProcess = React.memo( ({ contextId, endgamePid, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap index 53fa32038855e..e79b1e254fea3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap @@ -1,499 +1,102 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SuricataDetails rendering it renders the default SuricataDetails 1`] = ` - + + + + + timelineId="test" + /> + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap index 0177dfd941b36..1691eafc4afaf 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap @@ -1,10 +1,43 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SuricataSignature rendering it renders the default SuricataSignature 1`] = ` - + + + + + +
+ + Hello + + +
+
+
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx index ff84c6e03748e..20b64661b6a00 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -43,14 +43,12 @@ describe('SuricataDetails', () => { }); test('it returns null for text if the data contains no suricata data', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx index 442e5ae7631e1..35733e5e0b31b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx @@ -7,7 +7,6 @@ import { EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../../../../containers/source'; @@ -23,13 +22,14 @@ const Details = styled.div` Details.displayName = 'Details'; -export const SuricataDetails = pure<{ +export const SuricataDetails = React.memo<{ browserFields: BrowserFields; data: Ecs; timelineId: string; }>(({ data, timelineId }) => { const signature: string | null | undefined = get('suricata.eve.alert.signature[0]', data); const signatureId: number | null | undefined = get('suricata.eve.alert.signature_id[0]', data); + if (signatureId != null && signature != null) { return (
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx index 4b1375b33b086..6fe6523180e58 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx @@ -6,7 +6,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { ExternalLinkIcon } from '../../../../external_link_icon'; @@ -18,7 +17,7 @@ const LinkEuiFlexItem = styled(EuiFlexItem)` LinkEuiFlexItem.displayName = 'LinkEuiFlexItem'; -export const SuricataRefs = pure<{ signatureId: number }>(({ signatureId }) => { +export const SuricataRefs = React.memo<{ signatureId: number }>(({ signatureId }) => { const links = getLinksFromSignature(signatureId); return ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx index 512d02b364b2a..2278ed135e5e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx @@ -34,8 +34,8 @@ describe('SuricataSignature', () => { describe('Tokens', () => { test('should render empty if tokens are empty', () => { - const wrapper = mountWithIntl(); - expect(wrapper.isEmptyRender()).toBeTruthy(); + const wrapper = shallow(); + expect(wrapper.children().length).toEqual(0); }); test('should render a single if it is present', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index b7aac4a0da020..b85bef4d5ac36 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -6,7 +6,6 @@ import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DragEffects, DraggableWrapper } from '../../../../drag_and_drop/draggable_wrapper'; @@ -43,7 +42,7 @@ const LinkFlexItem = styled(EuiFlexItem)` LinkFlexItem.displayName = 'LinkFlexItem'; -export const Tokens = pure<{ tokens: string[] }>(({ tokens }) => ( +export const Tokens = React.memo<{ tokens: string[] }>(({ tokens }) => ( <> {tokens.map(token => ( @@ -57,7 +56,7 @@ export const Tokens = pure<{ tokens: string[] }>(({ tokens }) => ( Tokens.displayName = 'Tokens'; -export const DraggableSignatureId = pure<{ id: string; signatureId: number }>( +export const DraggableSignatureId = React.memo<{ id: string; signatureId: number }>( ({ id, signatureId }) => ( ( DraggableSignatureId.displayName = 'DraggableSignatureId'; -export const SuricataSignature = pure<{ +export const SuricataSignature = React.memo<{ contextId: string; id: string; signature: string; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap index b7668fe27eca8..bb0671693978b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap @@ -1,12 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AuthSsh rendering it renders against shallow snapshot 1`] = ` - - - + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap index 180d1814b4c6e..177a3531e5d7b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap @@ -1,94 +1,106 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SystemGenericDetails rendering it renders the default SystemGenericDetails 1`] = ` - + + + + timelineId="test" + /> +
`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap index cf61a61982e7b..19b7f8b6f77c6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap @@ -1,10 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SystemGenericFileDetails rendering it renders the default SystemGenericDetails 1`] = ` - - + + + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap index 0bd651df1d35d..a80bc3da8395c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap @@ -1,11 +1,41 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Package rendering it renders against shallow snapshot 1`] = ` - + + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx index c12cdeb694cd3..e42a91b7d7972 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx @@ -7,109 +7,93 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { TestProviders } from '../../../../../mock'; import { AuthSsh } from './auth_ssh'; describe('AuthSsh', () => { describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( - - - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); test('it returns null if sshSignature and sshMethod are both null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect(wrapper.children().length).toEqual(0); }); test('it returns null if sshSignature and sshMethod are both undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect(wrapper.children().length).toEqual(0); }); test('it returns null if sshSignature is null and sshMethod is undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect(wrapper.children().length).toEqual(0); }); test('it returns null if sshSignature is undefined and sshMethod is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect(wrapper.children().length).toEqual(0); }); test('it returns sshSignature if sshMethod is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); - expect(wrapper.text()).toEqual('[sshSignature-1]'); + expect(wrapper.find('DraggableBadge').prop('value')).toEqual('[sshSignature-1]'); }); test('it returns sshMethod if sshSignature is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); - expect(wrapper.text()).toEqual('[sshMethod-1]'); + expect(wrapper.find('DraggableBadge').prop('value')).toEqual('[sshMethod-1]'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx index 0c637c0ee7193..60eab8381e98d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DraggableBadge } from '../../../../draggables'; import { TokensFlexItem } from '../helpers'; @@ -17,7 +16,7 @@ interface Props { sshMethod: string | null | undefined; } -export const AuthSsh = pure(({ contextId, eventId, sshSignature, sshMethod }) => ( +export const AuthSsh = React.memo(({ contextId, eventId, sshSignature, sshMethod }) => ( <> {sshSignature != null && ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx index 764a93a7294f4..e3627d0ec918a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; @@ -43,7 +42,7 @@ interface Props { workingDirectory: string | null | undefined; } -export const SystemGenericLine = pure( +export const SystemGenericLine = React.memo( ({ contextId, hostName, @@ -141,7 +140,7 @@ interface GenericDetailsProps { timelineId: string; } -export const SystemGenericDetails = pure( +export const SystemGenericDetails = React.memo( ({ data, contextId, text, timelineId }) => { const id = data._id; const message: string | null = data.message != null ? data.message[0] : null; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx index 308f80429d57e..b5d141b4aa2d4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -20,15 +20,13 @@ describe('SystemGenericFileDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy const browserFields: BrowserFields = {}; const wrapper = shallow( - - - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx index 0e75e8de6f9b2..401c1c522ca60 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { BrowserFields } from '../../../../../containers/source'; import { Ecs } from '../../../../../graphql/types'; @@ -64,7 +63,7 @@ interface Props { workingDirectory: string | null | undefined; } -export const SystemGenericFileLine = pure( +export const SystemGenericFileLine = React.memo( ({ args, contextId, @@ -216,7 +215,7 @@ interface GenericDetailsProps { timelineId: string; } -export const SystemGenericFileDetails = pure( +export const SystemGenericFileDetails = React.memo( ({ data, contextId, showMessage = true, text, timelineId }) => { const id = data._id; const message: string | null = data.message != null ? data.message[0] : null; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx index b88d5ad0bab92..a1121f5f43847 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx @@ -28,31 +28,27 @@ describe('Package', () => { }); test('it returns null if all of the package information is null ', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if all of the package information is undefined ', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx index dbcd8de7292f0..d87639d2b8d6e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DraggableBadge } from '../../../../draggables'; import { TokensFlexItem } from '../helpers'; @@ -18,7 +17,7 @@ interface Props { packageVersion: string | null | undefined; } -export const Package = pure( +export const Package = React.memo( ({ contextId, eventId, packageName, packageSummary, packageVersion }) => { if (packageName != null || packageSummary != null || packageVersion != null) { return ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx index 2ea8c6931f00b..93a4b105ce56e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -29,33 +29,29 @@ describe('UserHostWorkingDir', () => { }); test('it returns null if userDomain, userName, hostName, and workingDirectory are all null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if userDomain, userName, hostName, and workingDirectory are all undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx index f9ee78aaa5bde..281cfd39bd9d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DraggableBadge } from '../../../draggables'; import { TokensFlexItem } from './helpers'; @@ -23,7 +22,7 @@ interface Props { workingDirectory: string | null | undefined; } -export const UserHostWorkingDir = pure( +export const UserHostWorkingDir = React.memo( ({ contextId, eventId, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index b9b7598b206ad..6fc9145187a36 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = ` - + + + + + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 7e97208e06d29..d2d593dcd0138 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -16,14 +15,16 @@ import { ZeekDetails } from './zeek_details'; describe('ZeekDetails', () => { describe('rendering', () => { test('it renders the default ZeekDetails', () => { - const wrapper = shallow( - + const wrapper = mountWithIntl( + + + ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('ZeekDetails'))).toMatchSnapshot(); }); test('it returns zeek.connection if the data does contain zeek.connection data', () => { @@ -126,7 +127,12 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect( + wrapper + .find('ZeekDetails') + .children() + .exists() + ).toBeFalsy(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx index ab518dc5ff77a..1a31e560d7810 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx @@ -6,7 +6,6 @@ import { EuiSpacer } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../../../../containers/source'; @@ -21,15 +20,20 @@ const Details = styled.div` Details.displayName = 'Details'; -export const ZeekDetails = pure<{ browserFields: BrowserFields; data: Ecs; timelineId: string }>( - ({ data, timelineId }) => - data.zeek != null ? ( -
- - - -
- ) : null +interface ZeekDetailsProps { + browserFields: BrowserFields; + data: Ecs; + timelineId: string; +} + +export const ZeekDetails = React.memo(({ data, timelineId }) => + data.zeek != null ? ( +
+ + + +
+ ) : null ); ZeekDetails.displayName = 'ZeekDetails'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index 16ff537540ea3..e442f884a8e4b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -71,7 +71,12 @@ describe('ZeekSignature', () => { describe('#TotalVirusLinkSha', () => { test('should return null if value is null', () => { const wrapper = mountWithIntl(); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect( + wrapper + .find('TotalVirusLinkSha') + .children() + .exists() + ).toBeFalsy(); }); test('should render value', () => { @@ -88,7 +93,12 @@ describe('ZeekSignature', () => { describe('#Link', () => { test('should return null if value is null', () => { const wrapper = mountWithIntl(); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect( + wrapper + .find('Link') + .children() + .exists() + ).toBeFalsy(); }); test('should render value', () => { @@ -111,7 +121,12 @@ describe('ZeekSignature', () => { ); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect( + wrapper + .find('DraggableZeekElement') + .children() + .exists() + ).toBeFalsy(); }); test('it renders the default ZeekSignature', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 8a63c08bc9e5d..6a6ae4e4e7da5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -7,7 +7,6 @@ import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { get } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { Ecs } from '../../../../../graphql/types'; @@ -64,7 +63,7 @@ export const md5StringRenderer: StringRenderer = (value: string) => `md5: ${valu export const sha1StringRenderer: StringRenderer = (value: string) => `sha1: ${value.substr(0, 7)}...`; -export const DraggableZeekElement = pure<{ +export const DraggableZeekElement = React.memo<{ id: string; field: string; value: string | null | undefined; @@ -108,37 +107,44 @@ export const DraggableZeekElement = pure<{ DraggableZeekElement.displayName = 'DraggableZeekElement'; -export const Link = pure<{ value: string | null | undefined; link?: string | null }>( - ({ value, link }) => { - if (value != null) { - if (link != null) { - return ( - -
- {value} - -
-
- ); - } else { - return ( - -
- - -
-
- ); - } +interface LinkProps { + value: string | null | undefined; + link?: string | null; +} + +export const Link = React.memo(({ value, link }) => { + if (value != null) { + if (link != null) { + return ( + +
+ {value} + +
+
+ ); } else { - return null; + return ( + +
+ + +
+
+ ); } + } else { + return null; } -); +}); Link.displayName = 'Link'; -export const TotalVirusLinkSha = pure<{ value: string | null | undefined }>(({ value }) => +interface TotalVirusLinkShaProps { + value: string | null | undefined; +} + +export const TotalVirusLinkSha = React.memo(({ value }) => value != null ? (
@@ -188,7 +194,12 @@ export const extractStateValue = (state: string | null | undefined): string | nu export const constructDroppedValue = (dropped: boolean | null | undefined): string | null => dropped != null ? String(dropped) : null; -export const ZeekSignature = pure<{ data: Ecs; timelineId: string }>(({ data, timelineId }) => { +interface ZeekSignatureProps { + data: Ecs; + timelineId: string; +} + +export const ZeekSignature = React.memo(({ data, timelineId }) => { const id = `zeek-signature-draggable-zeek-element-${timelineId}-${data._id}`; const sessionId: string | null | undefined = get('zeek.session_id[0]', data); const dataSet: string | null | undefined = get('event.dataset[0]', data); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap index 9d96afc811178..5674c18010f67 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap @@ -1,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SortIndicator rendering renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx index d9b642c5188ec..fc77bbd725704 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx @@ -6,7 +6,6 @@ import { EuiIcon } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { Direction } from '../../../../graphql/types'; @@ -38,7 +37,7 @@ interface Props { } /** Renders a sort indicator */ -export const SortIndicator = pure(({ sortDirection }) => ( +export const SortIndicator = React.memo(({ sortDirection }) => ( )); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx index e5656455623b5..cd5a677dbb393 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx @@ -30,7 +30,9 @@ import { Body } from './index'; import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { timelineActions, appActions } from '../../../store/actions'; -import { TimelineModel } from '../../../store/timeline/model'; +import { timelineDefaults, TimelineModel } from '../../../store/timeline/model'; +import { plainRowRenderer } from './renderers/plain_row_renderer'; +import { useTimelineTypeContext } from '../timeline_context'; interface OwnProps { browserFields: BrowserFields; @@ -107,6 +109,8 @@ const StatefulBodyComponent = React.memo( updateNote, updateSort, }) => { + const timelineTypeContext = useTimelineTypeContext(); + const getNotesByIds = useCallback( (noteIds: string[]): Note[] => appSelectors.getNotes(notesById, noteIds), [notesById] @@ -167,7 +171,7 @@ const StatefulBodyComponent = React.memo( onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} range={range!} - rowRenderers={rowRenderers} + rowRenderers={timelineTypeContext.showRowRenderers ? rowRenderers : [plainRowRenderer]} sort={sort} toggleColumn={toggleColumn} updateNote={onUpdateNote} @@ -202,7 +206,7 @@ const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const getNotesByIds = appSelectors.notesByIdsSelector(); const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, id); + const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; const { columns, eventIdToNoteIds, pinnedEventIds } = timeline; return { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts index 40e94cb099644..03efc26bbc9b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts @@ -21,11 +21,11 @@ export const COPY_TO_CLIPBOARD = i18n.translate( ); export const UNPINNED = i18n.translate('xpack.siem.timeline.body.pinning.unpinnedTooltip', { - defaultMessage: 'This is event is NOT persisted with the timeline', + defaultMessage: 'Unpinned event', }); export const PINNED = i18n.translate('xpack.siem.timeline.body.pinning.pinnedTooltip', { - defaultMessage: 'This event is persisted with the timeline', + defaultMessage: 'Pinned event', }); export const PINNED_WITH_NOTES = i18n.translate( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap index edaf5e0459b49..24b034d893d6b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap @@ -1,149 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DataProviders rendering renders correctly against snapshot 1`] = ` - + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap index 1100e1307a7f1..5b4405b8d3bc7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap @@ -1,7 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Empty rendering renders correctly against snapshot 1`] = ` - - - + + + + Drop anything + + + + highlighted + + + + + + here to build an + + + + query + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap index 4bd97dfee5d40..16094c585911b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap @@ -1,21 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Provider rendering renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap index 313538306655f..b344381f99d4f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap @@ -1,152 +1,514 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Providers rendering renders correctly against snapshot 1`] = ` - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Drop here + + to build an + + OR + + query + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx index 9c5b8fb30fd61..c249e263d1205 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx @@ -14,11 +14,7 @@ import { TestProviders } from '../../../mock/test_providers'; describe('Empty', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index 3ef7240ee0375..87d45f6d3db17 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -6,7 +6,6 @@ import { EuiBadge, EuiBadgeProps, EuiText } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { AndOrBadge } from '../../and_or_badge'; @@ -88,7 +87,7 @@ interface Props { /** * Prompts the user to drop anything with a facet count into the data providers section. */ -export const Empty = pure(({ showSmallMsg = false }) => ( +export const Empty = React.memo(({ showSmallMsg = false }) => ( `${droppableTimelineProvidersPref * the user to drop anything with a facet count into * the data pro section. */ -export const DataProviders = pure( +export const DataProviders = React.memo( ({ browserFields, id, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.tsx index 7b573b19e2fd4..8fd164eb8a3e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.tsx @@ -6,7 +6,6 @@ import { noop } from 'lodash/fp'; import React from 'react'; -import { pure } from 'recompose'; import { DataProvider, IS_OPERATOR } from './data_provider'; import { ProviderItemBadge } from './provider_item_badge'; @@ -15,7 +14,7 @@ interface OwnProps { dataProvider: DataProvider; } -export const Provider = pure(({ dataProvider }) => ( +export const Provider = React.memo(({ dataProvider }) => ( ( +export const ProviderBadge = React.memo( ({ deleteProvider, field, isEnabled, isExcluded, operator, providerId, togglePopover, val }) => { const deleteFilter: React.MouseEventHandler = ( event: React.MouseEvent diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx index 0d3874943d2ed..17457b900f3a9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -7,7 +7,6 @@ import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { rgba } from 'polished'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { AndOrBadge } from '../../and_or_badge'; @@ -78,7 +77,7 @@ interface ProviderItemDropProps { timelineId: string; } -export const ProviderItemAndDragDrop = pure( +export const ProviderItemAndDragDrop = React.memo( ({ browserFields, dataProvider, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx index 8a99ed7417a63..d6e092550473d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx @@ -21,19 +21,17 @@ describe('Providers', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - - - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index 5a8654509fa88..4d095485ef69d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormHelpText } from '@elastic/eui'; import * as React from 'react'; import { Draggable } from 'react-beautiful-dnd'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { @@ -112,7 +111,7 @@ export const getDraggableId = ({ id, dataProviderId }: GetDraggableIdParams): st * 2) temporarily disabling a data provider * 3) applying boolean negation to the data provider */ -export const Providers = pure( +export const Providers = React.memo( ({ browserFields, id, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx index ce74f26b8db82..978386e611808 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx @@ -27,6 +27,7 @@ import { OnChangeItemsPerPage, OnLoadMore } from '../events'; import { LastUpdatedAt } from './last_updated'; import * as i18n from './translations'; +import { useTimelineTypeContext } from '../timeline_context'; const FixedWidthLastUpdated = styled.div<{ compact: boolean }>` width: ${({ compact }) => (!compact ? 200 : 25)}px; @@ -77,24 +78,6 @@ ServerSideEventCount.displayName = 'ServerSideEventCount'; /** The height of the footer, exported for use in height calculations */ export const footerHeight = 40; // px -interface FooterProps { - compact: boolean; - getUpdatedAt: () => number; - hasNextPage: boolean; - height: number; - isEventViewer?: boolean; - isLive: boolean; - isLoading: boolean; - itemsCount: number; - itemsPerPage: number; - itemsPerPageOptions: number[]; - nextCursor: string; - onChangeItemsPerPage: OnChangeItemsPerPage; - onLoadMore: OnLoadMore; - serverSideEventCount: number; - tieBreaker: string; -} - /** Displays the server-side count of events */ export const EventsCountComponent = ({ closePopover, @@ -110,43 +93,49 @@ export const EventsCountComponent = ({ itemsCount: number; onClick: () => void; serverSideEventCount: number; -}) => ( -
- - - {itemsCount} - - - {` ${i18n.OF} `} - - } - isOpen={isOpen} - closePopover={closePopover} - panelPaddingSize="none" - > - - - - - - {serverSideEventCount} - {' '} - {i18n.EVENTS} - - -
-); +}) => { + const timelineTypeContext = useTimelineTypeContext(); + return ( +
+ + + {itemsCount} + + + {` ${i18n.OF} `} + + } + isOpen={isOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + + + + {serverSideEventCount} + {' '} + {timelineTypeContext.documentType ?? i18n.EVENTS} + + +
+ ); +}; EventsCountComponent.displayName = 'EventsCountComponent'; @@ -183,6 +172,24 @@ export const PagingControl = React.memo(PagingControlComponent); PagingControl.displayName = 'PagingControl'; +interface FooterProps { + compact: boolean; + getUpdatedAt: () => number; + hasNextPage: boolean; + height: number; + isEventViewer?: boolean; + isLive: boolean; + isLoading: boolean; + itemsCount: number; + itemsPerPage: number; + itemsPerPageOptions: number[]; + nextCursor: string; + onChangeItemsPerPage: OnChangeItemsPerPage; + onLoadMore: OnLoadMore; + serverSideEventCount: number; + tieBreaker: string; +} + /** Renders a loading indicator and paging controls */ export const FooterComponent = ({ compact, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/last_updated.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/last_updated.tsx index a17e5a6da6331..06ece50690c09 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/last_updated.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/last_updated.tsx @@ -7,7 +7,6 @@ import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import React, { useEffect, useState } from 'react'; -import { pure } from 'recompose'; import * as i18n from './translations'; @@ -16,7 +15,7 @@ interface LastUpdatedAtProps { updatedAt: number; } -export const Updated = pure<{ date: number; prefix: string; updatedAt: number }>( +export const Updated = React.memo<{ date: number; prefix: string; updatedAt: number }>( ({ date, prefix, updatedAt }) => ( <> {prefix} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index 8c911b4ab06cb..4156c67e9b841 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -14,7 +14,7 @@ import { esFilters } from '../../../../../../../src/plugins/data/public'; import { WithSource } from '../../containers/source'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; import { timelineActions } from '../../store/actions'; -import { KqlMode, TimelineModel } from '../../store/timeline/model'; +import { KqlMode, timelineDefaults, TimelineModel } from '../../store/timeline/model'; import { ColumnHeader } from './body/column_headers/column_header'; import { DataProvider, QueryOperator } from './data_providers/data_provider'; @@ -315,7 +315,7 @@ const makeMapStateToProps = () => { const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); const getInputsTimeline = inputsSelectors.getTimelineSelector(); const mapStateToProps = (state: State, { id }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, id); + const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; const input: inputsModel.InputsRange = getInputsTimeline(state); const { columns, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index c60410bbd2863..0e222f470f0d7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -19,7 +19,6 @@ import { EuiToolTip, } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import uuid from 'uuid'; import { Note } from '../../../lib/note'; @@ -47,7 +46,7 @@ type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean } type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; -export const StarIcon = pure<{ +export const StarIcon = React.memo<{ isFavorite: boolean; timelineId: string; updateIsFavorite: UpdateIsFavorite; @@ -70,61 +69,73 @@ export const StarIcon = pure<{ )); StarIcon.displayName = 'StarIcon'; -export const Description = pure<{ +interface DescriptionProps { description: string; timelineId: string; updateDescription: UpdateDescription; -}>(({ description, timelineId, updateDescription }) => ( - - - updateDescription({ id: timelineId, description: e.target.value })} - placeholder={i18n.DESCRIPTION} - spellCheck={true} - value={description} - /> - - -)); -Description.displayName = 'Description'; +} -export const Name = pure<{ timelineId: string; title: string; updateTitle: UpdateTitle }>( - ({ timelineId, title, updateTitle }) => ( - - updateTitle({ id: timelineId, title: e.target.value })} - placeholder={i18n.UNTITLED_TIMELINE} - spellCheck={true} - value={title} - /> +export const Description = React.memo( + ({ description, timelineId, updateDescription }) => ( + + + updateDescription({ id: timelineId, description: e.target.value })} + placeholder={i18n.DESCRIPTION} + spellCheck={true} + value={description} + /> + ) ); +Description.displayName = 'Description'; + +interface NameProps { + timelineId: string; + title: string; + updateTitle: UpdateTitle; +} + +export const Name = React.memo(({ timelineId, title, updateTitle }) => ( + + updateTitle({ id: timelineId, title: e.target.value })} + placeholder={i18n.UNTITLED_TIMELINE} + spellCheck={true} + value={title} + /> + +)); Name.displayName = 'Name'; -export const NewTimeline = pure<{ +interface NewTimelineProps { createTimeline: CreateTimeline; onClosePopover: () => void; timelineId: string; -}>(({ createTimeline, onClosePopover, timelineId }) => ( - { - createTimeline({ id: timelineId, show: true }); - onClosePopover(); - }} - > - {i18n.NEW_TIMELINE} - -)); +} + +export const NewTimeline = React.memo( + ({ createTimeline, onClosePopover, timelineId }) => ( + { + createTimeline({ id: timelineId, show: true }); + onClosePopover(); + }} + > + {i18n.NEW_TIMELINE} + + ) +); NewTimeline.displayName = 'NewTimeline'; interface NotesButtonProps { @@ -142,47 +153,54 @@ interface NotesButtonProps { const getNewNoteId = (): string => uuid.v4(); -const LargeNotesButton = pure<{ noteIds: string[]; text?: string; toggleShowNotes: () => void }>( - ({ noteIds, text, toggleShowNotes }) => ( - toggleShowNotes()} - size="m" - > - - - - - - {text && text.length ? {text} : null} - - - - {noteIds.length} - - - - - ) -); +interface LargeNotesButtonProps { + noteIds: string[]; + text?: string; + toggleShowNotes: () => void; +} + +const LargeNotesButton = React.memo(({ noteIds, text, toggleShowNotes }) => ( + toggleShowNotes()} + size="m" + > + + + + + + {text && text.length ? {text} : null} + + + + {noteIds.length} + + + + +)); LargeNotesButton.displayName = 'LargeNotesButton'; -const SmallNotesButton = pure<{ noteIds: string[]; toggleShowNotes: () => void }>( - ({ noteIds, toggleShowNotes }) => ( - toggleShowNotes()} - /> - ) -); +interface SmallNotesButtonProps { + noteIds: string[]; + toggleShowNotes: () => void; +} + +const SmallNotesButton = React.memo(({ noteIds, toggleShowNotes }) => ( + toggleShowNotes()} + /> +)); SmallNotesButton.displayName = 'SmallNotesButton'; /** * The internal implementation of the `NotesButton` */ -const NotesButtonComponent = pure( +const NotesButtonComponent = React.memo( ({ animate = true, associateNote, @@ -220,7 +238,7 @@ const NotesButtonComponent = pure( ); NotesButtonComponent.displayName = 'NotesButtonComponent'; -export const NotesButton = pure( +export const NotesButton = React.memo( ({ animate = true, associateNote, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index 31d2b7a2d85f2..31d1002f16179 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -21,7 +21,7 @@ import { inputsSelectors, } from '../../../store'; import { timelineActions } from '../../../store/actions'; -import { KqlMode, TimelineModel } from '../../../store/timeline/model'; +import { KqlMode, timelineDefaults, TimelineModel } from '../../../store/timeline/model'; import { DispatchUpdateReduxTime, dispatchUpdateReduxTime } from '../../super_date_picker'; import { DataProvider } from '../data_providers/data_provider'; import { SearchOrFilter } from './search_or_filter'; @@ -195,7 +195,7 @@ const makeMapStateToProps = () => { const getInputsTimeline = inputsSelectors.getTimelineSelector(); const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId); + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; const input: inputsModel.InputsRange = getInputsTimeline(state); const policy: inputsModel.Policy = getInputsPolicy(state); return { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 373d9a50d245b..b645202ab4c54 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -6,7 +6,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled, { createGlobalStyle } from 'styled-components'; import { esFilters, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; @@ -84,7 +83,7 @@ const ModeFlexItem = styled(EuiFlexItem)` ModeFlexItem.displayName = 'ModeFlexItem'; -export const SearchOrFilter = pure( +export const SearchOrFilter = React.memo( ({ applyKqlFilterQuery, browserFields, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx index b6006dea8b8ac..584fe03d2149b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, useContext, useEffect, memo, useState } from 'react'; +import React, { createContext, memo, useContext, useEffect, useState } from 'react'; const initTimelineContext = false; export const TimelineContext = createContext(initTimelineContext); @@ -14,30 +14,55 @@ const initTimelineWidth = 0; export const TimelineWidthContext = createContext(initTimelineWidth); export const useTimelineWidthContext = () => useContext(TimelineWidthContext); +export interface TimelineTypeContextProps { + documentType?: string; + footerText?: string; + showCheckboxes: boolean; + showRowRenderers: boolean; + title?: string; +} +const initTimelineType: TimelineTypeContextProps = { + documentType: undefined, + footerText: undefined, + showCheckboxes: false, + showRowRenderers: true, + title: undefined, +}; +export const TimelineTypeContext = createContext(initTimelineType); +export const useTimelineTypeContext = () => useContext(TimelineTypeContext); + interface ManageTimelineContextProps { children: React.ReactNode; loading: boolean; width: number; + type?: TimelineTypeContextProps; } // todo we need to refactor this as more complex context/reducer with useReducer // to avoid so many Context, at least the separation of code is there now export const ManageTimelineContext = memo( - ({ children, loading, width }) => { + ({ children, loading, width, type = initTimelineType }) => { const [myLoading, setLoading] = useState(initTimelineContext); const [myWidth, setWidth] = useState(initTimelineWidth); + const [myType, setType] = useState(initTimelineType); useEffect(() => { setLoading(loading); }, [loading]); + useEffect(() => { + setType(type); + }, [type]); + useEffect(() => { setWidth(width); }, [width]); return ( - {children} + + {children} + ); } diff --git a/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx index 1db521d76d522..07ea165fcbb5c 100644 --- a/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx @@ -5,7 +5,6 @@ */ import React, { useState, useCallback } from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; interface Props { @@ -36,11 +35,13 @@ const HoverActionsPanelContainer = styled.div` HoverActionsPanelContainer.displayName = 'HoverActionsPanelContainer'; -const HoverActionsPanel = pure<{ children: JSX.Element; show: boolean }>(({ children, show }) => ( - - {show ? children : null} - -)); +const HoverActionsPanel = React.memo<{ children: JSX.Element; show: boolean }>( + ({ children, show }) => ( + + {show ? children : null} + + ) +); HoverActionsPanel.displayName = 'HoverActionsPanel'; diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap index e5311bfb050a3..1eef27bcc0a87 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap @@ -1,47 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`WrapperPage it renders 1`] = ` - - -

- Test page -

-
-
+ +

+ Test page +

+
`; exports[`WrapperPage restrict width custom max width when restrictWidth is number 1`] = ` - - -

- Test page -

-
-
+ +

+ Test page +

+
`; exports[`WrapperPage restrict width custom max width when restrictWidth is string 1`] = ` - - -

- Test page -

-
-
+ +

+ Test page +

+
`; exports[`WrapperPage restrict width default max width when restrictWidth is true 1`] = ` - - -

- Test page -

-
-
+ +

+ Test page +

+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx index 95e80e8b9e5de..3ea243fe5cfe7 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx @@ -24,7 +24,7 @@ describe('WrapperPage', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('WrapperPage'))).toMatchSnapshot(); }); describe('restrict width', () => { @@ -37,7 +37,7 @@ describe('WrapperPage', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('WrapperPage'))).toMatchSnapshot(); }); test('custom max width when restrictWidth is number', () => { @@ -49,7 +49,7 @@ describe('WrapperPage', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('WrapperPage'))).toMatchSnapshot(); }); test('custom max width when restrictWidth is string', () => { @@ -61,7 +61,7 @@ describe('WrapperPage', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('WrapperPage'))).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx index 309693427459e..d777c8d6c9eab 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx @@ -27,9 +27,10 @@ const Wrapper = styled.div` } `} `; + Wrapper.displayName = 'Wrapper'; -export interface WrapperPageProps { +interface WrapperPageProps { children: React.ReactNode; className?: string; restrictWidth?: boolean | number | string; @@ -60,4 +61,5 @@ export const WrapperPage = React.memo( ); } ); + WrapperPage.displayName = 'WrapperPage'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 798cf91612a85..a26f376f962d7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -16,15 +16,17 @@ import { Rule, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; /** * Add provided Rule * * @param rule to add * @param kbnVersion current Kibana Version to use for headers + * @param signal to cancel request */ export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: 'POST', credentials: 'same-origin', headers: { @@ -47,6 +49,7 @@ export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Prom * @param pagination desired pagination options (e.g. page/perPage) * @param id if specified, will return specific rule if exists * @param kbnVersion current Kibana Version to use for headers + * @param signal to cancel request */ export const fetchRules = async ({ filterOptions = { @@ -75,8 +78,8 @@ export const fetchRules = async ({ const endpoint = id != null - ? `${chrome.getBasePath()}/api/detection_engine/rules?id="${id}"` - : `${chrome.getBasePath()}/api/detection_engine/rules/_find?${queryParams.join('&')}`; + ? `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id="${id}"` + : `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`; const response = await fetch(endpoint, { method: 'GET', @@ -106,7 +109,7 @@ export const enableRules = async ({ kbnVersion, }: EnableRulesProps): Promise => { const requests = ids.map(id => - fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: 'PUT', credentials: 'same-origin', headers: { @@ -134,7 +137,7 @@ export const enableRules = async ({ export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promise => { // TODO: Don't delete if immutable! const requests = ids.map(id => - fetch(`${chrome.getBasePath()}/api/detection_engine/rules?id=${id}`, { + fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { method: 'DELETE', credentials: 'same-origin', headers: { @@ -163,7 +166,7 @@ export const duplicateRules = async ({ kbnVersion, }: DuplicateRulesProps): Promise => { const requests = rules.map(rule => - fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: 'POST', credentials: 'same-origin', headers: { diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index 8cbda9cd5afe9..dd744f4d7ecd5 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -8,7 +8,12 @@ import { isEmpty, get } from 'lodash/fp'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { getIndexFields, sourceQuery } from '../../../containers/source'; +import { + BrowserFields, + getBrowserFields, + getIndexFields, + sourceQuery, +} from '../../../containers/source'; import { useStateToaster } from '../../../components/toasters'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import { SourceQuery } from '../../../graphql/types'; @@ -16,20 +21,22 @@ import { useApolloClient } from '../../../utils/apollo_context'; import * as i18n from './translations'; -interface FetchIndexPattern { +interface FetchIndexPatternReturn { + browserFields: BrowserFields | null; isLoading: boolean; indices: string[]; indicesExists: boolean; indexPatterns: IIndexPattern | null; } -type Return = [FetchIndexPattern, Dispatch>]; +type Return = [FetchIndexPatternReturn, Dispatch>]; -export const useFetchIndexPatterns = (): Return => { +export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => { const apolloClient = useApolloClient(); - const [indices, setIndices] = useState([]); + const [indices, setIndices] = useState(defaultIndices); const [indicesExists, setIndicesExists] = useState(false); const [indexPatterns, setIndexPatterns] = useState(null); + const [browserFields, setBrowserFields] = useState(null); const [isLoading, setIsLoading] = useState(false); const [, dispatchToaster] = useStateToaster(); @@ -62,6 +69,7 @@ export const useFetchIndexPatterns = (): Return => { setIndexPatterns( getIndexFields(indices.join(), get('data.source.status.indexFields', result)) ); + setBrowserFields(getBrowserFields(get('data.source.status.indexFields', result))); } }, error => { @@ -80,5 +88,5 @@ export const useFetchIndexPatterns = (): Return => { }; }, [indices]); - return [{ isLoading, indices, indicesExists, indexPatterns }, setIndices]; + return [{ browserFields, isLoading, indices, indicesExists, indexPatterns }, setIndices]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx b/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx index d2494f34ad087..003032493fca2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx @@ -8,7 +8,6 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { Query } from 'react-apollo'; import { connect } from 'react-redux'; -import { pure } from 'recompose'; import chrome from 'ui/chrome'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; @@ -39,7 +38,7 @@ export interface IpOverviewProps extends QueryTemplateProps { ip: string; } -const IpOverviewComponentQuery = pure( +const IpOverviewComponentQuery = React.memo( ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( query={ipOverviewQuery} diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx index 0d87c0ab9a1c2..20ed7fa991d15 100644 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx @@ -8,7 +8,6 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { Query } from 'react-apollo'; import { connect } from 'react-redux'; -import { pure } from 'recompose'; import chrome from 'ui/chrome'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; @@ -37,7 +36,7 @@ export interface KpiHostDetailsReducer { isInspected: boolean; } -const KpiHostDetailsComponentQuery = pure( +const KpiHostDetailsComponentQuery = React.memo( ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( query={kpiHostDetailsQuery} diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx index 77575f0137af0..f8aa8aa38e8e1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx @@ -8,9 +8,8 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { Query } from 'react-apollo'; import { connect } from 'react-redux'; - import chrome from 'ui/chrome'; -import { pure } from 'recompose'; + import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { GetKpiHostsQuery, KpiHostsData } from '../../graphql/types'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -37,7 +36,7 @@ export interface KpiHostsProps extends QueryTemplateProps { children: (args: KpiHostsArgs) => React.ReactNode; } -const KpiHostsComponentQuery = pure( +const KpiHostsComponentQuery = React.memo( ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( query={kpiHostsQuery} diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx index e48ea19ba11ef..269c3593d4d73 100644 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx @@ -8,9 +8,8 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { Query } from 'react-apollo'; import { connect } from 'react-redux'; -import { pure } from 'recompose'; - import chrome from 'ui/chrome'; + import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { GetKpiNetworkQuery, KpiNetworkData } from '../../graphql/types'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -37,7 +36,7 @@ export interface KpiNetworkProps extends QueryTemplateProps { children: (args: KpiNetworkArgs) => React.ReactNode; } -const KpiNetworkComponentQuery = pure( +const KpiNetworkComponentQuery = React.memo( ({ id = ID, children, filterQuery, isInspected, skip, sourceId, startDate, endDate }) => ( query={kpiNetworkQuery} diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx index 4659d063ca1fd..6ef0ee7b33589 100644 --- a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx @@ -8,9 +8,8 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { Query } from 'react-apollo'; import { connect } from 'react-redux'; -import { pure } from 'recompose'; - import chrome from 'ui/chrome'; + import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { GetOverviewHostQuery, OverviewHostData } from '../../../graphql/types'; import { inputsModel, inputsSelectors } from '../../../store/inputs'; @@ -41,7 +40,7 @@ export interface OverviewHostProps extends QueryTemplateProps { startDate: number; } -const OverviewHostComponentQuery = pure( +const OverviewHostComponentQuery = React.memo( ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( query={overviewHostQuery} diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx b/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx index a292bd788b9eb..677d96c10eee2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx @@ -8,7 +8,6 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { Query } from 'react-apollo'; import { connect } from 'react-redux'; -import { pure } from 'recompose'; import chrome from 'ui/chrome'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; @@ -41,37 +40,37 @@ export interface OverviewNetworkProps extends QueryTemplateProps { startDate: number; } -export const OverviewNetworkComponentQuery = pure( - ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( - - query={overviewNetworkQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const overviewNetwork = getOr({}, `source.OverviewNetwork`, data); - return children({ - id, - inspect: getOr(null, 'source.OverviewNetwork.inspect', data), - overviewNetwork, - loading, - refetch, - }); - }} - - ) -); +export const OverviewNetworkComponentQuery = React.memo< + OverviewNetworkProps & OverviewNetworkReducer +>(({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( + + query={overviewNetworkQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const overviewNetwork = getOr({}, `source.OverviewNetwork`, data); + return children({ + id, + inspect: getOr(null, 'source.OverviewNetwork.inspect', data), + overviewNetwork, + loading, + refetch, + }); + }} + +)); OverviewNetworkComponentQuery.displayName = 'OverviewNetworkComponentQuery'; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index 653d5543c0923..8bddbd14a367c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -70,7 +70,7 @@ export const getIndexFields = memoizeOne( : { fields: [], title } ); -const getBrowserFields = memoizeOne( +export const getBrowserFields = memoizeOne( (fields: IndexField[]): BrowserFields => fields && fields.length > 0 ? fields.reduce( diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx index 21ef4ffdd8e37..40ed3b3747c10 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx @@ -19,11 +19,12 @@ import { TimelineEdges, TimelineItem, } from '../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../store'; +import { inputsModel, inputsSelectors, State } from '../../store'; import { createFilter } from '../helpers'; import { QueryTemplate, QueryTemplateProps } from '../query_template'; import { timelineQuery } from './index.gql_query'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; export interface TimelineArgs { events: TimelineItem[]; @@ -44,6 +45,7 @@ export interface TimelineQueryReduxProps { export interface OwnProps extends QueryTemplateProps { children?: (args: TimelineArgs) => React.ReactNode; id: string; + indexPattern?: IIndexPattern; limit: number; sortField: SortField; fields: string[]; @@ -67,6 +69,7 @@ class TimelineQueryComponent extends QueryTemplate< const { children, id, + indexPattern, isInspected, limit, fields, @@ -80,7 +83,8 @@ class TimelineQueryComponent extends QueryTemplate< sourceId, pagination: { limit, cursor: null, tiebreaker: null }, sortField, - defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY), + defaultIndex: + indexPattern?.title.split(',') ?? chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY), inspect: isInspected, }; return ( diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx b/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx index 894f1fcdea373..c392c2511551f 100644 --- a/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { Clipboard } from './clipboard'; @@ -23,7 +22,7 @@ WithCopyToClipboardContainer.displayName = 'WithCopyToClipboardContainer'; * Renders `children` with an adjacent icon that when clicked, copies `text` to * the clipboard and displays a confirmation toast */ -export const WithCopyToClipboard = pure<{ text: string; titleSummary?: string }>( +export const WithCopyToClipboard = React.memo<{ text: string; titleSummary?: string }>( ({ text, titleSummary, children }) => ( <>{children} diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx index e573afaafc2d2..d4c06d998c5a2 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx @@ -13,7 +13,6 @@ import * as React from 'react'; import { ApolloProvider } from 'react-apollo'; import { DragDropContext, DropResult, ResponderProvided } from 'react-beautiful-dnd'; import { Provider as ReduxStoreProvider } from 'react-redux'; -import { pure } from 'recompose'; import { Store } from 'redux'; import { BehaviorSubject } from 'rxjs'; import { ThemeProvider } from 'styled-components'; @@ -86,7 +85,7 @@ Object.defineProperty(window, 'localStorage', { }); /** A utility for wrapping children in the providers required to run most tests */ -export const TestProviders = pure( +export const TestProviders = React.memo( ({ children, store = createStore(state, apolloClientObservable), onDragEnd = jest.fn() }) => ( @@ -102,7 +101,7 @@ export const TestProviders = pure( ) ); -export const TestProviderWithoutDragAndDrop = pure( +export const TestProviderWithoutDragAndDrop = React.memo( ({ children, store = createStore(state, apolloClientObservable) }) => ( {children} diff --git a/x-pack/legacy/plugins/siem/public/pages/404.tsx b/x-pack/legacy/plugins/siem/public/pages/404.tsx index f806a5a7fcdd3..ba1cb4f40cbed 100644 --- a/x-pack/legacy/plugins/siem/public/pages/404.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/404.tsx @@ -5,12 +5,11 @@ */ import React from 'react'; -import { pure } from 'recompose'; import { FormattedMessage } from '@kbn/i18n/react'; import { WrapperPage } from '../components/wrapper_page'; -export const NotFoundPage = pure(() => ( +export const NotFoundPage = React.memo(() => ( ( /> )); + NotFoundPage.displayName = 'NotFoundPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index f02e80ebfaf66..e1373c8b18bb7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -4,15 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButton, - EuiFilterButton, - EuiFilterGroup, - EuiPanel, - EuiSelect, - EuiSpacer, -} from '@elastic/eui'; -import React, { useState } from 'react'; +import { EuiButton, EuiPanel, EuiSelect, EuiSpacer } from '@elastic/eui'; +import React from 'react'; import { StickyContainer } from 'react-sticky'; import { FiltersGlobal } from '../../components/filters_global'; @@ -20,100 +13,12 @@ import { HeaderPage } from '../../components/header_page'; import { HeaderSection } from '../../components/header_section'; import { HistogramSignals } from '../../components/page/detection_engine/histogram_signals'; import { SiemSearchBar } from '../../components/search_bar'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../components/detection_engine/utility_bar'; import { WrapperPage } from '../../components/wrapper_page'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { SpyRoute } from '../../utils/route/spy_routes'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import * as i18n from './translations'; - -const OpenSignals = React.memo(() => { - return ( - <> - - - - {`${i18n.PANEL_SUBTITLE_SHOWING}: 7,712 signals`} - - - - {'Selected: 20 signals'} - -

{'Batch actions context menu here.'}

} - > - {'Batch actions'} -
- - - {'Select all signals on all pages'} - -
- - - {'Clear 7 filters'} - - {'Clear aggregation'} - -
- - - -

{'Customize columns context menu here.'}

} - > - {'Customize columns'} -
- - {'Aggregate data'} -
-
-
- - {/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} - - ); -}); - -const ClosedSignals = React.memo(() => { - return ( - <> - - - - {`${i18n.PANEL_SUBTITLE_SHOWING}: 7,712 signals`} - - - - - -

{'Customize columns context menu here.'}

} - > - {'Customize columns'} -
- - {'Aggregate data'} -
-
-
- - {/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} - - ); -}); +import { SignalsTable } from './signals'; export const DetectionEngineComponent = React.memo(() => { const sampleChartOptions = [ @@ -129,9 +34,6 @@ export const DetectionEngineComponent = React.memo(() => { { text: 'Top users', value: 'users' }, ]; - const filterGroupOptions = ['open', 'closed']; - const [filterGroupState, setFilterGroupState] = useState(filterGroupOptions[0]); - return ( <> @@ -164,28 +66,7 @@ export const DetectionEngineComponent = React.memo(() => { - - - - setFilterGroupState(filterGroupOptions[0])} - withNext - > - {'Open signals'} - - - setFilterGroupState(filterGroupOptions[1])} - > - {'Closed signals'} - - - - - {filterGroupState === filterGroupOptions[0] ? : } - + ) : ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/closed_signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/closed_signals/index.tsx new file mode 100644 index 0000000000000..7ccc95a1d7083 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/closed_signals/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../../components/detection_engine/utility_bar'; +import * as i18n from '../../translations'; + +export const ClosedSignals = React.memo<{ totalCount: number }>(({ totalCount }) => { + return ( + <> + + + + {`${i18n.PANEL_SUBTITLE_SHOWING}: ${totalCount} signals`} + + + + + +

{'Customize columns context menu here.'}

} + > + {'Customize columns'} +
+ + {'Aggregate data'} +
+
+
+ + {/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/open_signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/open_signals/index.tsx new file mode 100644 index 0000000000000..f65f511ab33a9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/open_signals/index.tsx @@ -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 React from 'react'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../../components/detection_engine/utility_bar'; +import * as i18n from '../../translations'; + +export const OpenSignals = React.memo<{ totalCount: number }>(({ totalCount }) => { + return ( + <> + + + + {`${i18n.PANEL_SUBTITLE_SHOWING}: ${totalCount} signals`} + + + + {'Selected: 20 signals'} + +

{'Batch actions context menu here.'}

} + > + {'Batch actions'} +
+ + + {'Select all signals on all pages'} + +
+
+
+ + {/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx new file mode 100644 index 0000000000000..e90487a3b023c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx @@ -0,0 +1,126 @@ +/* + * 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 { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header'; +import { defaultColumnHeaderType } from '../../../components/timeline/body/column_headers/default_headers'; +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_DATE_COLUMN_MIN_WIDTH, +} from '../../../components/timeline/body/helpers'; + +import * as i18n from './translations'; +import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model'; +import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query'; + +export const signalsOpenFilters: esFilters.Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.status', + params: { + query: 'open', + }, + }, + query: { + match_phrase: { + 'signal.status': 'open', + }, + }, + }, +]; + +export const signalsClosedFilters: esFilters.Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.status', + params: { + query: 'closed', + }, + }, + query: { + match_phrase: { + 'signal.status': 'closed', + }, + }, + }, +]; + +export const signalsHeaders: ColumnHeader[] = [ + { + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.name', + label: i18n.SIGNALS_HEADERS_RULE, + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.type', + label: i18n.SIGNALS_HEADERS_METHOD, + width: 80, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.severity', + label: i18n.SIGNALS_HEADERS_SEVERITY, + width: 80, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.risk_score', + label: i18n.SIGNALS_HEADERS_RISK_SCORE, + width: 120, + }, + { + category: 'event', + columnHeaderType: defaultColumnHeaderType, + id: 'event.action', + type: 'string', + aggregatable: true, + width: 140, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.category', + width: 150, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'host.name', + width: 120, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'user.name', + width: 120, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'source.ip', + width: 120, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'destination.ip', + width: 120, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: '@timestamp', + width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, +]; + +export const signalsDefaultModel: SubsetTimelineModel = { + ...timelineDefaults, + columns: signalsHeaders, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx new file mode 100644 index 0000000000000..ca178db9cd97f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx @@ -0,0 +1,97 @@ +/* + * 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 { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { OpenSignals } from './components/open_signals'; +import { ClosedSignals } from './components/closed_signals'; +import { GlobalTime } from '../../../containers/global_time'; +import { StatefulEventsViewer } from '../../../components/events_viewer'; +import * as i18n from './translations'; +import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; +import { signalsClosedFilters, signalsDefaultModel, signalsOpenFilters } from './default_config'; + +const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; +const FILTER_OPEN = 'open'; +const FILTER_CLOSED = 'closed'; + +export const SignalsTableFilterGroup = React.memo( + ({ onFilterGroupChanged }: { onFilterGroupChanged: (filterGroup: string) => void }) => { + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + + return ( + + { + setFilterGroup(FILTER_OPEN); + onFilterGroupChanged(FILTER_OPEN); + }} + withNext + > + {'Open signals'} + + + { + setFilterGroup(FILTER_CLOSED); + onFilterGroupChanged(FILTER_CLOSED); + }} + > + {'Closed signals'} + + + ); + } +); + +export const SignalsTable = React.memo(() => { + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: string) => { + setFilterGroup(newFilterGroup); + }, + [setFilterGroup] + ); + + return ( + <> + + {({ to, from, setQuery, deleteQuery, isInitializing }) => ( + + } + id={SIGNALS_PAGE_TIMELINE_ID} + start={from} + timelineTypeContext={{ + documentType: i18n.SIGNALS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_SIGNALS, + showCheckboxes: true, + showRowRenderers: false, + title: i18n.SIGNALS_TABLE_TITLE, + }} + utilityBar={(totalCount: number) => + filterGroup === FILTER_OPEN ? ( + + ) : ( + + ) + } + /> + )} + + + ); +}); + +SignalsTable.displayName = 'SignalsTable'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/translations.ts new file mode 100644 index 0000000000000..1806ba85f8b55 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/translations.ts @@ -0,0 +1,64 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', { + defaultMessage: 'Detection engine', +}); + +export const PANEL_SUBTITLE_SHOWING = i18n.translate( + 'xpack.siem.detectionEngine.panelSubtitleShowing', + { + defaultMessage: 'Showing', + } +); + +export const SIGNALS_TABLE_TITLE = i18n.translate('xpack.siem.detectionEngine.signals.tableTitle', { + defaultMessage: 'All signals', +}); + +export const SIGNALS_DOCUMENT_TYPE = i18n.translate( + 'xpack.siem.detectionEngine.signals.documentTypeTitle', + { + defaultMessage: 'Signals', + } +); + +export const TOTAL_COUNT_OF_SIGNALS = i18n.translate( + 'xpack.siem.detectionEngine.signals.totalCountOfSignalsTitle', + { + defaultMessage: 'signals match the search criteria', + } +); + +export const SIGNALS_HEADERS_RULE = i18n.translate( + 'xpack.siem.eventsViewer.signals.defaultHeaders.ruleTitle', + { + defaultMessage: 'Rule', + } +); + +export const SIGNALS_HEADERS_METHOD = i18n.translate( + 'xpack.siem.eventsViewer.signals.defaultHeaders.methodTitle', + { + defaultMessage: 'Method', + } +); + +export const SIGNALS_HEADERS_SEVERITY = i18n.translate( + 'xpack.siem.eventsViewer.signals.defaultHeaders.severityTitle', + { + defaultMessage: 'Severity', + } +); + +export const SIGNALS_HEADERS_RISK_SCORE = i18n.translate( + 'xpack.siem.eventsViewer.signals.defaultHeaders.riskScoreTitle', + { + defaultMessage: 'Risk Score', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 2cc98930767dc..97f0a21928a8a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -6,7 +6,6 @@ import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { AutoSizer } from '../../components/auto_sizer'; @@ -56,7 +55,7 @@ const calculateFlyoutHeight = ({ windowHeight: number; }): number => Math.max(0, windowHeight - globalHeaderSize); -export const HomePage = pure(() => ( +export const HomePage: React.FC = () => ( {({ measureRef, windowMeasurement: { height: windowHeight = 0 } }) => ( @@ -115,7 +114,7 @@ export const HomePage = pure(() => ( path={`/:pageName(${SiemPageName.timelines})`} render={() => } /> - + } /> ( @@ -128,7 +127,7 @@ export const HomePage = pure(() => ( )} /> - + } /> )} @@ -141,5 +140,6 @@ export const HomePage = pure(() => ( )} -)); +); + HomePage.displayName = 'HomePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_empty_page.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_empty_page.tsx index 69ef8579ea8cc..ecd1e4f378cc5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_empty_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_empty_page.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { pure } from 'recompose'; import chrome from 'ui/chrome'; import { documentationLinks } from 'ui/documentation_links'; @@ -15,7 +14,7 @@ import * as i18n from './translations'; const basePath = chrome.getBasePath(); -export const HostsEmptyPage = pure(() => ( +export const HostsEmptyPage = React.memo(() => ( - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx index 6e121608e431d..34e7f49bd9bd5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { pure } from 'recompose'; import chrome from 'ui/chrome'; import { documentationLinks } from 'ui/documentation_links'; @@ -15,7 +14,7 @@ import * as i18n from './translations'; const basePath = chrome.getBasePath(); -export const NetworkEmptyPage = pure(() => ( +export const NetworkEmptyPage = React.memo(() => ( ( +export const Summary = React.memo(() => (

diff --git a/x-pack/legacy/plugins/siem/public/routes.tsx b/x-pack/legacy/plugins/siem/public/routes.tsx index 9a132eb8d4fac..0e9bcf5dc5bfa 100644 --- a/x-pack/legacy/plugins/siem/public/routes.tsx +++ b/x-pack/legacy/plugins/siem/public/routes.tsx @@ -20,8 +20,8 @@ export const PageRouter: FC = memo(({ history }) => ( - - + } /> + } /> diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts index 405564a4b5b0d..401edb73a0de8 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts @@ -9,7 +9,6 @@ import { ColumnHeader } from '../../components/timeline/body/column_headers/colu import { DataProvider } from '../../components/timeline/data_providers/data_provider'; import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/helpers'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; -import { defaultHeaders as eventsDefaultHeaders } from '../../components/events_viewer/default_headers'; import { Sort } from '../../components/timeline/body/sort'; import { Direction, PinnedEvent } from '../../graphql/types'; import { KueryFilterQuery, SerializedFilterQuery } from '../model'; @@ -74,34 +73,37 @@ export interface TimelineModel { version: string | null; } -export const timelineDefaults: Readonly> = { +export type SubsetTimelineModel = Readonly< + Pick< + TimelineModel, + | 'columns' + | 'dataProviders' + | 'description' + | 'eventIdToNoteIds' + | 'highlightedDropAndProviderId' + | 'historyIds' + | 'isFavorite' + | 'isLive' + | 'itemsPerPage' + | 'itemsPerPageOptions' + | 'kqlMode' + | 'kqlQuery' + | 'title' + | 'noteIds' + | 'pinnedEventIds' + | 'pinnedEventsSaveObject' + | 'dateRange' + | 'show' + | 'sort' + | 'width' + | 'isSaving' + | 'isLoading' + | 'savedObjectId' + | 'version' + > +>; + +export const timelineDefaults: SubsetTimelineModel & Pick = { columns: defaultHeaders, dataProviders: [], description: '', @@ -137,31 +139,3 @@ export const timelineDefaults: Readonly> = { ...timelineDefaults, columns: eventsDefaultHeaders }; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts b/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts index c248387e6b3fd..780145ebfa54c 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts @@ -9,8 +9,8 @@ import { createSelector } from 'reselect'; import { isFromKueryExpressionValid } from '../../lib/keury'; import { State } from '../reducer'; -import { eventsDefaults, timelineDefaults, TimelineModel } from './model'; -import { TimelineById, AutoSavedWarningMsg } from './types'; +import { TimelineModel } from './model'; +import { AutoSavedWarningMsg, TimelineById } from './types'; const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById; @@ -37,11 +37,9 @@ export const getShowCallOutUnauthorizedMsg = () => export const getTimelines = () => timelineByIdSelector; -export const getTimelineByIdSelector = () => - createSelector(selectTimeline, timeline => timeline || timelineDefaults); +export const getTimelineByIdSelector = () => createSelector(selectTimeline, timeline => timeline); -export const getEventsByIdSelector = () => - createSelector(selectTimeline, timeline => timeline || eventsDefaults); +export const getEventsByIdSelector = () => createSelector(selectTimeline, timeline => timeline); export const getKqlFilterQuerySelector = () => createSelector(selectTimeline, timeline => diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js index 3e1c5f51ebb5c..b282a8bf1e861 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -42,9 +42,10 @@ const allRulesNdJson = 'all_rules.ndjson'; // For converting, if you want to use these instead of rely on the defaults then // comment these in and use them for the script. Otherwise this is commented out // so we can utilize the defaults of input and output which are based on saved objects -// of siem:defaultIndex and siem:defaultSignalsIndex +// of siem:defaultIndex and your kibana.dev.yml setting of xpack.siem.signalsIndex. If +// the setting of xpack.siem.signalsIndex is not set it defaults to .siem-signals // const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; -// const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; +// const OUTPUT_INDEX = '.siem-signals-some-other-index'; const walk = dir => { const list = fs.readdirSync(dir); diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index 3d73b9f4d90b0..d6db1c4ef5bb9 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -18,11 +18,15 @@ import { import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type'; import { isAlertExecutor } from './lib/detection_engine/alerts/types'; import { createRulesRoute } from './lib/detection_engine/routes/create_rules_route'; +import { createIndexRoute } from './lib/detection_engine/routes/index/create_index_route'; +import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_route'; import { readRulesRoute } from './lib/detection_engine/routes/read_rules_route'; import { findRulesRoute } from './lib/detection_engine/routes/find_rules_route'; import { deleteRulesRoute } from './lib/detection_engine/routes/delete_rules_route'; import { updateRulesRoute } from './lib/detection_engine/routes/update_rules_route'; +import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; import { ServerFacade } from './types'; +import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; const APP_ID = 'siem'; @@ -43,15 +47,25 @@ export const initServerWithKibana = ( const libs = compose(kbnServer, mode); initServer(libs); - // Signals/Alerting Rules routes for - // routes such as ${DETECTION_ENGINE_RULES_URL} - // that have the REST endpoints of /api/detection_engine/rules + // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules + // All REST rule creation, deletion, updating, etc... createRulesRoute(kbnServer); readRulesRoute(kbnServer); updateRulesRoute(kbnServer); deleteRulesRoute(kbnServer); findRulesRoute(kbnServer); + // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals + // POST /api/detection_engine/signals/status + // Example usage can be found in siem/server/lib/detection_engine/scripts/signals + setSignalsStatusRoute(kbnServer); + + // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index + // All REST index creation, policy management for spaces + createIndexRoute(kbnServer); + readIndexRoute(kbnServer); + deleteIndexRoute(kbnServer); + const xpackMainPlugin = kbnServer.plugins.xpack_main; xpackMainPlugin.registerFeature({ id: APP_ID, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 75757bbaa0c1f..d82424c1c3bd3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -2,11 +2,12 @@ README.md for developers working on the backend detection engine on how to get s using the CURL scripts in the scripts folder. The scripts rely on CURL and jq: -* [CURL](https://curl.haxx.se) -* [jq](https://stedolan.github.io/jq/) +- [CURL](https://curl.haxx.se) +- [jq](https://stedolan.github.io/jq/) Install curl and jq + ```sh brew update brew install curl @@ -21,7 +22,6 @@ export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} export ELASTICSEARCH_URL=https://${ip}:9200 export KIBANA_URL=http://localhost:5601 -export SIGNALS_INDEX=.siem-signals-${your user id} export TASK_MANAGER_INDEX=.kibana-task-manager-${your user id} export KIBANA_INDEX=.kibana-${your user id} ``` @@ -32,6 +32,12 @@ source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set: source ~/.zshrc ``` +Open your `kibana.dev.yml` file and add these lines: + +```sh +xpack.siem.signalsIndex: .siem-signals-${your user id} +``` + Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will get in the way of the CURL scripts written as is. You should see alerting and actions starting up like so afterwards @@ -40,18 +46,11 @@ server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status changed f server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready ``` -Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same -value as you did with the environment variable of `${SIGNALS_INDEX}`, which should be `.siem-signals-${your user id}` - -``` -.siem-signals-${your user id} -``` - Go to the scripts folder `cd kibana/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` and run: ```sh ./hard_reset.sh -./post_signal.sh +./post_rule.sh ``` which will: @@ -59,9 +58,9 @@ which will: - Delete any existing actions you have - Delete any existing alerts you have - Delete any existing alert tasks you have -- Delete any existing signal mapping you might have had. -- Add the latest signal index and its mappings using your settings from `${SIGNALS_INDEX}` environment variable. -- Posts the sample rule from `rules/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable +- Delete any existing signal mapping, policies, and template, you might have previously had. +- Add the latest signal index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.siem.signalsIndex`. +- Posts the sample rule from `rules/root_or_admin_1.json` - The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit Now you can run @@ -128,9 +127,9 @@ post rules to `test-space` you set `SPACE_URL` to be: export SPACE_URL=/s/test-space ``` -The `${SPACE_URL}` is in front of all the APIs to correctly create, modify, delete, and update +The `${SPACE_URL}` is in front of all the APIs to correctly create, modify, delete, and update them from within the defined space. If this variable is not defined the default which is the url of an -empty string will be used. +empty string will be used. Add the `.siem-signals-${your user id}` to your advanced SIEM settings to see any signals created which should update once every 5 minutes at this point. @@ -155,3 +154,13 @@ logging.events: See these two README.md's pages for more references on the alerting and actions API: https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/alerting/README.md https://github.com/elastic/kibana/tree/master/x-pack/legacy/plugins/actions +### Signals API + +To update the status of a signal or group of signals, the following scripts provide an example of how to +go about doing so. +` cd x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` +`./signals/put_signal_doc.sh` will post a sample signal doc into the signals index to play with +`./signals/set_status_with_id.sh closed` will update the status of the sample signal to closed +`./signals/set_status_with_id.sh open` will update the status of the sample signal to open +`./signals/set_status_with_query.sh closed` will update the status of the signals in the result of the query to closed. +`./signals/set_status_with_query.sh open` will update the status of the signals in the result of the query to open. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts index 07eb7c885b443..bd7ba915af9b0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts @@ -5,13 +5,9 @@ */ import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { - DEFAULT_SIGNALS_INDEX_KEY, - DEFAULT_INDEX_KEY, - DEFAULT_SIGNALS_INDEX, -} from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { AlertServices } from '../../../../../alerting/server/types'; -import { getInputOutputIndex, getOutputIndex, getInputIndex } from './get_input_output_index'; +import { getInputIndex } from './get_input_output_index'; import { defaultIndexPattern } from '../../../../default_index_pattern'; describe('get_input_output_index', () => { @@ -46,240 +42,61 @@ describe('get_input_output_index', () => { }); describe('getInputOutputIndex', () => { - test('Returns inputIndex as is if inputIndex and outputIndex are both passed in', async () => { + test('Returns inputIndex if inputIndex is passed in', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: {}, })); - const { inputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - ['test-input-index-1'], - 'test-output-index' - ); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', ['test-input-index-1']); expect(inputIndex).toEqual(['test-input-index-1']); }); - test('Returns outputIndex as is if inputIndex and outputIndex are both passed in', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - ['test-input-index-1'], - 'test-output-index' - ); - expect(outputIndex).toEqual('test-output-index'); - }); - - test('Returns inputIndex as is if inputIndex is defined but outputIndex is null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const { inputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - ['test-input-index-1'], - null - ); - expect(inputIndex).toEqual(['test-input-index-1']); - }); - - test('Returns outputIndex as is if inputIndex is null but outputIndex is defined', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - null, - 'test-output-index' - ); - expect(outputIndex).toEqual('test-output-index'); - }); - - test('Returns a saved object outputIndex if both passed in are undefined', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', - }, - })); - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - undefined, - undefined - ); - expect(outputIndex).toEqual('.signals-test-index'); - }); - - test('Returns a saved object outputIndex if passed in outputIndex is undefined', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', - }, - })); - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - ['test-input-index-1'], - undefined - ); - expect(outputIndex).toEqual('.signals-test-index'); - }); - - test('Returns a saved object outputIndex default from constants if both passed in input and configuration are null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: null, - }, - })); - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const { outputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - - test('Returns a saved object outputIndex default from constants if both passed in input and configuration are missing', async () => { - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - undefined, - undefined - ); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - - test('Returns a saved object inputIndex if passed in inputIndex and outputIndex are undefined', async () => { + test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: { [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], }, })); - const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); }); - test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { + test('Returns a saved object inputIndex if passed in inputIndex is null', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: { [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], }, })); - const { inputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - undefined, - 'output-index-1' - ); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); }); - test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration is null', async () => { + test('Returns a saved object inputIndex default from constants if inputIndex passed in is null and the key is also null', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: { [DEFAULT_INDEX_KEY]: null, }, })); - const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); - expect(inputIndex).toEqual(defaultIndexPattern); - }); - - test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes is missing', async () => { - const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); expect(inputIndex).toEqual(defaultIndexPattern); }); - }); - - describe('getOutputIndex', () => { - test('test output index is returned when passed in as is', async () => { - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex('output-index-1', mockConfiguration); - expect(outputIndex).toEqual('output-index-1'); - }); - - test('configured output index is returned when output index is null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: '.siem-test-signals', - }, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex(null, mockConfiguration); - expect(outputIndex).toEqual('.siem-test-signals'); - }); - test('output index from constants is returned when output index is null and so is the configuration', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: null, - }, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex(null, mockConfiguration); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - - test('output index from constants is returned when output index is null and configuration is missing', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex(null, mockConfiguration); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - - test('output index from constants is returned when output index is null and attributes is missing', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex(null, mockConfiguration); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - }); - - describe('getInputIndex', () => { - test('test input index is returned when passed in as is', async () => { - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(['input-index-1'], mockConfiguration); - expect(inputIndex).toEqual(['input-index-1']); - }); - - test('configured input index is returned when input index is null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_INDEX_KEY]: ['input-index-1', 'input-index-2'], - }, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(null, mockConfiguration); - expect(inputIndex).toEqual(['input-index-1', 'input-index-2']); - }); - - test('input index from constants is returned when input index is null and so is the configuration', async () => { + test('Returns a saved object inputIndex default from constants if inputIndex passed in is undefined and the key is also null', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: { [DEFAULT_INDEX_KEY]: null, }, })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(null, mockConfiguration); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); expect(inputIndex).toEqual(defaultIndexPattern); }); - test('input index from constants is returned when input index is null and configuration is missing', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(null, mockConfiguration); + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is undefined', async () => { + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); expect(inputIndex).toEqual(defaultIndexPattern); }); - test('input index from constants is returned when input index is null and attributes is missing', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(null, mockConfiguration); + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is null', async () => { + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); expect(inputIndex).toEqual(defaultIndexPattern); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts index 567ab27976d8d..624e012717820 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts @@ -4,27 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectAttributes } from 'src/core/server'; import { defaultIndexPattern } from '../../../../default_index_pattern'; import { AlertServices } from '../../../../../alerting/server/types'; -import { - DEFAULT_INDEX_KEY, - DEFAULT_SIGNALS_INDEX_KEY, - DEFAULT_SIGNALS_INDEX, -} from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -interface IndexObjectAttributes extends SavedObjectAttributes { - [DEFAULT_INDEX_KEY]: string[]; - [DEFAULT_SIGNALS_INDEX_KEY]: string; -} - -export const getInputIndex = ( - inputIndex: string[] | undefined | null, - configuration: SavedObject -): string[] => { +export const getInputIndex = async ( + services: AlertServices, + version: string, + inputIndex: string[] | null | undefined +): Promise => { if (inputIndex != null) { return inputIndex; } else { + const configuration = await services.savedObjectsClient.get('config', version); if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { return configuration.attributes[DEFAULT_INDEX_KEY]; } else { @@ -32,41 +24,3 @@ export const getInputIndex = ( } } }; - -export const getOutputIndex = ( - outputIndex: string | undefined | null, - configuration: SavedObject -): string => { - if (outputIndex != null) { - return outputIndex; - } else { - if ( - configuration.attributes != null && - configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY] != null - ) { - return configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY]; - } else { - return DEFAULT_SIGNALS_INDEX; - } - } -}; - -export const getInputOutputIndex = async ( - services: AlertServices, - version: string, - inputIndex: string[] | null | undefined, - outputIndex: string | null | undefined -): Promise<{ - inputIndex: string[]; - outputIndex: string; -}> => { - if (inputIndex != null && outputIndex != null) { - return { inputIndex, outputIndex }; - } else { - const configuration = await services.savedObjectsClient.get('config', version); - return { - inputIndex: getInputIndex(inputIndex, configuration), - outputIndex: getOutputIndex(outputIndex, configuration), - }; - } -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts index 61fe9c7c22639..577de2ce0d532 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts @@ -16,7 +16,7 @@ import { buildEventsSearchQuery } from './build_events_query'; import { searchAfterAndBulkCreate } from './utils'; import { RuleAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; -import { getInputOutputIndex } from './get_input_output_index'; +import { getInputIndex } from './get_input_output_index'; export const rulesAlertType = ({ logger, @@ -84,12 +84,7 @@ export const rulesAlertType = ({ ? DEFAULT_SEARCH_AFTER_PAGE_SIZE : params.maxSignals; - const { inputIndex, outputIndex: signalsIndex } = await getInputOutputIndex( - services, - version, - index, - outputIndex - ); + const inputIndex = await getInputIndex(services, version, index); const esFilter = await getFilter({ type, filter, @@ -122,7 +117,7 @@ export const rulesAlertType = ({ noReIndexResult.hits.total.value } signals from the indexes of "${inputIndex.join( ', ' - )}" using signal rule "id: ${alertId}", "ruleId: ${ruleId}", pushing signals to index ${signalsIndex}` + )}" using signal rule "id: ${alertId}", "ruleId: ${ruleId}", pushing signals to index ${outputIndex}` ); } @@ -132,7 +127,7 @@ export const rulesAlertType = ({ services, logger, id: alertId, - signalsIndex, + signalsIndex: outputIndex, name, createdBy, updatedBy, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 843ba4b116779..cb1d59254923b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -70,6 +70,16 @@ export type RuleAlertParamsRest = Omit< output_index: RuleAlertParams['outputIndex']; }; +export interface SignalsParams { + signalIds: string[] | undefined | null; + query: object | undefined | null; + status: 'open' | 'closed'; +} + +export type SignalsRestParams = Omit & { + signal_ids: SignalsParams['signalIds']; +}; + export type OutputRuleAlertRest = RuleAlertParamsRest & { id: string; created_by: string | undefined | null; @@ -150,6 +160,10 @@ export interface RulesRequest extends RequestFacade { payload: RuleAlertParamsRest; } +export interface SignalsRequest extends RequestFacade { + payload: SignalsRestParams; +} + export interface UpdateRulesRequest extends RequestFacade { payload: UpdateRuleAlertParamsRest; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts new file mode 100644 index 0000000000000..9c8dca0cb370f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.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 { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +// See the reference(s) below on explanations about why -000001 was chosen and +// why the is_write_index is true as well as the bootstrapping step which is needed. +// Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html +export const createBootstrapIndex = async ( + callWithRequest: CallWithRequest< + { path: string; method: 'PUT'; body: unknown }, + CallClusterOptions, + boolean + >, + index: string +): Promise => { + return callWithRequest('transport.request', { + path: `${index}-000001`, + method: 'PUT', + body: { + aliases: { + [index]: { + is_write_index: true, + }, + }, + }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts new file mode 100644 index 0000000000000..6f16eb8fbdeb1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndicesDeleteParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const deleteAllIndex = async ( + callWithRequest: CallWithRequest, + index: string +): Promise => { + return callWithRequest('indices.delete', { + index, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts new file mode 100644 index 0000000000000..153b9ae4e4136 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CallWithRequest } from './types'; + +export const deletePolicy = async ( + callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, {}, unknown>, + policy: string +): Promise => { + return callWithRequest('transport.request', { + path: `_ilm/policy/${policy}`, + method: 'DELETE', + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts new file mode 100644 index 0000000000000..b048dd27efb83 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndicesDeleteTemplateParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const deleteTemplate = async ( + callWithRequest: CallWithRequest, + name: string +): Promise => { + return callWithRequest('indices.deleteTemplate', { + name, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts new file mode 100644 index 0000000000000..24164e894788a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndicesExistsParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const getIndexExists = async ( + callWithRequest: CallWithRequest, + index: string +): Promise => { + return callWithRequest('indices.exists', { + index, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts new file mode 100644 index 0000000000000..847c32d9d61fb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CallWithRequest } from './types'; + +export const getPolicyExists = async ( + callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, {}, unknown>, + policy: string +): Promise => { + try { + await callWithRequest('transport.request', { + path: `_ilm/policy/${policy}`, + method: 'GET', + }); + // Return true that there exists a policy which is not 404 or some error + // Since there is not a policy exists API, this is how we create one by calling + // into the API to get it if it exists or rely on it to throw a 404 + return true; + } catch (err) { + if (err.statusCode === 404) { + return false; + } else { + throw err; + } + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts new file mode 100644 index 0000000000000..482fc8d855828 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndicesExistsTemplateParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const getTemplateExists = async ( + callWithRequest: CallWithRequest, + template: string +): Promise => { + return callWithRequest('indices.existsTemplate', { + name: template, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts new file mode 100644 index 0000000000000..6c9d529078a77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndicesGetSettingsParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const readIndex = async ( + callWithRequest: CallWithRequest, + index: string +): Promise => { + return callWithRequest('indices.get', { + index, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts new file mode 100644 index 0000000000000..2511984b412f3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.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 { CallWithRequest } from './types'; + +export const setPolicy = async ( + callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, {}, unknown>, + policy: string, + body: unknown +): Promise => { + return callWithRequest('transport.request', { + path: `_ilm/policy/${policy}`, + method: 'PUT', + body, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts new file mode 100644 index 0000000000000..a679a61e10c00 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndicesPutTemplateParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const setTemplate = async ( + callWithRequest: CallWithRequest, + name: string, + body: unknown +): Promise => { + return callWithRequest('indices.putTemplate', { + name, + body, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/types.ts new file mode 100644 index 0000000000000..70b33ad274ee1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/types.ts @@ -0,0 +1,7 @@ +/* + * 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 type CallWithRequest = (endpoint: string, params: T, options?: U) => Promise; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts index c02af2c841a30..8b7f6c765e710 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts @@ -6,6 +6,8 @@ import Hapi from 'hapi'; import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; + import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; import { actionsClientMock } from '../../../../../../actions/server/actions_client.mock'; @@ -46,11 +48,17 @@ export const createMockServer = (config: Record = defaultConfig) const actionsClient = actionsClientMock.create(); const alertsClient = alertsClientMock.create(); + const elasticsearch = { + getCluster: jest.fn().mockImplementation(() => ({ + callWithRequest: jest.fn(), + })), + }; server.decorate('request', 'getAlertsClient', () => alertsClient); server.decorate('request', 'getBasePath', () => '/s/default'); server.decorate('request', 'getActionsClient', () => actionsClient); + server.plugins.elasticsearch = (elasticsearch as unknown) as ElasticsearchPlugin; - return { server, alertsClient, actionsClient }; + return { server, alertsClient, actionsClient, elasticsearch }; }; export const createMockServerWithoutAlertClientDecoration = ( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index e64153e81fe4a..c18891c032060 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,8 +6,11 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { RuleAlertParamsRest, RuleAlertType } from '../../alerts/types'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { RuleAlertParamsRest, RuleAlertType, SignalsRestParams } from '../../alerts/types'; +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_SIGNALS_STATUS_URL, +} from '../../../../../common/constants'; // The Omit of filter is because of a Hapi Server Typing issue that I am unclear // where it comes from. I would hope to remove the "filter" as an omit at some point @@ -49,6 +52,20 @@ export const typicalFilterPayload = (): Partial => ({ filter: {}, }); +export const typicalSetStatusSignalByIdsPayload = (): Partial => ({ + signal_ids: ['somefakeid1', 'somefakeid2'], + status: 'closed', +}); + +export const typicalSetStatusSignalByQueryPayload = (): Partial => ({ + query: { range: { '@timestamp': { gte: 'now-2M', lte: 'now/M' } } }, + status: 'closed', +}); + +export const setStatusSignalMissingIdsAndQueryPayload = (): Partial => ({ + status: 'closed', +}); + export const getUpdateRequest = (): ServerInjectOptions => ({ method: 'PUT', url: DETECTION_ENGINE_RULES_URL, @@ -113,6 +130,22 @@ export const getCreateRequest = (): ServerInjectOptions => ({ }, }); +export const getSetSignalStatusByIdsRequest = (): ServerInjectOptions => ({ + method: 'POST', + url: DETECTION_ENGINE_SIGNALS_STATUS_URL, + payload: { + ...typicalSetStatusSignalByIdsPayload(), + }, +}); + +export const getSetSignalStatusByQueryRequest = (): ServerInjectOptions => ({ + method: 'POST', + url: DETECTION_ENGINE_SIGNALS_STATUS_URL, + payload: { + ...typicalSetStatusSignalByQueryPayload(), + }, +}); + export const createActionResult = (): ActionResult => ({ id: 'result-1', actionTypeId: 'action-id-1', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts index 4c222c196300c..4aa57f005445b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts @@ -22,11 +22,15 @@ import { import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; describe('create_rules', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); + ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); + elasticsearch.getCluster = jest.fn().mockImplementation(() => ({ + callWithRequest: jest.fn().mockImplementation(() => true), + })); + createRulesRoute(server); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts index 2b69e57f2c2ee..31068dac5d23a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -14,97 +14,112 @@ import { RulesRequest } from '../alerts/types'; import { createRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { readRules } from '../alerts/read_rules'; -import { transformOrError, transformError } from './utils'; +import { transformOrError, transformError, getIndex, callWithRequestFactory } from './utils'; +import { getIndexExists } from '../index/get_index_exists'; -export const createCreateRulesRoute: Hapi.ServerRoute = { - method: 'POST', - path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:siem'], - validate: { - options: { - abortEarly: false, +export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: createRulesSchema, }, - payload: createRulesSchema, }, - }, - async handler(request: RulesRequest, headers) { - const { - description, - enabled, - false_positives: falsePositives, - filter, - from, - immutable, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threats, - to, - type, - references, - } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { - return headers.response().code(404); - } - - try { - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId }); - if (rule != null) { - return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); - } - } - - const createdRule = await createRules({ - alertsClient, - actionsClient, + async handler(request: RulesRequest, headers) { + const { description, enabled, - falsePositives, + false_positives: falsePositives, filter, from, immutable, query, language, - outputIndex, - savedId, + output_index: outputIndex, + saved_id: savedId, meta, filters, - ruleId: ruleId != null ? ruleId : uuid.v4(), + rule_id: ruleId, index, interval, - maxSignals, - riskScore, + max_signals: maxSignals, + risk_score: riskScore, name, severity, tags, + threats, to, type, - threats, references, - }); - return transformOrError(createdRule); - } catch (err) { - return transformError(err); - } - }, + } = request.payload; + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + try { + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const callWithRequest = callWithRequestFactory(request); + const indexExists = await getIndexExists(callWithRequest, finalIndex); + if (!indexExists) { + return new Boom( + `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + { + statusCode: 400, + } + ); + } + if (ruleId != null) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); + } + } + const createdRule = await createRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + filter, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + meta, + filters, + ruleId: ruleId != null ? ruleId : uuid.v4(), + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threats, + references, + }); + return transformOrError(createdRule); + } catch (err) { + return transformError(err); + } + }, + }; }; export const createRulesRoute = (server: ServerFacade) => { - server.route(createCreateRulesRoute); + server.route(createCreateRulesRoute(server)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts new file mode 100644 index 0000000000000..14a061fd4ccb7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import Boom from 'boom'; + +import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; +import signalsPolicy from './signals_policy.json'; +import { ServerFacade, RequestFacade } from '../../../../types'; +import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { getIndexExists } from '../../index/get_index_exists'; +import { getPolicyExists } from '../../index/get_policy_exists'; +import { setPolicy } from '../../index/set_policy'; +import { setTemplate } from '../../index/set_template'; +import { getSignalsTemplate } from './get_signals_template'; +import { getTemplateExists } from '../../index/get_template_exists'; +import { createBootstrapIndex } from '../../index/create_bootstrap_index'; + +export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: DETECTION_ENGINE_INDEX_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RequestFacade) { + try { + const index = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request); + const indexExists = await getIndexExists(callWithRequest, index); + if (indexExists) { + return new Boom(`index ${index} already exists`, { statusCode: 409 }); + } else { + const policyExists = await getPolicyExists(callWithRequest, index); + if (!policyExists) { + await setPolicy(callWithRequest, index, signalsPolicy); + } + const templateExists = await getTemplateExists(callWithRequest, index); + if (!templateExists) { + const template = getSignalsTemplate(index); + await setTemplate(callWithRequest, index, template); + } + createBootstrapIndex(callWithRequest, index); + return { acknowledged: true }; + } + } catch (err) { + return transformError(err); + } + }, + }; +}; + +export const createIndexRoute = (server: ServerFacade) => { + server.route(createCreateIndexRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts new file mode 100644 index 0000000000000..89f21bbada939 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import Boom from 'boom'; + +import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; +import { ServerFacade, RequestFacade } from '../../../../types'; +import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { getIndexExists } from '../../index/get_index_exists'; +import { getPolicyExists } from '../../index/get_policy_exists'; +import { deletePolicy } from '../../index/delete_policy'; +import { getTemplateExists } from '../../index/get_template_exists'; +import { deleteAllIndex } from '../../index/delete_all_index'; +import { deleteTemplate } from '../../index/delete_template'; + +/** + * Deletes all of the indexes, template, ilm policies, and aliases. You can check + * this by looking at each of these settings from ES after a deletion: + * GET /_template/.siem-signals-default + * GET /.siem-signals-default-000001/ + * GET /_ilm/policy/.signals-default + * GET /_alias/.siem-signals-default + * + * And ensuring they're all gone + */ +export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'DELETE', + path: DETECTION_ENGINE_INDEX_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RequestFacade) { + try { + const index = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request); + const indexExists = await getIndexExists(callWithRequest, index); + if (!indexExists) { + return new Boom(`index ${index} does not exist`, { statusCode: 404 }); + } else { + await deleteAllIndex(callWithRequest, `${index}-*`); + const policyExists = await getPolicyExists(callWithRequest, index); + if (policyExists) { + await deletePolicy(callWithRequest, index); + } + const templateExists = await getTemplateExists(callWithRequest, index); + if (templateExists) { + await deleteTemplate(callWithRequest, index); + } + return { acknowledged: true }; + } + } catch (err) { + return transformError(err); + } + }, + }; +}; + +export const deleteIndexRoute = (server: ServerFacade) => { + server.route(createDeleteIndexRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json index 94f251645d3fd..06edf94484af3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json @@ -5,186 +5,6 @@ "@timestamp": { "type": "date" }, - "signal": { - "properties": { - "parent": { - "properties": { - "index": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "depth": { - "type": "long" - } - } - }, - "rule": { - "properties": { - "id": { - "type": "keyword" - }, - "rule_id": { - "type": "keyword" - }, - "false_positives": { - "type": "keyword" - }, - "saved_id": { - "type": "keyword" - }, - "max_signals": { - "type": "keyword" - }, - "risk_score": { - "type": "keyword" - }, - "output_index": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "filter": { - "type": "object" - }, - "from": { - "type": "keyword" - }, - "immutable": { - "type": "keyword" - }, - "index": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "query": { - "type": "keyword" - }, - "references": { - "type": "keyword" - }, - "severity": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "threats": { - "type": "object" - }, - "type": { - "type": "keyword" - }, - "size": { - "type": "keyword" - }, - "to": { - "type": "keyword" - }, - "enabled": { - "type": "keyword" - }, - "filters": { - "type": "object" - }, - "created_by": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "original_time": { - "type": "date" - }, - "original_event": { - "properties": { - "action": { - "type": "keyword" - }, - "category": { - "type": "keyword" - }, - "code": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - }, - "module": { - "type": "keyword" - }, - "original": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "outcome": { - "type": "keyword" - }, - "provider": { - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "status": { - "type": "keyword" - } - } - }, "agent": { "properties": { "ephemeral_id": { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts new file mode 100644 index 0000000000000..80594ca74a353 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.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 { getSignalsTemplate } from './get_signals_template'; + +describe('get_signals_template', () => { + test('it should set the lifecycle name and the rollover alias to be the name of the index passed in', () => { + const template = getSignalsTemplate('test-index'); + expect(template.settings).toEqual({ + index: { lifecycle: { name: 'test-index', rollover_alias: 'test-index' } }, + }); + }); + + test('it should set have the index patterns with an ending glob in it', () => { + const template = getSignalsTemplate('test-index'); + expect(template.index_patterns).toEqual(['test-index-*']); + }); + + test('it should have a mappings section which is an object type', () => { + const template = getSignalsTemplate('test-index'); + expect(typeof template.mappings).toEqual('object'); + }); + + test('it should have a signals section which is an object type', () => { + const template = getSignalsTemplate('test-index'); + expect(typeof template.mappings.properties.signal).toEqual('object'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts new file mode 100644 index 0000000000000..c6580f0bdda42 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.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 signalsMapping from './signals_mapping.json'; +import ecsMapping from './ecs_mapping.json'; + +export const getSignalsTemplate = (index: string) => { + ecsMapping.mappings.properties.signal = signalsMapping.mappings.properties.signal; + const template = { + settings: { + index: { + lifecycle: { + name: index, + rollover_alias: index, + }, + }, + }, + index_patterns: [`${index}-*`], + mappings: ecsMapping.mappings, + }; + return template; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts new file mode 100644 index 0000000000000..87b6ffb985c37 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import Boom from 'boom'; + +import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; +import { ServerFacade, RequestFacade } from '../../../../types'; +import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { getIndexExists } from '../../index/get_index_exists'; + +export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'GET', + path: DETECTION_ENGINE_INDEX_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RequestFacade, headers) { + try { + const index = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request); + const indexExists = await getIndexExists(callWithRequest, index); + if (indexExists) { + // head request is used for if you want to get if the index exists + // or not and it will return a content-length: 0 along with either a 200 or 404 + // depending on if the index exists or not. + if (request.method.toLowerCase() === 'head') { + return headers.response().code(200); + } else { + return headers.response({ name: index }).code(200); + } + } else { + if (request.method.toLowerCase() === 'head') { + return headers.response().code(404); + } else { + return new Boom('An index for this space does not exist', { statusCode: 404 }); + } + } + } catch (err) { + return transformError(err); + } + }, + }; +}; + +export const readIndexRoute = (server: ServerFacade) => { + server.route(createReadIndexRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json new file mode 100644 index 0000000000000..501522105bdbc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json @@ -0,0 +1,186 @@ +{ + "mappings": { + "properties": { + "signal": { + "properties": { + "parent": { + "properties": { + "index": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "depth": { + "type": "long" + } + } + }, + "rule": { + "properties": { + "id": { + "type": "keyword" + }, + "rule_id": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "risk_score": { + "type": "keyword" + }, + "output_index": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "filter": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threats": { + "type": "object" + }, + "type": { + "type": "keyword" + }, + "size": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "original_time": { + "type": "date" + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + } + } + } + } + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json new file mode 100644 index 0000000000000..640d8e14190cd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json @@ -0,0 +1,15 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "10gb", + "max_age": "7d" + } + } + } + } + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 690d9b11b1455..dd45490c778bf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -4,12 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createRulesSchema, updateRulesSchema, findRulesSchema, queryRulesSchema } from './schemas'; +import { + createRulesSchema, + updateRulesSchema, + findRulesSchema, + queryRulesSchema, + setSignalsStatusSchema, +} from './schemas'; import { RuleAlertParamsRest, FindParamsRest, UpdateRuleAlertParamsRest, ThreatParams, + SignalsRestParams, } from '../alerts/types'; describe('schemas', () => { @@ -2006,186 +2013,187 @@ describe('schemas', () => { }).value.threats ).toBe(undefined); }); - }); - test('threats is not defaulted to undefined on update with empty array', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [], - }).value.threats - ).toMatchObject([]); - }); - test('threats is valid when updated with all sub-objects', () => { - const expected: ThreatParams[] = [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', + + test('threats is not defaulted to undefined on update with empty array', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [], + }).value.threats + ).toMatchObject([]); + }); + test('threats is valid when updated with all sub-objects', () => { + const expected: ThreatParams[] = [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', }, - ], - }, - ]; - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', }, - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', + ], + }, + ]; + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', }, - ], - }, - ], - }).value.threats - ).toMatchObject(expected); - }); - test('threats is invalid when updated with missing property framework', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], }, - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', + ], + }).value.threats + ).toMatchObject(expected); + }); + test('threats is invalid when updated with missing property framework', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', }, - ], - }, - ], - }).error - ).toBeTruthy(); - }); - test('threats is invalid when updated with missing tactic sub-object', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - framework: 'fake', - techniques: [ - { + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeTruthy(); + }); + test('threats is invalid when updated with missing tactic sub-object', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeTruthy(); + }); + test('threats is invalid when updated with missing techniques', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef', }, - ], - }, - ], - }).error - ).toBeTruthy(); - }); - test('threats is invalid when updated with missing techniques', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - framework: 'fake', - tactic: { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', }, - }, - ], - }).error - ).toBeTruthy(); + ], + }).error + ).toBeTruthy(); + }); }); describe('find rules schema', () => { @@ -2340,4 +2348,62 @@ describe('schemas', () => { ).toBeFalsy(); }); }); + + describe('set signal status schema', () => { + test('signal_ids and status is valid', () => { + expect( + setSignalsStatusSchema.validate>({ + signal_ids: ['somefakeid'], + status: 'open', + }).error + ).toBeFalsy(); + }); + + test('query and status is valid', () => { + expect( + setSignalsStatusSchema.validate>({ + query: {}, + status: 'open', + }).error + ).toBeFalsy(); + }); + + test('signal_ids and missing status is invalid', () => { + expect( + setSignalsStatusSchema.validate>({ + signal_ids: ['somefakeid'], + }).error + ).toBeTruthy(); + }); + + test('query and missing status is invalid', () => { + expect( + setSignalsStatusSchema.validate>({ + query: {}, + }).error + ).toBeTruthy(); + }); + + test('status is present but query or signal_ids is missing is invalid', () => { + expect( + setSignalsStatusSchema.validate>({ + status: 'closed', + }).error + ).toBeTruthy(); + }); + + test('signal_ids is present but status has wrong value', () => { + expect( + setSignalsStatusSchema.validate< + Partial< + Omit & { + status: string; + } + > + >({ + status: 'fakeVal', + }).error + ).toBeTruthy(); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index 59d52c113cbf2..d1a7a01c72415 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -32,6 +32,7 @@ const risk_score = Joi.number() .greater(-1) .less(101); const severity = Joi.string(); +const status = Joi.string().valid('open', 'closed'); const to = Joi.string(); const type = Joi.string().valid('filter', 'query', 'saved_query'); const queryFilter = Joi.string(); @@ -44,6 +45,8 @@ const per_page = Joi.number() const page = Joi.number() .min(1) .default(1); +const signal_ids = Joi.array().items(Joi.string()); +const signal_status_query = Joi.object(); const sort_field = Joi.string(); const sort_order = Joi.string().valid('asc', 'desc'); const tags = Joi.array().items(Joi.string()); @@ -213,3 +216,9 @@ export const findRulesSchema = Joi.object({ }), sort_order, }); + +export const setSignalsStatusSchema = Joi.object({ + signal_ids, + query: signal_status_query, + status: status.required(), +}).xor('signal_ids', 'query'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts new file mode 100644 index 0000000000000..3cb64586d6cb5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -0,0 +1,114 @@ +/* + * 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 { createMockServer } from '../__mocks__/_mock_server'; +import { setSignalsStatusRoute } from './open_close_signals_route'; +import * as myUtils from '../utils'; +import { ServerInjectOptions } from 'hapi'; +import { + getSetSignalStatusByIdsRequest, + getSetSignalStatusByQueryRequest, + typicalSetStatusSignalByIdsPayload, + typicalSetStatusSignalByQueryPayload, + setStatusSignalMissingIdsAndQueryPayload, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; + +describe('set signal status', () => { + let { server, elasticsearch } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); + ({ server, elasticsearch } = createMockServer()); + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn(() => true), + })); + setSignalsStatusRoute(server); + }); + + describe('status on signal', () => { + test('returns 200 when setting a status on a signal by ids', async () => { + const { statusCode } = await server.inject(getSetSignalStatusByIdsRequest()); + expect(statusCode).toBe(200); + expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); + }); + + test('returns 200 when setting a status on a signal by query', async () => { + const { statusCode } = await server.inject(getSetSignalStatusByQueryRequest()); + expect(statusCode).toBe(200); + }); + }); + + describe('validation', () => { + test('returns 200 if signal_ids and status are present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_SIGNALS_STATUS_URL, + payload: typicalSetStatusSignalByIdsPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if query and status are present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_SIGNALS_STATUS_URL, + payload: typicalSetStatusSignalByQueryPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if signal_ids and query are not present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_SIGNALS_STATUS_URL, + payload: setStatusSignalMissingIdsAndQueryPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 400 if signal_ids are present but status is not', async () => { + const { status, ...justIds } = typicalSetStatusSignalByIdsPayload(); + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_SIGNALS_STATUS_URL, + payload: justIds, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 400 if query is present but status is not', async () => { + const { status, ...justTheQuery } = typicalSetStatusSignalByQueryPayload(); + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_SIGNALS_STATUS_URL, + payload: justTheQuery, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 400 if query and signal_ids are present but status is not', async () => { + const allTogether = { + ...typicalSetStatusSignalByIdsPayload(), + ...typicalSetStatusSignalByQueryPayload(), + }; + const { status, ...queryAndSignalIdsNoStatus } = allTogether; + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_SIGNALS_STATUS_URL, + payload: queryAndSignalIdsNoStatus, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts new file mode 100644 index 0000000000000..b4152256fbb53 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; +import { SignalsRequest } from '../../alerts/types'; +import { setSignalsStatusSchema } from '../schemas'; +import { ServerFacade } from '../../../../types'; +import { transformError, getIndex } from '../utils'; + +export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: DETECTION_ENGINE_SIGNALS_STATUS_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: setSignalsStatusSchema, + }, + }, + async handler(request: SignalsRequest, headers) { + const { signal_ids: signalIds, query, status } = request.payload; + const index = getIndex(request, server); + const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data'); + let queryObject; + if (signalIds) { + queryObject = { ids: { values: signalIds } }; + } + if (query) { + queryObject = query; + } + try { + return callWithRequest(request, 'updateByQuery', { + index, + body: { + script: { + source: `ctx._source.signal.status = '${status}'`, + lang: 'painless', + }, + query: queryObject, + }, + }); + } catch (exc) { + // error while getting or updating signal with id: id in signal index .siem-signals + return transformError(exc); + } + }, + }; +}; + +export const setSignalsStatusRoute = (server: ServerFacade) => { + server.route(setSignalsStatusRouteDef(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 40a33e9d97a18..4c5a5a6af93de 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,9 @@ import Boom from 'boom'; import { pickBy } from 'lodash/fp'; +import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; import { RuleAlertType, isAlertType, OutputRuleAlertRest, isAlertTypes } from '../alerts/types'; +import { ServerFacade, RequestFacade } from '../../../types'; export const getIdError = ({ id, @@ -88,3 +90,16 @@ export const transformError = (err: Error & { statusCode?: number }) => { } } }; + +export const getIndex = (request: RequestFacade, server: ServerFacade): string => { + const spaceId = server.plugins.spaces.getSpaceId(request); + const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); + return `${signalsIndex}-${spaceId}`; +}; + +export const callWithRequestFactory = (request: RequestFacade) => { + const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data'); + return (endpoint: string, params: T, options?: U) => { + return callWithRequest(request, endpoint, params, options); + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh index c2406dc7f6231..fb3bbbe0fad18 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh @@ -30,11 +30,6 @@ if [ -z "${KIBANA_URL}" ]; then exit 1 fi -if [ -z "${SIGNALS_INDEX}" ]; then - echo "Set SIGNALS_INDEX in your environment" - exit 1 -fi - if [ -z "${TASK_MANAGER_INDEX}" ]; then echo "Set TASK_MANAGER_INDEX in your environment" exit 1 diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh index 8d5deec1ba3a1..aa8f90f27d6d8 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh @@ -10,9 +10,7 @@ set -e ./check_env_variables.sh # Example: ./delete_signal_index.sh -# https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html curl -s -k \ - -H "Content-Type: application/json" \ + -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${ELASTICSEARCH_URL}/${SIGNALS_INDEX} \ - | jq . + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_mapping.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh similarity index 53% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_mapping.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh index 8b384fcc76f72..882451631c9eb 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_mapping.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh @@ -9,9 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_mapping.sh -# https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html +# Example: ./get_signal_index.sh curl -s -k \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${ELASTICSEARCH_URL}/${SIGNALS_INDEX}/_mapping \ - | jq . + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh index ee8fa18e1234d..3cd0bff7ee8ab 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh @@ -13,4 +13,4 @@ set -e ./delete_all_alerts.sh ./delete_all_alert_tasks.sh ./delete_signal_index.sh -./put_signal_index.sh +./post_signal_index.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh new file mode 100755 index 0000000000000..e408f21888c5f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./post_signal_index.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/detection_engine/index | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json index c136c9b0fe808..75a04b5426550 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json @@ -9,7 +9,6 @@ "type": "query", "from": "now-24h", "to": "now", - "output_index": ".siem-signals", "language": "lucene", "query": "", "filters": [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json index 5b69fced90daf..2417a000f9dce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json @@ -9,7 +9,6 @@ "type": "query", "from": "now-24h", "to": "now", - "output_index": ".siem-signals", "language": "lucene", "filters": [ { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh new file mode 100755 index 0000000000000..b4a494a102b54 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./signal_index_exists.sh +curl -s -k --head \ + -H 'Content-Type: application/json' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/put_signal_index.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/put_signal_doc.sh similarity index 66% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/put_signal_index.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/put_signal_doc.sh index 1b3b148a99161..dd107ae94ebbb 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/put_signal_index.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/put_signal_doc.sh @@ -9,10 +9,12 @@ set -e ./check_env_variables.sh -# Example: ./put_signal_index.sh +SIGNALS_INDEX=$(./get_signal_index.sh | jq ".name" -j) + +# Example: ./put_signal_doc.sh curl -s -k \ -H "Content-Type: application/json" \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -d @../signals_mapping.json \ - -X PUT ${ELASTICSEARCH_URL}/${SIGNALS_INDEX} \ + -d @./signals/sample_signal.json \ + -X PUT ${ELASTICSEARCH_URL}/$SIGNALS_INDEX/_doc/45562a28e0dFakeSignalId \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/sample_signal.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/sample_signal.json new file mode 100644 index 0000000000000..8cba054d4d205 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/sample_signal.json @@ -0,0 +1,198 @@ +{ + "@timestamp": "2019-11-27T00:11:45.677Z", + "host": { + "architecture": "x86_64", + "os": { + "kernel": "5.0.0-1015-gcp", + "codename": "disco", + "platform": "ubuntu", + "version": "19.04 (Disco Dingo)", + "family": "debian", + "name": "Ubuntu" + }, + "id": "e6f890af316a25920c951acefc46bfef", + "name": "suricata-iowa", + "containerized": false, + "ip": [ + "10.128.0.21", + "fe80::4001:aff:fe80:15" + ], + "mac": [ + "42:01:0a:80:00:15" + ], + "hostname": "suricata-iowa" + }, + "client": { + "ip": "10.128.0.21", + "port": 33814, + "packets": 2, + "bytes": 80 + }, + "user": { + "id": "0", + "name": "root" + }, + "group": { + "id": "0", + "name": "root" + }, + "server": { + "ip": "169.254.169.254", + "port": 80, + "packets": 3, + "bytes": 643 + }, + "system": { + "audit": { + "socket": { + "internal_version": "1.0.3", + "uid": 0, + "gid": 0, + "euid": 0, + "egid": 0, + "kernel_sock_address": "0xffff9e69fed14c80" + } + } + }, + "network": { + "packets": 5, + "bytes": 723, + "community_id": "1:drz3M0ZoUnQI9BgaXVRhAwCGOsw=", + "direction": "outbound", + "type": "ipv4", + "transport": "tcp" + }, + "flow": { + "final": true, + "complete": false + }, + "source": { + "ip": "10.128.0.21", + "port": 33814, + "packets": 2, + "bytes": 80 + }, + "process": { + "args": [ + "\/usr\/bin\/python3", + "\/usr\/bin\/google_network_daemon" + ], + "executable": "\/usr\/bin\/python3.7", + "created": "2019-11-04T15:01:04.190Z", + "pid": 808, + "name": "google_network_" + }, + "destination": { + "bytes": 643, + "ip": "169.254.169.254", + "port": 80, + "packets": 3 + }, + "agent": { + "version": "8.0.0", + "type": "auditbeat", + "ephemeral_id": "d59f3431-3d93-401f-b7f7-557184bbe9c6", + "hostname": "suricata-iowa", + "id": "d67ace4c-c168-4527-a349-7740d4e9dfaa" + }, + "event": { + "module": "system", + "dataset": "socket", + "kind": "signal", + "action": "network_flow", + "category": "network_traffic", + "start": "2019-11-27T00:11:34.865Z", + "end": "2019-11-27T00:11:34.866Z", + "duration": 1042842 + }, + "service": { + "type": "system" + }, + "ecs": { + "version": "1.1.0" + }, + "cloud": { + "provider": "gcp", + "instance": { + "id": "8744816015462516945", + "name": "suricata-iowa" + }, + "machine": { + "type": "n1-standard-2" + }, + "availability_zone": "us-central1-a", + "project": { + "id": "elastic-siem" + } + }, + "signal": { + "parent": { + "id": "0_Ezqm4BHe9nqdOiBktm", + "type": "event", + "index": "auditbeat-8.0.0-2019.09.06-000022", + "depth": 1 + }, + "original_time": "2019-11-27T00:11:40.012Z", + "status": "open", + "rule": { + "id": "8d9bb7f1-9d8a-4f13-ae18-ccaf4ff61893", + "rule_id": "rule-1", + "false_positives": [ + + ], + "max_signals": 100, + "risk_score": 1, + "description": "Detecting root and admin users", + "from": "now-6s", + "immutable": false, + "index": [ + "auditbeat-*", + "filebeat-*", + "packetbeat-*", + "winlogbeat-*" + ], + "interval": "5s", + "language": "kuery", + "name": "Detect Root\/Admin Users", + "query": "user.name: root or user.name: admin", + "references": [ + "http:\/\/www.example.com", + "https:\/\/ww.example.com" + ], + "severity": "high", + "tags": [ + + ], + "type": "query", + "to": "now", + "enabled": true, + "created_by": "elastic", + "updated_by": "elastic", + "threats": [ + { + "framework": "fake", + "technique": { + "reference": "techniqueRef", + "name": "techniqueName", + "id": "techniqueId" + }, + "tactic": { + "reference": "fakeRef", + "name": "fakeName", + "id": "fakeId" + } + } + ] + }, + "original_event": { + "module": "system", + "dataset": "socket", + "kind": "event", + "action": "network_flow", + "category": "network_traffic", + "start": "2019-11-27T00:11:34.865Z", + "end": "2019-11-27T00:11:34.866Z", + "duration": 1042842 + } + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_id.sh new file mode 100755 index 0000000000000..40c198638b15a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_id.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./set_status_with_id.sh closed +# Example: ./set_status_with_id.sh open + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/signals/status \ + -d '{"signal_ids": ["45562a28e0dFakeSignalId"], "status": "'$1'"}' \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_query.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_query.sh new file mode 100755 index 0000000000000..50187c7b2fb77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_query.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./sigsnals/set_status_with_query.sh closed +# Example: ./sigsnals/set_status_with_query.sh open + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/signals/status \ + -d '{ + "status": "'$1'", + "query": {"range":{"@timestamp":{"gte":"now-2M","lte":"now/M"}}}}' \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index d6fea820b2698..ad19872b7a75d 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -13,6 +13,7 @@ export interface ServerFacade { injectUiAppVars: Legacy.Server['injectUiAppVars']; plugins: { alerting?: Legacy.Server['plugins']['alerting']; + spaces: Legacy.Server['plugins']['spaces']; xpack_main: Legacy.Server['plugins']['xpack_main']; }; route: Legacy.Server['route']; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.test.ts index f6717a4ddf26c..0789780c62ace 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -39,7 +39,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { patch: () => {}, }, { - cloud: { config: { isCloudEnabled: false } }, + cloud: { isCloudEnabled: false }, elasticsearch: { getCluster: () => ({ callWithInternalUser: mockCallWithInternalUser }) }, } ); diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts index 815655df28399..13f44d2a1aeeb 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -28,7 +28,7 @@ let isCloudEnabled: boolean = false; let callWithInternalUser: any; export function registerRepositoriesRoutes(router: Router, plugins: Plugins) { - isCloudEnabled = plugins.cloud.config.isCloudEnabled; + isCloudEnabled = plugins.cloud && plugins.cloud.isCloudEnabled; callWithInternalUser = plugins.elasticsearch.getCluster('data').callWithInternalUser; router.get('repository_types', getTypesHandler); router.get('repositories', getAllHandler); diff --git a/x-pack/legacy/plugins/snapshot_restore/shim.ts b/x-pack/legacy/plugins/snapshot_restore/shim.ts index 335f7db32f0bf..ef8d65fca77d4 100644 --- a/x-pack/legacy/plugins/snapshot_restore/shim.ts +++ b/x-pack/legacy/plugins/snapshot_restore/shim.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; import { createRouter, Router } from '../../server/lib/create_router'; import { registerLicenseChecker } from '../../server/lib/register_license_checker'; import { elasticsearchJsPlugin } from './server/client/elasticsearch_slm'; - +import { CloudSetup } from '../../../plugins/cloud/server'; export interface Core { http: { createRouter(basePath: string): Router; @@ -24,11 +23,7 @@ export interface Plugins { license: { registerLicenseChecker: typeof registerLicenseChecker; }; - cloud: { - config: { - isCloudEnabled: boolean; - }; - }; + cloud: CloudSetup; settings: { config: { isSlmEnabled: boolean; @@ -42,6 +37,7 @@ export function createShim( server: Legacy.Server, pluginId: string ): { core: Core; plugins: Plugins } { + const { cloud } = server.newPlatform.setup.plugins; return { core: { http: { @@ -56,11 +52,7 @@ export function createShim( license: { registerLicenseChecker, }, - cloud: { - config: { - isCloudEnabled: get(server.plugins, 'cloud.config.isCloudEnabled', false), - }, - }, + cloud: cloud as CloudSetup, settings: { config: { isSlmEnabled: server.config() diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/_confirm_delete_modal.scss b/x-pack/legacy/plugins/spaces/public/views/management/components/_confirm_delete_modal.scss index 04bc51067d9df..887495a231485 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/_confirm_delete_modal.scss +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/_confirm_delete_modal.scss @@ -1,5 +1,3 @@ -@import '@elastic/eui/src/components/form/variables'; - .spcConfirmDeleteModal { - max-width: $euiFormMaxWidth + ($euiSizeL*2); + max-width: $euiFormMaxWidth + ($euiSizeL * 2); } diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/_space_selector.scss b/x-pack/legacy/plugins/spaces/public/views/space_selector/_space_selector.scss index 3fd2f91b25f4a..cfa2fd650e83b 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/_space_selector.scss +++ b/x-pack/legacy/plugins/spaces/public/views/space_selector/_space_selector.scss @@ -1,5 +1,3 @@ -@import '@elastic/eui/src/components/form/variables'; - .spcSpaceSelector { @include kibanaFullScreenGraphics; } @@ -27,7 +25,6 @@ @include euiBottomShadowMedium; } - .spcSpaceSelector__searchHolder { width: $euiFormMaxWidth; // make sure it's as wide as our default form element width max-width: 100%; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts index b40645799fb4b..62c87413c9bce 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts @@ -5,11 +5,8 @@ */ import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/public'; -import { - IndexPattern as IndexPatternType, - IndexPatterns as IndexPatternsType, -} from 'ui/index_patterns'; -import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { IndexPattern as IndexPatternType } from 'ui/index_patterns'; +import { esQuery, IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; type IndexPatternId = string; type SavedSearchId = string; @@ -23,7 +20,7 @@ export let refreshIndexPatterns: () => Promise; export function loadIndexPatterns( savedObjectsClient: SavedObjectsClientContract, - indexPatterns: IndexPatternsType + indexPatterns: IndexPatternsContract ) { fullIndexPatterns = indexPatterns; return savedObjectsClient @@ -56,7 +53,7 @@ export function loadIndexPatterns( type CombinedQuery = Record<'bool', any> | unknown; export function loadCurrentIndexPattern( - indexPatterns: IndexPatternsType, + indexPatterns: IndexPatternsContract, indexPatternId: IndexPatternId ) { fullIndexPatterns = indexPatterns; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx index e3515991e7bb1..3a3bd33ff6d70 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx @@ -6,12 +6,11 @@ import React, { createContext, useContext, FC } from 'react'; -import { - IndexPattern as IndexPatternType, - IndexPatterns as IndexPatternsType, -} from 'ui/index_patterns'; - import { SavedSearch } from '../../../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; +import { + IndexPattern, + IndexPatternsContract, +} from '../../../../../../../../src/plugins/data/public'; import { KibanaConfig } from '../../../../../../../../src/legacy/server/kbn_server'; // set() method is missing in original d.ts @@ -25,9 +24,9 @@ interface UninitializedKibanaContextValue { export interface InitializedKibanaContextValue { combinedQuery: any; - currentIndexPattern: IndexPatternType; + currentIndexPattern: IndexPattern; currentSavedSearch: SavedSearch; - indexPatterns: IndexPatternsType; + indexPatterns: IndexPatternsContract; initialized: boolean; kbnBaseUrl: string; kibanaConfig: KibanaConfigTypeFix; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx index 7d615bfa521f0..0a9de49168ad4 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx @@ -8,8 +8,6 @@ import React, { useEffect, useState, FC } from 'react'; import { npStart } from 'ui/new_platform'; -import { start as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; - import { useAppDependencies } from '../../app_dependencies'; import { @@ -21,7 +19,7 @@ import { import { KibanaContext, KibanaContextValue } from './kibana_context'; -const { indexPatterns } = data.indexPatterns; +const indexPatterns = npStart.plugins.data.indexPatterns; const savedObjectsClient = npStart.core.savedObjects.client; interface Props { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 5a5e8308b8d57..6ee03ccca16e1 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -22,6 +22,8 @@ import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/p import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; import { transformTableFactory } from './transform_table'; +const TransformTable = transformTableFactory(); + interface Props { transformConfig: TransformPivotConfig; } @@ -197,8 +199,6 @@ export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { const transformTableLoading = previewData.length === 0 && isLoading === true; const dataTestSubj = `transformPreviewTabContent${!transformTableLoading ? ' loaded' : ''}`; - const TransformTable = transformTableFactory(); - return (
', }); - + const { cloud } = npSetup.plugins as any; const legacyPluginsShim: LegacyPlugins = { + cloud: cloud as CloudSetup, __LEGACY: { XSRF: chrome.getXsrfToken(), - isCloudEnabled: chrome.getInjected('isCloudEnabled', false), }, management: { sections: { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts index 16a0c9632fb25..ed85b988c25d6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts @@ -9,11 +9,10 @@ import { RootComponent } from './application/app'; import { LegacyPlugins } from '../legacy'; export class UpgradeAssistantUIPlugin implements Plugin { - async setup( - { http }: CoreSetup, - { management, __LEGACY: { XSRF, isCloudEnabled } }: LegacyPlugins - ) { + async setup({ http }: CoreSetup, { cloud, management, __LEGACY: { XSRF } }: LegacyPlugins) { const appRegistrar = management.sections.get('kibana'); + const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); + return appRegistrar.registerApp({ mount(__, { __LEGACY: { renderToElement } }) { return renderToElement(() => RootComponent({ http, XSRF, isCloudEnabled })); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts index 3d4247ffe70bb..fae369fa59394 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts @@ -11,17 +11,20 @@ import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices'; - +import { CloudSetup } from '../../../../../plugins/cloud/server'; import { registerTelemetryRoutes } from './routes/telemetry'; -export class UpgradeAssistantServerPlugin implements Plugin { - setup( - { http }: CoreSetup, - { __LEGACY, usageCollection }: { usageCollection: UsageCollectionSetup; __LEGACY: ServerShim } - ) { +interface PluginsSetup { + __LEGACY: ServerShim; + usageCollection: UsageCollectionSetup; + cloud?: CloudSetup; +} + +export class UpgradeAssistantServerPlugin implements Plugin { + setup({ http }: CoreSetup, { __LEGACY, usageCollection, cloud }: PluginsSetup) { const router = http.createRouter(); const shimWithRouter: ServerShimWithRouter = { ...__LEGACY, router }; - registerClusterCheckupRoutes(shimWithRouter); + registerClusterCheckupRoutes(shimWithRouter, { cloud }); registerDeprecationLoggingRoutes(shimWithRouter); // The ReindexWorker uses a map of request headers that contain the authentication credentials diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts index 6afb9d2a5e935..3fe2e1797182b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts @@ -29,26 +29,27 @@ describe('cluster checkup API', () => { let mockRouter: MockRouter; let serverShim: any; let ctxMock: any; + let mockPluginsSetup: any; beforeEach(() => { mockRouter = createMockRouter(); + mockPluginsSetup = { + cloud: { + isCloudEnabled: true, + }, + }; ctxMock = { core: {}, }; serverShim = { router: mockRouter, plugins: { - cloud: { - config: { - isCloudEnabled: true, - }, - }, elasticsearch: { getCluster: () => ({ callWithRequest: jest.fn() } as any), } as any, }, }; - registerClusterCheckupRoutes(serverShim); + registerClusterCheckupRoutes(serverShim, mockPluginsSetup); }); describe('with cloud enabled', () => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts index 3cfa567755b0f..81cf690d813ad 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts @@ -8,12 +8,20 @@ import _ from 'lodash'; import { ServerShimWithRouter } from '../types'; import { getUpgradeAssistantStatus } from '../lib/es_migration_apis'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; - +import { CloudSetup } from '../../../../../../plugins/cloud/server'; import { createRequestShim } from './create_request_shim'; -export function registerClusterCheckupRoutes(server: ServerShimWithRouter) { +interface PluginsSetup { + cloud?: CloudSetup; +} + +export function registerClusterCheckupRoutes( + server: ServerShimWithRouter, + pluginsSetup: PluginsSetup +) { + const { cloud } = pluginsSetup; + const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - const isCloudEnabled = _.get(server.plugins, 'cloud.config.isCloudEnabled', false); server.router.get( { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts index c488f999b538e..d663361956374 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts @@ -31,11 +31,6 @@ describe('deprecation logging API', () => { serverShim = { router: mockRouter, plugins: { - cloud: { - config: { - isCloudEnabled: true, - }, - }, elasticsearch: { getCluster: () => ({ callWithRequest } as any), } as any, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts index f5bcb03f2b6de..edee1d09cdeeb 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts @@ -12,11 +12,6 @@ export interface ServerShim { plugins: { elasticsearch: ElasticsearchPlugin; xpack_main: XPackMainPlugin; - cloud: { - config: { - isCloudEnabled: boolean; - }; - }; }; log: any; events: any; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts index 246b9c22a08d7..0a57a67b898e0 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_details.ts @@ -6,17 +6,18 @@ import * as t from 'io-ts'; // IO type for validation -export const ErrorType = t.partial({ +export const MonitorErrorType = t.partial({ code: t.number, message: t.string, type: t.string, }); // Typescript type for type checking -export type Error = t.TypeOf; +export type MonitorError = t.TypeOf; export const MonitorDetailsType = t.intersection([ t.type({ monitorId: t.string }), - t.partial({ error: ErrorType }), + t.partial({ error: MonitorErrorType }), + t.partial({ timestamp: t.string }), ]); export type MonitorDetails = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index 690807cc91e27..c8de623cb0a13 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -29,7 +29,7 @@ export const uptime = (kibana: any) => }), main: 'plugins/uptime/app', order: 8900, - url: '/app/uptime/', + url: '/app/uptime#/', }, home: ['plugins/uptime/register_feature'], }, @@ -41,7 +41,7 @@ export const uptime = (kibana: any) => plugin(initializerContext).setup( { - route: (arg: any) => server.route(arg), + route: server.newPlatform.setup.core.http.createRouter(), }, { elasticsearch, diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index c2167993b80b9..3bf1d68590ec0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -81,9 +81,11 @@ exports[`MonitorList component renders a no items message when no data is provid responsive={true} /> - + @@ -248,9 +250,11 @@ exports[`MonitorList component renders the monitor list 1`] = ` responsive={true} /> - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap index 619cce88b309b..91e7b2c07070c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap @@ -81,9 +81,11 @@ exports[`MonitorList component renders a no items message when no data is provid responsive={true} /> - + @@ -248,9 +250,11 @@ exports[`MonitorList component renders the monitor list 1`] = ` responsive={true} /> - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx index 274a7ab0be9be..7b3b636d99b38 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx @@ -33,7 +33,7 @@ import { MonitorListDrawer } from './monitor_list_drawer'; import { MonitorBarSeries } from '../charts'; import { MonitorPageLink } from './monitor_page_link'; import { MonitorListActionsPopover } from './monitor_list_actions_popover'; -import { OverviewPageLink } from '../overview_page_link'; +import { OverviewPageLink } from './overview_page_link'; interface MonitorListQueryResult { monitorStates?: MonitorSummaryResult; @@ -238,8 +238,8 @@ export const MonitorListComponent = (props: Props) => { }, ]} /> - - + + + @@ -27,6 +30,7 @@ exports[`MonitorStatusList component renders checks 1`] = ` } /> + `; @@ -44,6 +48,9 @@ exports[`MonitorStatusList component renders null in place of child status with locationNames={Set {}} status="up" /> + @@ -57,5 +64,6 @@ exports[`MonitorStatusList component renders null in place of child status with } /> + `; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap index cdda21b75770a..5c1fe27d234a3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MostRecentError component renders properly with empty data 1`] = ` +exports[`MostRecentError component renders properly with mock data 1`] = ` Array [

- Most recent error + Most recent error (5 days ago)

,