diff --git a/.ci/Jenkinsfile_visual_baseline b/.ci/Jenkinsfile_visual_baseline new file mode 100644 index 0000000000000..4a1e0f7d74e07 --- /dev/null +++ b/.ci/Jenkinsfile_visual_baseline @@ -0,0 +1,21 @@ +#!/bin/groovy + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +kibanaPipeline(timeoutMinutes: 120) { + catchError { + parallel([ + workers.base(name: 'oss-visualRegression', label: 'linux && immutable') { + kibanaPipeline.buildOss() + kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh') + }, + workers.base(name: 'xpack-visualRegression', label: 'linux && immutable') { + kibanaPipeline.buildXpack() + kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') + }, + ]) + } + + kibanaPipeline.sendMail() +} diff --git a/.eslintrc.js b/.eslintrc.js index 087d6276cd33f..a678243e4f07a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -76,12 +76,6 @@ module.exports = { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['src/legacy/core_plugins/vis_type_vislib/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: [ 'src/legacy/core_plugins/vis_default_editor/public/components/controls/**/*.{ts,tsx}', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index de46bcfa69830..5948b9672e6d4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,7 +5,6 @@ # App /x-pack/legacy/plugins/lens/ @elastic/kibana-app /x-pack/legacy/plugins/graph/ @elastic/kibana-app -/src/plugins/share/ @elastic/kibana-app /src/legacy/server/url_shortening/ @elastic/kibana-app /src/legacy/server/sample_data/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app @@ -27,6 +26,7 @@ /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app /src/plugins/dev_tools/ @elastic/kibana-app +/src/plugins/dashboard_embeddable_container/ @elastic/kibana-app # App Architecture /packages/kbn-interpreter/ @elastic/kibana-app-arch @@ -42,7 +42,6 @@ /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch /src/plugins/bfetch/ @elastic/kibana-app-arch -/src/plugins/dashboard_embeddable_container/ @elastic/kibana-app-arch /src/plugins/data/ @elastic/kibana-app-arch /src/plugins/embeddable/ @elastic/kibana-app-arch /src/plugins/expressions/ @elastic/kibana-app-arch @@ -53,6 +52,9 @@ /src/plugins/navigation/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch /src/plugins/visualizations/ @elastic/kibana-app-arch +/src/plugins/share/ @elastic/kibana-app-arch +/examples/url_generators_examples/ @elastic/kibana-app-arch +/examples/url_generators_explorer/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch @@ -80,12 +82,14 @@ # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui +/x-pack/plugins/ml/ @elastic/ml-ui /x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui /x-pack/test/functional/services/machine_learning/ @elastic/ml-ui /x-pack/test/functional/services/ml.ts @elastic/ml-ui # ML team owns the transform plugin, ES team added here for visibility # because the plugin lives in Kibana's Elasticsearch management section. /x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui +/x-pack/plugins/transform/ @elastic/ml-ui @elastic/es-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui /x-pack/test/functional/services/transform_ui/ @elastic/ml-ui /x-pack/test/functional/services/transform.ts @elastic/ml-ui diff --git a/.gitignore b/.gitignore index 02b20da297fc6..efb5c57774633 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,3 @@ package-lock.json *.sublime-* npm-debug.log* .tern-project -x-pack/legacy/plugins/apm/tsconfig.json -apm.tsconfig.json diff --git a/Jenkinsfile b/Jenkinsfile index 85502369b07be..742aec1d4e7ab 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -40,6 +40,7 @@ kibanaPipeline(timeoutMinutes: 135) { 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + 'xpack-siemCypress': kibanaPipeline.functionalTestProcess('xpack-siemCypress', './test/scripts/jenkins_siem_cypress.sh'), // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), ]), ]) diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index c36d649837e8a..fa052c1179a30 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -20,6 +20,7 @@ export interface CoreSetup | [context](./kibana-plugin-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | +| [metrics](./kibana-plugin-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | | [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | | [uuid](./kibana-plugin-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.metrics.md b/docs/development/core/server/kibana-plugin-server.coresetup.metrics.md new file mode 100644 index 0000000000000..5db723751be85 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.metrics.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [metrics](./kibana-plugin-server.coresetup.metrics.md) + +## CoreSetup.metrics property + +[MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) + +Signature: + +```typescript +metrics: MetricsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-server.destructiveroutemethod.md b/docs/development/core/server/kibana-plugin-server.destructiveroutemethod.md new file mode 100644 index 0000000000000..48b1e837f6db9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.destructiveroutemethod.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [DestructiveRouteMethod](./kibana-plugin-server.destructiveroutemethod.md) + +## DestructiveRouteMethod type + +Set of HTTP methods changing the state of the server. + +Signature: + +```typescript +export declare type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.exportsavedobjectstostream.md b/docs/development/core/server/kibana-plugin-server.exportsavedobjectstostream.md new file mode 100644 index 0000000000000..76f0cea20d637 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.exportsavedobjectstostream.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [exportSavedObjectsToStream](./kibana-plugin-server.exportsavedobjectstostream.md) + +## exportSavedObjectsToStream() function + +Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-server.savedobjectsexportoptions.md) for more detailed information. + +Signature: + +```typescript +export declare function exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, } | SavedObjectsExportOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-server.importsavedobjectsfromstream.md b/docs/development/core/server/kibana-plugin-server.importsavedobjectsfromstream.md new file mode 100644 index 0000000000000..2293e196ae61e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.importsavedobjectsfromstream.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [importSavedObjectsFromStream](./kibana-plugin-server.importsavedobjectsfromstream.md) + +## importSavedObjectsFromStream() function + +Import saved objects from given stream. See the [options](./kibana-plugin-server.savedobjectsimportoptions.md) for more detailed information. + +Signature: + +```typescript +export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, } | SavedObjectsImportOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index c948c89920796..e843ffb265b82 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -37,6 +37,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthResultType](./kibana-plugin-server.authresulttype.md) | | | [AuthStatus](./kibana-plugin-server.authstatus.md) | Status indicating an outcome of the authentication. | +## Functions + +| Function | Description | +| --- | --- | +| [exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-server.savedobjectsexportoptions.md) for more detailed information. | +| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, })](./kibana-plugin-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-server.savedobjectsimportoptions.md) for more detailed information. | +| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, })](./kibana-plugin-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | + ## Interfaces | Interface | Description | @@ -188,6 +196,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md) | Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) | | [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) | A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md).See [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) for more usage examples. | | [ConfigPath](./kibana-plugin-server.configpath.md) | | +| [DestructiveRouteMethod](./kibana-plugin-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | @@ -232,6 +241,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md) | The custom validation function if @kbn/config-schema is not a valid solution for your specific plugin requirements. | | [RouteValidationSpec](./kibana-plugin-server.routevalidationspec.md) | Allowed property validation options: either @kbn/config-schema validations or custom validation functionsSee [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md) for custom validation. | | [RouteValidatorFullConfig](./kibana-plugin-server.routevalidatorfullconfig.md) | Route validations config and options merged into one object | +| [SafeRouteMethod](./kibana-plugin-server.saferoutemethod.md) | Set of HTTP methods not changing the state of the server. | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectMigrationFn](./kibana-plugin-server.savedobjectmigrationfn.md) | A migration function for a [saved object type](./kibana-plugin-server.savedobjectstype.md) used to migrate it to a given version | diff --git a/docs/development/core/server/kibana-plugin-server.resolvesavedobjectsimporterrors.md b/docs/development/core/server/kibana-plugin-server.resolvesavedobjectsimporterrors.md new file mode 100644 index 0000000000000..9fcb335aad556 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.resolvesavedobjectsimporterrors.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [resolveSavedObjectsImportErrors](./kibana-plugin-server.resolvesavedobjectsimporterrors.md) + +## resolveSavedObjectsImportErrors() function + +Resolve and return saved object import errors. See the [options](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. + +Signature: + +```typescript +export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, } | SavedObjectsResolveImportErrorsOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md index 0929e15b6228b..7fbab90cc2c8a 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -19,4 +19,5 @@ export interface RouteConfigOptions | [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | | [body](./kibana-plugin-server.routeconfigoptions.body.md) | Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody | Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md). | | [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | +| [xsrfRequired](./kibana-plugin-server.routeconfigoptions.xsrfrequired.md) | Method extends 'get' ? never : boolean | Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain kbn-xsrf header. - false. Disables xsrf protection.Set to true by default | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.xsrfrequired.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.xsrfrequired.md new file mode 100644 index 0000000000000..801a0c3dc299b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.xsrfrequired.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [xsrfRequired](./kibana-plugin-server.routeconfigoptions.xsrfrequired.md) + +## RouteConfigOptions.xsrfRequired property + +Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain `kbn-xsrf` header. - false. Disables xsrf protection. + +Set to true by default + +Signature: + +```typescript +xsrfRequired?: Method extends 'get' ? never : boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routemethod.md b/docs/development/core/server/kibana-plugin-server.routemethod.md index 939ae94b85691..ed0d8e9af4b19 100644 --- a/docs/development/core/server/kibana-plugin-server.routemethod.md +++ b/docs/development/core/server/kibana-plugin-server.routemethod.md @@ -9,5 +9,5 @@ The set of common HTTP methods supported by Kibana routing. Signature: ```typescript -export declare type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; +export declare type RouteMethod = SafeRouteMethod | DestructiveRouteMethod; ``` diff --git a/docs/development/core/server/kibana-plugin-server.saferoutemethod.md b/docs/development/core/server/kibana-plugin-server.saferoutemethod.md new file mode 100644 index 0000000000000..432aa4c6e7014 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.saferoutemethod.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SafeRouteMethod](./kibana-plugin-server.saferoutemethod.md) + +## SafeRouteMethod type + +Set of HTTP methods not changing the state of the server. + +Signature: + +```typescript +export declare type SafeRouteMethod = 'get' | 'options'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md index 9653fa802a3e8..013773e0789a1 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportOptions | Property | Type | Description | | --- | --- | --- | -| [namespace](./kibana-plugin-server.savedobjectsimportoptions.namespace.md) | string | | -| [objectLimit](./kibana-plugin-server.savedobjectsimportoptions.objectlimit.md) | number | | -| [overwrite](./kibana-plugin-server.savedobjectsimportoptions.overwrite.md) | boolean | | -| [readStream](./kibana-plugin-server.savedobjectsimportoptions.readstream.md) | Readable | | -| [savedObjectsClient](./kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | | -| [supportedTypes](./kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md) | string[] | | +| [namespace](./kibana-plugin-server.savedobjectsimportoptions.namespace.md) | string | if specified, will import in given namespace, else will import as global object | +| [objectLimit](./kibana-plugin-server.savedobjectsimportoptions.objectlimit.md) | number | The maximum number of object to import | +| [overwrite](./kibana-plugin-server.savedobjectsimportoptions.overwrite.md) | boolean | if true, will override existing object if present | +| [readStream](./kibana-plugin-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-server.savedobject.md) to import | +| [savedObjectsClient](./kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | [client](./kibana-plugin-server.savedobjectsclientcontract.md) to use to perform the import operation | +| [supportedTypes](./kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md) | string[] | the list of allowed types to import | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md index 2b15ba2a1b7ec..bf8e56f65607c 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.namespace property +if specified, will import in given namespace, else will import as global object + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md index 89c01a13644b8..526aef96eb8c0 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.objectLimit property +The maximum number of object to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md index 54728aaa80fed..3a84001bbbad4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.overwrite property +if true, will override existing object if present + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md index 7739fdfbc8460..64875d42515aa 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.readStream property +The stream of [saved objects](./kibana-plugin-server.savedobject.md) to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md index 23d5aba5fe114..864fe64f53a4e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.savedObjectsClient property +[client](./kibana-plugin-server.savedobjectsclientcontract.md) to use to perform the import operation + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md index 03ee12ab2a0f7..a897551bfcb12 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md @@ -4,6 +4,8 @@ ## SavedObjectsImportOptions.supportedTypes property +the list of allowed types to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md index 8ed978d4a2639..75c9d77c5bf67 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md @@ -16,10 +16,10 @@ export interface SavedObjectsResolveImportErrorsOptions | Property | Type | Description | | --- | --- | --- | -| [namespace](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | | -| [objectLimit](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | | -| [readStream](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | | -| [retries](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | | -| [savedObjectsClient](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | | -| [supportedTypes](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md) | string[] | | +| [namespace](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | if specified, will import in given namespace | +| [objectLimit](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | The maximum number of object to import | +| [readStream](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-server.savedobject.md) to resolve errors from | +| [retries](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | saved object import references to retry | +| [savedObjectsClient](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | client to use to perform the import operation | +| [supportedTypes](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md) | string[] | the list of allowed types to import | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md index b88f124545bd5..87b69c78b33ee 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.namespace property +if specified, will import in given namespace + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md index a2753ceccc36f..57a3c358406d8 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.objectLimit property +The maximum number of object to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md index e7a31deed6faa..f109816c0de54 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.readStream property +The stream of [saved objects](./kibana-plugin-server.savedobject.md) to resolve errors from + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md index 658aa64cfc33f..265dd21b3728a 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.retries property +saved object import references to retry + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md index 8a8c620b2cf21..9a1864bfbbcd6 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.savedObjectsClient property +client to use to perform the import operation + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md index 9cc97c34669b7..e5db98aec23d9 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md @@ -4,6 +4,8 @@ ## SavedObjectsResolveImportErrorsOptions.supportedTypes property +the list of allowed types to import + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md new file mode 100644 index 0000000000000..d8ec90d1718dc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) > [getImportExportObjectLimit](./kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md) + +## SavedObjectsServiceSetup.getImportExportObjectLimit property + +Returns the maximum number of objects allowed for import or export operations. + +Signature: + +```typescript +getImportExportObjectLimit: () => number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md index 963c4bbeb5515..2cc070d105d9f 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md @@ -54,6 +54,7 @@ export class Plugin() { | Property | Type | Description | | --- | --- | --- | | [addClientWrapper](./kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) with the given priority. | +| [getImportExportObjectLimit](./kibana-plugin-server.savedobjectsservicesetup.getimportexportobjectlimit.md) | () => number | Returns the maximum number of objects allowed for import or export operations. | | [registerType](./kibana-plugin-server.savedobjectsservicesetup.registertype.md) | (type: SavedObjectsType) => void | Register a [savedObjects type](./kibana-plugin-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-server.savedobjectmigrationmap.md) for more details about these. | | [setClientFactoryProvider](./kibana-plugin-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | diff --git a/docs/images/intro-dashboard.png b/docs/images/intro-dashboard.png new file mode 100755 index 0000000000000..5d18acb67bef5 Binary files /dev/null and b/docs/images/intro-dashboard.png differ diff --git a/docs/images/intro-data-tutorial.png b/docs/images/intro-data-tutorial.png new file mode 100644 index 0000000000000..a00e41c8b2a4c Binary files /dev/null and b/docs/images/intro-data-tutorial.png differ diff --git a/docs/images/intro-discover.png b/docs/images/intro-discover.png new file mode 100755 index 0000000000000..27e7a2c728597 Binary files /dev/null and b/docs/images/intro-discover.png differ diff --git a/docs/images/intro-kibana.png b/docs/images/intro-kibana.png new file mode 100644 index 0000000000000..1a59230f2f166 Binary files /dev/null and b/docs/images/intro-kibana.png differ diff --git a/docs/images/intro-management.png b/docs/images/intro-management.png new file mode 100644 index 0000000000000..3c14529a53e90 Binary files /dev/null and b/docs/images/intro-management.png differ diff --git a/docs/images/intro-spaces.jpg b/docs/images/intro-spaces.jpg new file mode 100755 index 0000000000000..7569dfc16b4f7 Binary files /dev/null and b/docs/images/intro-spaces.jpg differ diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index c698e2db86ddb..f62a4d28dfc0d 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -116,7 +116,8 @@ cluster alert notifications from Monitoring. ==== Dashboard [horizontal] -`xpackDashboardMode:roles`:: The roles that belong to <>. +`xpackDashboardMode:roles`:: **Deprecated. Use <> instead.** +The roles that belong to <>. [float] [[kibana-discover-settings]] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 7d0adb9b0e7ef..3d99e7298755f 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -325,10 +325,6 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request's `Referer` header. This setting may not be used when `server.compression.enabled` is set to `false`. -[[server-cors]]`server.cors:`:: *Default: `false`* Set to `true` to enable CORS support. This setting is required to configure `server.cors.origin`. - -`server.cors.origin:`:: *Default: none* Specifies origins. "origin" must be an array. To use this setting, you must set `server.cors` to `true`. To accept all origins, use `server.cors.origin: ["*"]`. - `server.customResponseHeaders:`:: *Default: `{}`* Header names and values to send on all responses to the client from the Kibana server. diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index fcb072c7c925f..bbaf22b497868 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -1,12 +1,165 @@ [[introduction]] -== Introduction +== {kib} — your window into the Elastic Stack +++++ +What is Kibana? +++++ -Kibana is an open source analytics and visualization platform designed to work with Elasticsearch. You use Kibana to -search, view, and interact with data stored in Elasticsearch indices. You can easily perform advanced data analysis -and visualize your data in a variety of charts, tables, and maps. +**_Explore and visualize your data and manage all things Elastic Stack._** -Kibana makes it easy to understand large volumes of data. Its simple, browser-based interface enables you to quickly -create and share dynamic dashboards that display changes to Elasticsearch queries in real time. +Whether you’re a user or admin, {kib} makes your data actionable by providing +three key functions. Kibana is: -Setting up Kibana is a snap. You can install Kibana and start exploring your Elasticsearch indices in minutes -- no -code, no additional infrastructure required. +* **An open-source analytics and visualization platform.** +Use {kib} to explore your {es} data, and then build beautiful visualizations and dashboards. + +* **A UI for managing the Elastic Stack.** +Manage your security settings, assign user roles, take snapshots, roll up your data, +and more — all from the convenience of a {kib} UI. + +* **A centralized hub for Elastic's solutions.** From log analytics to +document discovery to SIEM, {kib} is the portal for accessing these and other capabilities. + +[role="screenshot"] +image::images/intro-kibana.png[] + +[float] +[[get-data-into-kibana]] +=== Getting data into {kib} + +{kib} is designed to use {es} as a data source. Think of Elasticsearch as the engine that stores +and processes the data, with {kib} sitting on top. + +From the home page, {kib} provides these options for getting data in: + +* Set up a data flow to Elasticsearch using our built-in tutorials. +(If a tutorial doesn’t exist for your data, go to the +{beats-ref}/beats-reference.html[Beats overview] to learn about other data shippers +in the {beats} family.) +* <> and take {kib} for a test drive without loading data yourself. +* Import static data using the +https://www.elastic.co/blog/importing-csv-and-log-data-into-elasticsearch-with-file-data-visualizer[file upload feature]. +* Index your data into Elasticsearch with {ref}/getting-started-index.html[REST APIs] + or https://www.elastic.co/guide/en/elasticsearch/client/index.html[client libraries]. ++ +[role="screenshot"] +image::images/intro-data-tutorial.png[Ways to get data in from the home page] + + +{kib} uses an +<> to tell it which {es} indices to explore. +If you add sample data or run a built-in tutorial, you get an index pattern for free, +and are good to start exploring. If you load your own data, you can create +an index pattern in <>. + +[float] +[[explore-and-query]] +=== Explore & query + +Ready to dive into your data? With <>, you can explore your data and +search for hidden insights and relationships. Ask your questions, and then +narrow the results to just the data you want. + +[role="screenshot"] +image::images/intro-discover.png[] + +[float] +[[visualize-and-analyze]] +=== Visualize & analyze + +A visualization is worth a thousand log lines, and {kib} provides +many options for showcasing your data. Use <>, +our drag-and-drop interface, +to rapidly build +charts, tables, metrics, and more. If there +is a better visualization for your data, *Lens* suggests it, allowing for quick +switching between visualization types. + +Once your visualizations are just the way you want, +use <> to collect them in one place. A dashboard provides +insights into your data from multiple perspectives. + +[role="screenshot"] +image::images/intro-dashboard.png[] + +{kib} also offers these visualization features: + +* <> allows you to display your data in +line charts, bar graphs, pie charts, histograms, and tables +(just to name a few). It's also home to *Lens*, mentioned above. +*Visualize* supports the ability to add interactive +controls to your dashboard, and filter dashboard content in real time. + +* <> gives you the ability to present your data in a +visually compelling, pixel-perfect report. Give your data the “wow” factor +needed to impress your CEO or to captivate people with a big-screen display. + +* <> enables you to ask (and answer) meaningful +questions of your location-based data. *Elastic Maps* supports multiple +layers and data sources, mapping of individual geo points and shapes, +and dynamic client-side styling. + +* <> allows you to combine +an infinite number of aggregations to display complex data in a meaningful way. +With TSVB, you can analyze multiple index patterns and customize +every aspect of your visualization. Choose your own date format and color +gradients, and easily switch your data view between time series, metric, +top N, gauge, and markdown. + +[float] +[[organize-and-secure]] +=== Organize & secure + +Want to share Kibana’s goodness with other people or teams? You can do so with +<>, built for organizing your visualizations, dashboards, and indices. +Think of a space as its own mini {kib} installation — it’s isolated from +all other spaces, so you can tailor it to your specific needs without impacting others. + +You can even choose which features to enable within each space. Don’t need +Machine learning in your “Executive” space? Simply turn it off. + +[role="screenshot"] +image::images/intro-spaces.jpg[] + +You can take this all one step further with Kibana’s security features, and +control which users have access to each space. {kib} allows for fine-grained +controls, so you can give a user read-only access to +dashboards in one space, but full access to all of Kibana’s features in another. + +[float] +[[manage-all-things-stack]] +=== Manage all things Elastic Stack + +<> provides guided processes for managing all +things Elastic Stack — indices, clusters, licenses, UI settings, index patterns, +and more. Want to update your {es} indices? Set user roles and privileges? +Turn on dark mode? Kibana has UIs for all that. + +[role="screenshot"] +image::images/intro-management.png[] + +[float] +[[extend-your-use-case]] +=== Extend your use case — or add a new one + +As a hub for Elastic's https://www.elastic.co/products/[solutions], {kib} +can help you find security vulnerabilities, +monitor performance, and address your business needs. Get alerted if a key +metric spikes. Detect anomalous behavior or forecast future spikes. Root out +bottlenecks in your application code. Kibana doesn’t limit or dictate how you explore your data. + +[role="screenshot"] +image::siem/images/detections-ui.png[] + +[float] +[[try-kibana]] +=== Give {kib} a try + +There is no faster way to try out {kib} than with our hosted {es} Service. +https://www.elastic.co/cloud/elasticsearch-service/signup[Sign up for a free trial] +and start exploring data in minutes. + +You can also <> — no code, no additional +infrastructure required. + +Our <> and in-product guidance can +help you get up and running, faster. Use our Help menu if you have questions or feedback. diff --git a/examples/search_explorer/public/es_strategy.tsx b/examples/search_explorer/public/es_strategy.tsx index 5d2617e64a79e..aaf9dada90341 100644 --- a/examples/search_explorer/public/es_strategy.tsx +++ b/examples/search_explorer/public/es_strategy.tsx @@ -33,8 +33,6 @@ import { import { DoSearch } from './do_search'; import { GuideSection } from './guide_section'; -// @ts-ignore -import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_service'; // @ts-ignore import serverStrategy from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_strategy'; @@ -127,10 +125,7 @@ export class EsSearchTest extends React.Component { }, { title: 'Server', - code: [ - { description: 'es_search_service.ts', snippet: serverPlugin }, - { description: 'es_search_strategy.ts', snippet: serverStrategy }, - ], + code: [{ description: 'es_search_strategy.ts', snippet: serverStrategy }], }, ]} demo={this.renderDemo()} diff --git a/examples/ui_action_examples/public/hello_world_action.tsx b/examples/ui_action_examples/public/hello_world_action.tsx index f4c3bfeee6a6d..da20f40464516 100644 --- a/examples/ui_action_examples/public/hello_world_action.tsx +++ b/examples/ui_action_examples/public/hello_world_action.tsx @@ -22,7 +22,7 @@ import { OverlayStart } from '../../../src/core/public'; import { createAction } from '../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../src/plugins/kibana_react/public'; -export const HELLO_WORLD_ACTION_TYPE = 'HELLO_WORLD_ACTION_TYPE'; +export const ACTION_HELLO_WORLD = 'ACTION_HELLO_WORLD'; interface StartServices { openModal: OverlayStart['openModal']; @@ -30,7 +30,7 @@ interface StartServices { export const createHelloWorldAction = (getStartServices: () => Promise) => createAction({ - type: HELLO_WORLD_ACTION_TYPE, + type: ACTION_HELLO_WORLD, getDisplayName: () => 'Hello World!', execute: async () => { const { openModal } = await getStartServices(); diff --git a/examples/ui_action_examples/public/index.ts b/examples/ui_action_examples/public/index.ts index 9dce2191d2670..88a36d278e256 100644 --- a/examples/ui_action_examples/public/index.ts +++ b/examples/ui_action_examples/public/index.ts @@ -23,4 +23,4 @@ import { PluginInitializer } from '../../../src/core/public'; export const plugin: PluginInitializer = () => new UiActionExamplesPlugin(); export { HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; -export { HELLO_WORLD_ACTION_TYPE } from './hello_world_action'; +export { ACTION_HELLO_WORLD } from './hello_world_action'; diff --git a/examples/ui_action_examples/public/plugin.ts b/examples/ui_action_examples/public/plugin.ts index 08b65714dbf66..c47746d4b3fd6 100644 --- a/examples/ui_action_examples/public/plugin.ts +++ b/examples/ui_action_examples/public/plugin.ts @@ -19,7 +19,7 @@ import { Plugin, CoreSetup } from '../../../src/core/public'; import { UiActionsSetup } from '../../../src/plugins/ui_actions/public'; -import { createHelloWorldAction } from './hello_world_action'; +import { createHelloWorldAction, ACTION_HELLO_WORLD } from './hello_world_action'; import { helloWorldTrigger, HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; interface UiActionExamplesSetupDependencies { @@ -28,7 +28,11 @@ interface UiActionExamplesSetupDependencies { declare module '../../../src/plugins/ui_actions/public' { export interface TriggerContextMapping { - [HELLO_WORLD_TRIGGER_ID]: undefined; + [HELLO_WORLD_TRIGGER_ID]: {}; + } + + export interface ActionContextMapping { + [ACTION_HELLO_WORLD]: {}; } } @@ -42,7 +46,7 @@ export class UiActionExamplesPlugin })); uiActions.registerAction(helloWorldAction); - uiActions.attachAction(helloWorldTrigger.id, helloWorldAction.id); + uiActions.attachAction(helloWorldTrigger.id, helloWorldAction); } public start() {} diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx index 2770b0e3bd5ff..64a820ab6d194 100644 --- a/examples/ui_actions_explorer/public/actions/actions.tsx +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -27,44 +27,48 @@ export const USER_TRIGGER = 'USER_TRIGGER'; export const COUNTRY_TRIGGER = 'COUNTRY_TRIGGER'; export const PHONE_TRIGGER = 'PHONE_TRIGGER'; -export const VIEW_IN_MAPS_ACTION = 'VIEW_IN_MAPS_ACTION'; -export const TRAVEL_GUIDE_ACTION = 'TRAVEL_GUIDE_ACTION'; -export const CALL_PHONE_NUMBER_ACTION = 'CALL_PHONE_NUMBER_ACTION'; -export const EDIT_USER_ACTION = 'EDIT_USER_ACTION'; -export const PHONE_USER_ACTION = 'PHONE_USER_ACTION'; -export const SHOWCASE_PLUGGABILITY_ACTION = 'SHOWCASE_PLUGGABILITY_ACTION'; +export const ACTION_VIEW_IN_MAPS = 'ACTION_VIEW_IN_MAPS'; +export const ACTION_TRAVEL_GUIDE = 'ACTION_TRAVEL_GUIDE'; +export const ACTION_CALL_PHONE_NUMBER = 'ACTION_CALL_PHONE_NUMBER'; +export const ACTION_EDIT_USER = 'ACTION_EDIT_USER'; +export const ACTION_PHONE_USER = 'ACTION_PHONE_USER'; +export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY'; -export const showcasePluggability = createAction({ - type: SHOWCASE_PLUGGABILITY_ACTION, +export const showcasePluggability = createAction({ + type: ACTION_SHOWCASE_PLUGGABILITY, getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', execute: async () => alert("Isn't that cool?!"), }); -export type PhoneContext = string; +export interface PhoneContext { + phone: string; +} -export const makePhoneCallAction = createAction({ - type: CALL_PHONE_NUMBER_ACTION, +export const makePhoneCallAction = createAction({ + type: ACTION_CALL_PHONE_NUMBER, getDisplayName: () => 'Call phone number', - execute: async phone => alert(`Pretend calling ${phone}...`), + execute: async context => alert(`Pretend calling ${context.phone}...`), }); -export const lookUpWeatherAction = createAction<{ country: string }>({ - type: TRAVEL_GUIDE_ACTION, +export const lookUpWeatherAction = createAction({ + type: ACTION_TRAVEL_GUIDE, getIconType: () => 'popout', getDisplayName: () => 'View travel guide', - execute: async ({ country }) => { - window.open(`https://www.worldtravelguide.net/?s=${country},`, '_blank'); + execute: async context => { + window.open(`https://www.worldtravelguide.net/?s=${context.country}`, '_blank'); }, }); -export type CountryContext = string; +export interface CountryContext { + country: string; +} -export const viewInMapsAction = createAction({ - type: VIEW_IN_MAPS_ACTION, +export const viewInMapsAction = createAction({ + type: ACTION_VIEW_IN_MAPS, getIconType: () => 'popout', getDisplayName: () => 'View in maps', - execute: async country => { - window.open(`https://www.google.com/maps/place/${country}`, '_blank'); + execute: async context => { + window.open(`https://www.google.com/maps/place/${context.country}`, '_blank'); }, }); @@ -100,11 +104,8 @@ function EditUserModal({ } export const createEditUserAction = (getOpenModal: () => Promise) => - createAction<{ - user: User; - update: (user: User) => void; - }>({ - type: EDIT_USER_ACTION, + createAction({ + type: ACTION_EDIT_USER, getIconType: () => 'pencil', getDisplayName: () => 'Edit user', execute: async ({ user, update }) => { @@ -120,8 +121,8 @@ export interface UserContext { } export const createPhoneUserAction = (getUiActionsApi: () => Promise) => - createAction({ - type: PHONE_USER_ACTION, + createAction({ + type: ACTION_PHONE_USER, getDisplayName: () => 'Call phone number', isCompatible: async ({ user }) => user.phone !== undefined, execute: async ({ user }) => { @@ -133,7 +134,7 @@ export const createPhoneUserAction = (getUiActionsApi: () => Promise { uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, undefined)} + onClick={() => uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})} > Say hello world! @@ -76,8 +76,9 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { { - const dynamicAction = createAction<{}>({ - type: `${HELLO_WORLD_ACTION_TYPE}-${name}`, + const dynamicAction = createAction({ + id: `${ACTION_HELLO_WORLD}-${name}`, + type: ACTION_HELLO_WORLD, getDisplayName: () => `Say hello to ${name}`, execute: async () => { const overlay = openModal( @@ -95,7 +96,7 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { }, }); uiActionsApi.registerAction(dynamicAction); - uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction.type); + uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); setConfirmationText( `You've successfully added a new action: ${dynamicAction.getDisplayName( {} diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx index fecada71099e8..f1895905a45e1 100644 --- a/examples/ui_actions_explorer/public/plugin.tsx +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -27,17 +27,17 @@ import { lookUpWeatherAction, viewInMapsAction, createEditUserAction, - CALL_PHONE_NUMBER_ACTION, - VIEW_IN_MAPS_ACTION, - TRAVEL_GUIDE_ACTION, - PHONE_USER_ACTION, - EDIT_USER_ACTION, makePhoneCallAction, showcasePluggability, - SHOWCASE_PLUGGABILITY_ACTION, UserContext, CountryContext, PhoneContext, + ACTION_EDIT_USER, + ACTION_SHOWCASE_PLUGGABILITY, + ACTION_CALL_PHONE_NUMBER, + ACTION_TRAVEL_GUIDE, + ACTION_VIEW_IN_MAPS, + ACTION_PHONE_USER, } from './actions/actions'; interface StartDeps { @@ -54,6 +54,15 @@ declare module '../../../src/plugins/ui_actions/public' { [COUNTRY_TRIGGER]: CountryContext; [PHONE_TRIGGER]: PhoneContext; } + + export interface ActionContextMapping { + [ACTION_EDIT_USER]: UserContext; + [ACTION_SHOWCASE_PLUGGABILITY]: {}; + [ACTION_CALL_PHONE_NUMBER]: PhoneContext; + [ACTION_TRAVEL_GUIDE]: CountryContext; + [ACTION_VIEW_IN_MAPS]: CountryContext; + [ACTION_PHONE_USER]: UserContext; + } } export class UiActionsExplorerPlugin implements Plugin { @@ -67,29 +76,24 @@ export class UiActionsExplorerPlugin implements Plugin (await startServices)[1].uiActions) ); - deps.uiActions.registerAction( + deps.uiActions.attachAction( + USER_TRIGGER, createEditUserAction(async () => (await startServices)[0].overlays.openModal) ); - deps.uiActions.attachAction(USER_TRIGGER, PHONE_USER_ACTION); - deps.uiActions.attachAction(USER_TRIGGER, EDIT_USER_ACTION); - // What's missing here is type analysis to ensure the context emitted by the trigger - // is the same context that the action requires. - deps.uiActions.attachAction(COUNTRY_TRIGGER, VIEW_IN_MAPS_ACTION); - deps.uiActions.attachAction(COUNTRY_TRIGGER, TRAVEL_GUIDE_ACTION); - deps.uiActions.attachAction(COUNTRY_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); - deps.uiActions.attachAction(PHONE_TRIGGER, CALL_PHONE_NUMBER_ACTION); - deps.uiActions.attachAction(PHONE_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); - deps.uiActions.attachAction(USER_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); + deps.uiActions.attachAction(COUNTRY_TRIGGER, viewInMapsAction); + deps.uiActions.attachAction(COUNTRY_TRIGGER, lookUpWeatherAction); + deps.uiActions.attachAction(COUNTRY_TRIGGER, showcasePluggability); + deps.uiActions.attachAction(PHONE_TRIGGER, makePhoneCallAction); + deps.uiActions.attachAction(PHONE_TRIGGER, showcasePluggability); + deps.uiActions.attachAction(USER_TRIGGER, showcasePluggability); core.application.register({ id: 'uiActionsExplorer', diff --git a/examples/ui_actions_explorer/public/trigger_context_example.tsx b/examples/ui_actions_explorer/public/trigger_context_example.tsx index 00d974e938138..4b88652103966 100644 --- a/examples/ui_actions_explorer/public/trigger_context_example.tsx +++ b/examples/ui_actions_explorer/public/trigger_context_example.tsx @@ -47,7 +47,7 @@ const createRowData = ( { - uiActionsApi.executeTriggerActions(COUNTRY_TRIGGER, user.countryOfResidence); + uiActionsApi.executeTriggerActions(COUNTRY_TRIGGER, { country: user.countryOfResidence }); }} > {user.countryOfResidence} @@ -59,7 +59,7 @@ const createRowData = ( { - uiActionsApi.executeTriggerActions(PHONE_TRIGGER, user.phone!); + uiActionsApi.executeTriggerActions(PHONE_TRIGGER, { phone: user.phone! }); }} > {user.phone} diff --git a/examples/url_generators_examples/README.md b/examples/url_generators_examples/README.md new file mode 100644 index 0000000000000..facd5c90c8c96 --- /dev/null +++ b/examples/url_generators_examples/README.md @@ -0,0 +1,7 @@ +## Access links examples + +This example app shows how to: + - Register a direct access link generator. + - Handle migration of legacy generators into a new one. + +To run this example, use the command `yarn start --run-examples`. Navigate to the access links explorer app \ No newline at end of file diff --git a/examples/url_generators_examples/kibana.json b/examples/url_generators_examples/kibana.json new file mode 100644 index 0000000000000..0767018e3bb98 --- /dev/null +++ b/examples/url_generators_examples/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "urlGeneratorsExamples", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["url_generators_examples"], + "server": false, + "ui": true, + "requiredPlugins": ["share"], + "optionalPlugins": [] +} diff --git a/examples/url_generators_examples/package.json b/examples/url_generators_examples/package.json new file mode 100644 index 0000000000000..e07482db25f43 --- /dev/null +++ b/examples/url_generators_examples/package.json @@ -0,0 +1,17 @@ +{ + "name": "url_generators_examples", + "version": "1.0.0", + "main": "target/examples/url_generators_examples", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/examples/url_generators_examples/public/app.tsx b/examples/url_generators_examples/public/app.tsx new file mode 100644 index 0000000000000..c39cd876ea9b1 --- /dev/null +++ b/examples/url_generators_examples/public/app.tsx @@ -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 React from 'react'; +import ReactDOM from 'react-dom'; + +import { EuiPageBody } from '@elastic/eui'; +import { EuiPageContent } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; +import { Route, Switch, Redirect, Router, useLocation } from 'react-router-dom'; +import { createBrowserHistory } from 'history'; +import { EuiText } from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; + +function useQuery() { + const { search } = useLocation(); + const params = React.useMemo(() => new URLSearchParams(search), [search]); + return params; +} + +interface HelloPageProps { + firstName: string; + lastName: string; +} + +const HelloPage = ({ firstName, lastName }: HelloPageProps) => ( + {`Hello ${firstName} ${lastName}`} +); + +export const Routes: React.FC<{}> = () => { + const query = useQuery(); + + return ( + + + + + + + + + + + + + ); +}; + +export const LinksExample: React.FC<{ + appBasePath: string; +}> = props => { + const history = React.useMemo( + () => + createBrowserHistory({ + basename: props.appBasePath, + }), + [props.appBasePath] + ); + return ( + + + + ); +}; + +export const renderApp = (props: { appBasePath: string }, { element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/legacy/core_plugins/visualizations/public/legacy_mocks.ts b/examples/url_generators_examples/public/index.ts similarity index 83% rename from src/legacy/core_plugins/visualizations/public/legacy_mocks.ts rename to examples/url_generators_examples/public/index.ts index 6cd57bb88bc26..e87f9237bff38 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_mocks.ts +++ b/examples/url_generators_examples/public/index.ts @@ -17,5 +17,6 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { searchSourceMock } from '../../../../plugins/data/public/search/search_source/mocks'; +import { AccessLinksExamplesPlugin } from './plugin'; + +export const plugin = () => new AccessLinksExamplesPlugin(); diff --git a/examples/url_generators_examples/public/plugin.tsx b/examples/url_generators_examples/public/plugin.tsx new file mode 100644 index 0000000000000..016494037ec05 --- /dev/null +++ b/examples/url_generators_examples/public/plugin.tsx @@ -0,0 +1,76 @@ +/* + * 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 { SharePluginStart, SharePluginSetup } from '../../../src/plugins/share/public'; +import { Plugin, CoreSetup, AppMountParameters } from '../../../src/core/public'; +import { + HelloLinkGeneratorState, + createHelloPageLinkGenerator, + LegacyHelloLinkGeneratorState, + HELLO_URL_GENERATOR_V1, + HELLO_URL_GENERATOR, + helloPageLinkGeneratorV1, +} from './url_generator'; + +declare module '../../../src/plugins/share/public' { + export interface UrlGeneratorStateMapping { + [HELLO_URL_GENERATOR_V1]: LegacyHelloLinkGeneratorState; + [HELLO_URL_GENERATOR]: HelloLinkGeneratorState; + } +} + +interface StartDeps { + share: SharePluginStart; +} + +interface SetupDeps { + share: SharePluginSetup; +} + +const APP_ID = 'urlGeneratorsExamples'; + +export class AccessLinksExamplesPlugin implements Plugin { + public setup(core: CoreSetup, { share: { urlGenerators } }: SetupDeps) { + urlGenerators.registerUrlGenerator( + createHelloPageLinkGenerator(async () => ({ + appBasePath: (await core.getStartServices())[0].application.getUrlForApp(APP_ID), + })) + ); + + urlGenerators.registerUrlGenerator(helloPageLinkGeneratorV1); + + core.application.register({ + id: APP_ID, + title: 'Access links examples', + async mount(params: AppMountParameters) { + const { renderApp } = await import('./app'); + return renderApp( + { + appBasePath: params.appBasePath, + }, + params + ); + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/url_generators_examples/public/url_generator.ts b/examples/url_generators_examples/public/url_generator.ts new file mode 100644 index 0000000000000..f21b1c9295e66 --- /dev/null +++ b/examples/url_generators_examples/public/url_generator.ts @@ -0,0 +1,78 @@ +/* + * 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 url from 'url'; +import { UrlGeneratorState, UrlGeneratorsDefinition } from '../../../src/plugins/share/public'; + +/** + * The name of the latest variable can always stay the same so code that + * uses this link generator statically will switch to the latest version. + * Typescript will warn the developer if incorrect state is being passed + * down. + */ +export const HELLO_URL_GENERATOR = 'HELLO_URL_GENERATOR_V2'; + +export interface HelloLinkState { + firstName: string; + lastName: string; +} + +export type HelloLinkGeneratorState = UrlGeneratorState; + +export const createHelloPageLinkGenerator = ( + getStartServices: () => Promise<{ appBasePath: string }> +): UrlGeneratorsDefinition => ({ + id: HELLO_URL_GENERATOR, + createUrl: async state => { + const startServices = await getStartServices(); + const appBasePath = startServices.appBasePath; + const parsedUrl = url.parse(window.location.href); + + return url.format({ + protocol: parsedUrl.protocol, + host: parsedUrl.host, + pathname: `${appBasePath}/hello`, + query: { + ...state, + }, + }); + }, +}); + +/** + * The name of this legacy generator id changes, but the *value* stays the same. + */ +export const HELLO_URL_GENERATOR_V1 = 'HELLO_URL_GENERATOR'; + +export interface HelloLinkStateV1 { + name: string; +} + +export type LegacyHelloLinkGeneratorState = UrlGeneratorState< + HelloLinkStateV1, + typeof HELLO_URL_GENERATOR, + HelloLinkState +>; + +export const helloPageLinkGeneratorV1: UrlGeneratorsDefinition = { + id: HELLO_URL_GENERATOR_V1, + isDeprecated: true, + migrate: async state => { + return { id: HELLO_URL_GENERATOR, state: { firstName: state.name, lastName: '' } }; + }, +}; diff --git a/examples/url_generators_examples/tsconfig.json b/examples/url_generators_examples/tsconfig.json new file mode 100644 index 0000000000000..091130487791b --- /dev/null +++ b/examples/url_generators_examples/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [] +} diff --git a/examples/url_generators_explorer/README.md b/examples/url_generators_explorer/README.md new file mode 100644 index 0000000000000..922cf37aff847 --- /dev/null +++ b/examples/url_generators_explorer/README.md @@ -0,0 +1,8 @@ +## Access links explorer + +This example app shows how to: + - Generate links to other applications + - Generate dynamic links, when the target application is not known + - Handle backward compatibility of urls + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/url_generators_explorer/kibana.json b/examples/url_generators_explorer/kibana.json new file mode 100644 index 0000000000000..94ab75b338889 --- /dev/null +++ b/examples/url_generators_explorer/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "urlGeneratorsExplorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["url_generators_explorer"], + "server": false, + "ui": true, + "requiredPlugins": ["share", "urlGeneratorsExamples"], + "optionalPlugins": [] +} diff --git a/examples/url_generators_explorer/package.json b/examples/url_generators_explorer/package.json new file mode 100644 index 0000000000000..52da533dc0c05 --- /dev/null +++ b/examples/url_generators_explorer/package.json @@ -0,0 +1,17 @@ +{ + "name": "url_generators_explorer", + "version": "1.0.0", + "main": "target/examples/url_generators_explorer", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/examples/url_generators_explorer/public/app.tsx b/examples/url_generators_explorer/public/app.tsx new file mode 100644 index 0000000000000..77e804ae08c5f --- /dev/null +++ b/examples/url_generators_explorer/public/app.tsx @@ -0,0 +1,170 @@ +/* + * 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 React, { useState, useEffect } from 'react'; +import ReactDOM from 'react-dom'; + +import { EuiPage } from '@elastic/eui'; + +import { EuiButton } from '@elastic/eui'; +import { EuiPageBody } from '@elastic/eui'; +import { EuiPageContent } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; +import { UrlGeneratorsService } from '../../../src/plugins/share/public'; +import { + HELLO_URL_GENERATOR, + HELLO_URL_GENERATOR_V1, +} from '../../url_generators_examples/public/url_generator'; + +interface Props { + getLinkGenerator: UrlGeneratorsService['getUrlGenerator']; +} + +interface MigratedLink { + isDeprecated: boolean; + linkText: string; + link: string; +} + +const ActionsExplorer = ({ getLinkGenerator }: Props) => { + const [migratedLinks, setMigratedLinks] = useState([] as MigratedLink[]); + const [buildingLinks, setBuildingLinks] = useState(false); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + /** + * Lets pretend we grabbed these links from a persistent store, like a saved object. + * Some of these links were created with older versions of the hello link generator. + * They use deprecated generator ids. + */ + const [persistedLinks, setPersistedLinks] = useState([ + { + id: HELLO_URL_GENERATOR_V1, + linkText: 'Say hello to Mary', + state: { + name: 'Mary', + }, + }, + { + id: HELLO_URL_GENERATOR, + linkText: 'Say hello to George', + state: { + firstName: 'George', + lastName: 'Washington', + }, + }, + ]); + + useEffect(() => { + setBuildingLinks(true); + + const updateLinks = async () => { + const updatedLinks = await Promise.all( + persistedLinks.map(async savedLink => { + const generator = getLinkGenerator(savedLink.id); + const link = await generator.createUrl(savedLink.state); + return { + isDeprecated: generator.isDeprecated, + linkText: savedLink.linkText, + link, + }; + }) + ); + setMigratedLinks(updatedLinks); + setBuildingLinks(false); + }; + + updateLinks(); + }, [getLinkGenerator, persistedLinks]); + + return ( + + + Access links explorer + + + +

Create new links using the most recent version of a url generator.

+
+ { + setFirstName(e.target.value); + }} + /> + setLastName(e.target.value)} /> + + setPersistedLinks([ + ...persistedLinks, + { + id: HELLO_URL_GENERATOR, + state: { firstName, lastName }, + linkText: `Say hello to ${firstName} ${lastName}`, + }, + ]) + } + > + Add new link + + + + +

+ Existing links retrieved from storage. The links that were generated from legacy + generators are in red. This can be useful for developers to know they will have to + migrate persisted state or in a future version of Kibana, these links may no longer + work. They still work now because legacy url generators must provide a state + migration function. +

+
+ {buildingLinks ? ( +
loading...
+ ) : ( + migratedLinks.map(link => ( + + + {link.linkText} + +
+
+ )) + )} +
+
+
+
+ ); +}; + +export const renderApp = (props: Props, { element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/examples/url_generators_explorer/public/index.ts similarity index 76% rename from src/legacy/core_plugins/visualizations/public/legacy_imports.ts rename to examples/url_generators_explorer/public/index.ts index 0a3b1938436c0..30ff481dbe3a5 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/examples/url_generators_explorer/public/index.ts @@ -17,11 +17,6 @@ * under the License. */ -export { - IAggConfig, - IAggConfigs, - isDateHistogramBucketAggConfig, - setBounds, -} from '../../data/public'; -export { createAggConfigs } from 'ui/agg_types'; -export { createSavedSearchesLoader } from '../../../../plugins/discover/public'; +import { AccessLinksExplorerPlugin } from './plugin'; + +export const plugin = () => new AccessLinksExplorerPlugin(); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.mocks.ts b/examples/url_generators_explorer/public/page.tsx similarity index 54% rename from src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.mocks.ts rename to examples/url_generators_explorer/public/page.tsx index a0b9d7c779b02..90bea35804822 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.mocks.ts +++ b/examples/url_generators_explorer/public/page.tsx @@ -17,26 +17,35 @@ * under the License. */ +import React from 'react'; + import { - notificationServiceMock, - overlayServiceMock, - httpServiceMock, - injectedMetadataServiceMock, -} from '../../../../../../../core/public/mocks'; + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +interface PageProps { + title: string; + children: React.ReactNode; +} -jest.doMock('ui/new_platform', () => { - return { - npSetup: { - core: { - notifications: notificationServiceMock.createSetupContract(), - }, - }, - npStart: { - core: { - overlays: overlayServiceMock.createStartContract(), - http: httpServiceMock.createStartContract({ basePath: 'path' }), - injectedMetadata: injectedMetadataServiceMock.createStartContract(), - }, - }, - }; -}); +export function Page({ title, children }: PageProps) { + return ( + + + + +

{title}

+
+
+
+ + {children} + +
+ ); +} diff --git a/src/plugins/data/server/search/es_search/es_search_service.ts b/examples/url_generators_explorer/public/plugin.tsx similarity index 52% rename from src/plugins/data/server/search/es_search/es_search_service.ts rename to examples/url_generators_explorer/public/plugin.tsx index b33b6c6ecd318..1fe70476b8e79 100644 --- a/src/plugins/data/server/search/es_search/es_search_service.ts +++ b/examples/url_generators_explorer/public/plugin.tsx @@ -17,26 +17,32 @@ * under the License. */ -import { ISearchSetup } from '../i_search_setup'; -import { PluginInitializerContext, CoreSetup, Plugin } from '../../../../../core/server'; -import { esSearchStrategyProvider } from './es_search_strategy'; -import { ES_SEARCH_STRATEGY } from '../../../common/search'; +import { SharePluginStart } from 'src/plugins/share/public'; +import { Plugin, CoreSetup, AppMountParameters } from '../../../src/core/public'; -interface IEsSearchDependencies { - search: ISearchSetup; +interface StartDeps { + share: SharePluginStart; } -export class EsSearchService implements Plugin { - constructor(private initializerContext: PluginInitializerContext) {} - - public setup(core: CoreSetup, deps: IEsSearchDependencies) { - deps.search.registerSearchStrategyProvider( - this.initializerContext.opaqueId, - ES_SEARCH_STRATEGY, - esSearchStrategyProvider - ); +export class AccessLinksExplorerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.application.register({ + id: 'urlGeneratorsExplorer', + title: 'Access links explorer', + async mount(params: AppMountParameters) { + const depsStart = (await core.getStartServices())[1]; + const { renderApp } = await import('./app'); + return renderApp( + { + getLinkGenerator: depsStart.share.urlGenerators.getUrlGenerator, + }, + params + ); + }, + }); } public start() {} + public stop() {} } diff --git a/examples/url_generators_explorer/tsconfig.json b/examples/url_generators_explorer/tsconfig.json new file mode 100644 index 0000000000000..091130487791b --- /dev/null +++ b/examples/url_generators_explorer/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [] +} diff --git a/src/legacy/ui/public/time_buckets/index.d.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/async_import.ts similarity index 91% rename from src/legacy/ui/public/time_buckets/index.d.ts rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/async_import.ts index 70b9495b81f0e..9a51937cbac1e 100644 --- a/src/legacy/ui/public/time_buckets/index.d.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/async_import.ts @@ -17,6 +17,4 @@ * under the License. */ -declare module 'ui/time_buckets' { - export const TimeBuckets: any; -} +export function foo() {} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts index 9d3871df24739..1ba0b69681152 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts @@ -19,3 +19,7 @@ export * from './lib'; export * from './ext'; + +export async function getFoo() { + return await import('./async_import'); +} diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 1a974d3e81092..d52d89eebe2f1 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`builds expected bundles, saves bundle counts to metadata: 1 async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,exports,__webpack_require__){\\"use strict\\";Object.defineProperty(exports,\\"__esModule\\",{value:true});exports.foo=foo;function foo(){}}}]);"`; + exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConfig 1`] = ` OptimizerConfig { "bundles": Array [ @@ -55,6 +57,6 @@ OptimizerConfig { } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"__REPLACE_WITH_PUBLIC_PATH__\\";return __webpack_require__(__webpack_require__.s=4)}([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/foo.plugin.js'), 'utf8') ).toMatchSnapshot('foo bundle'); + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/1.plugin.js'), 'utf8') + ).toMatchSnapshot('1 async bundle'); + expect( Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/bar/target/public/bar.plugin.js'), 'utf8') ).toMatchSnapshot('bar bundle'); @@ -135,9 +139,10 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const foo = config.bundles.find(b => b.id === 'foo')!; expect(foo).toBeTruthy(); foo.cache.refresh(); - expect(foo.cache.getModuleCount()).toBe(3); + expect(foo.cache.getModuleCount()).toBe(4); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, @@ -148,8 +153,8 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(bar).toBeTruthy(); bar.cache.refresh(); expect(bar.cache.getModuleCount()).toBe( - // code + styles + style/css-loader runtime - 14 + // code + styles + style/css-loader runtimes + 15 ); expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` @@ -159,6 +164,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index 7a8097fd2b2c7..e87ddc7d0185c 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -127,7 +127,7 @@ const observeCompiler = ( ); } - const files = Array.from(referencedFiles); + const files = Array.from(referencedFiles).sort(ascending(p => p)); const mtimes = new Map( files.map((path): [string, number | undefined] => { try { @@ -146,7 +146,7 @@ const observeCompiler = ( optimizerCacheKey: workerConfig.optimizerCacheKey, cacheKey: bundle.createCacheKey(files, mtimes), moduleCount: normalModules.length, - files: files.sort(ascending(f => f)), + files, }); return compilerMsgs.compilerSuccess({ diff --git a/packages/kbn-storybook/storybook_config/config.js b/packages/kbn-storybook/storybook_config/config.js index a7975773e675b..d97bd3f7c2dcc 100644 --- a/packages/kbn-storybook/storybook_config/config.js +++ b/packages/kbn-storybook/storybook_config/config.js @@ -55,7 +55,7 @@ addParameters({ brandTitle: 'Kibana Storybook', brandUrl: 'https://github.com/elastic/kibana/tree/master/packages/kbn-storybook', }), - showPanel: true, + showPanel: false, isFullscreen: false, panelPosition: 'bottom', isToolshown: true, diff --git a/packages/kbn-test/src/ci_parallel_process_prefix.ts b/packages/kbn-test/src/ci_parallel_process_prefix.ts new file mode 100644 index 0000000000000..67dafc7e85324 --- /dev/null +++ b/packages/kbn-test/src/ci_parallel_process_prefix.ts @@ -0,0 +1,30 @@ +/* + * 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 job = process.env.JOB ? `job-${process.env.JOB}-` : ''; +const num = process.env.CI_PARALLEL_PROCESS_NUMBER + ? `worker-${process.env.CI_PARALLEL_PROCESS_NUMBER}-` + : ''; + +/** + * A prefix string that is unique for each parallel CI process that + * is an empty string outside of CI, so it can be safely injected + * into strings as a prefix + */ +export const CI_PARALLEL_PROCESS_PREFIX = `${job}${num}`; 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 d8a952bee42e5..7da79b5b67e63 100644 --- a/packages/kbn-test/src/failed_tests_reporter/github_api.ts +++ b/packages/kbn-test/src/failed_tests_reporter/github_api.ts @@ -33,6 +33,15 @@ export interface GithubIssue { body: string; } +/** + * Minimal GithubIssue type that can be easily replicated by dry-run helpers + */ +export interface GithubIssueMini { + number: GithubIssue['number']; + body: GithubIssue['body']; + html_url: GithubIssue['html_url']; +} + type RequestOptions = AxiosRequestConfig & { safeForDryRun?: boolean; maxAttempts?: number; @@ -162,7 +171,7 @@ export class GithubApi { } async createIssue(title: string, body: string, labels?: string[]) { - const resp = await this.request( + const resp = await this.request( { method: 'POST', url: Url.resolve(BASE_URL, 'issues'), @@ -173,11 +182,13 @@ export class GithubApi { }, }, { + body, + number: 999, html_url: 'https://dryrun', } ); - return resp.data.html_url; + return resp.data; } private async request( 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 ef6ab3c51ab19..5bbc72fe04e86 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 @@ -78,9 +78,7 @@ describe('updateFailureIssue()', () => { 'https://build-url', { html_url: 'https://github.com/issues/1234', - labels: ['some-label'], number: 1234, - title: 'issue title', body: dedent` # existing issue body 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 97e9d517576fc..1413d05498459 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts @@ -18,7 +18,7 @@ */ import { TestFailure } from './get_failures'; -import { GithubIssue, GithubApi } from './github_api'; +import { GithubIssueMini, GithubApi } from './github_api'; import { getIssueMetadata, updateIssueMetadata } from './issue_metadata'; export async function createFailureIssue(buildUrl: string, failure: TestFailure, api: GithubApi) { @@ -44,7 +44,7 @@ export async function createFailureIssue(buildUrl: string, failure: TestFailure, return await api.createIssue(title, body, ['failed-test']); } -export async function updateFailureIssue(buildUrl: string, issue: GithubIssue, api: GithubApi) { +export async function updateFailureIssue(buildUrl: string, issue: GithubIssueMini, api: GithubApi) { // Increment failCount const newCount = getIssueMetadata(issue.body, 'test.failCount', 0) + 1; const newBody = updateIssueMetadata(issue.body, { 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 fc52fa6cbf9e7..9324f9eb42aa5 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 @@ -20,8 +20,8 @@ import { REPO_ROOT, run, createFailError, createFlagError } from '@kbn/dev-utils'; import globby from 'globby'; -import { getFailures } from './get_failures'; -import { GithubApi } from './github_api'; +import { getFailures, TestFailure } from './get_failures'; +import { GithubApi, GithubIssueMini } from './github_api'; import { updateFailureIssue, createFailureIssue } from './report_failure'; import { getIssueMetadata } from './issue_metadata'; import { readTestReport } from './test_report'; @@ -73,6 +73,11 @@ export function runFailedTestsReporterCli() { absolute: true, }); + const newlyCreatedIssues: Array<{ + failure: TestFailure; + newIssue: GithubIssueMini; + }> = []; + for (const reportPath of reportPaths) { const report = await readTestReport(reportPath); const messages = Array.from(getReportMessageIter(report)); @@ -94,12 +99,22 @@ export function runFailedTestsReporterCli() { continue; } - const existingIssue = await githubApi.findFailedTestIssue( + let existingIssue: GithubIssueMini | undefined = await githubApi.findFailedTestIssue( i => getIssueMetadata(i.body, 'test.class') === failure.classname && getIssueMetadata(i.body, 'test.name') === failure.name ); + if (!existingIssue) { + const newlyCreated = newlyCreatedIssues.find( + ({ failure: f }) => f.classname === failure.classname && f.name === failure.name + ); + + if (newlyCreated) { + existingIssue = newlyCreated.newIssue; + } + } + if (existingIssue) { const newFailureCount = await updateFailureIssue(buildUrl, existingIssue, githubApi); const url = existingIssue.html_url; @@ -110,11 +125,12 @@ export function runFailedTestsReporterCli() { continue; } - const newIssueUrl = await createFailureIssue(buildUrl, failure, githubApi); + const newIssue = await createFailureIssue(buildUrl, failure, githubApi); pushMessage('Test has not failed recently on tracked branches'); if (updateGithub) { - pushMessage(`Created new issue: ${newIssueUrl}`); + pushMessage(`Created new issue: ${newIssue.html_url}`); } + newlyCreatedIssues.push({ failure, newIssue }); } // mutates report to include messages and writes updated report to disk diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js index 136704a639bff..5f58190078f0d 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js @@ -42,10 +42,11 @@ export async function runElasticsearch({ config, options }) { esFrom: esFrom || config.get('esTestCluster.from'), dataArchive: config.get('esTestCluster.dataArchive'), esArgs, + esEnvVars, ssl, }); - await cluster.start(esArgs, esEnvVars); + await cluster.start(); if (isSecurityEnabled) { await setupUsers({ diff --git a/packages/kbn-test/src/junit_report_path.ts b/packages/kbn-test/src/junit_report_path.ts index d46c9455dcff0..90405d7a89c02 100644 --- a/packages/kbn-test/src/junit_report_path.ts +++ b/packages/kbn-test/src/junit_report_path.ts @@ -18,17 +18,13 @@ */ import { resolve } from 'path'; - -const job = process.env.JOB ? `job-${process.env.JOB}-` : ''; -const num = process.env.CI_PARALLEL_PROCESS_NUMBER - ? `worker-${process.env.CI_PARALLEL_PROCESS_NUMBER}-` - : ''; +import { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; export function makeJunitReportPath(rootDirectory: string, reportName: string) { return resolve( rootDirectory, 'target/junit', process.env.JOB || '.', - `TEST-${job}${num}${reportName}.xml` + `TEST-${CI_PARALLEL_PROCESS_PREFIX}${reportName}.xml` ); } diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index 355304c86a3c3..f795b32d78b8e 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -22,6 +22,7 @@ import { format } from 'url'; import { get } from 'lodash'; import toPath from 'lodash/internal/toPath'; import { Cluster } from '@kbn/es'; +import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; import { esTestConfig } from './es_test_config'; import { KIBANA_ROOT } from '../'; @@ -38,14 +39,22 @@ export function createLegacyEsTestCluster(options = {}) { basePath = resolve(KIBANA_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), dataArchive, - esArgs, + esArgs: customEsArgs = [], + esEnvVars, + clusterName: customClusterName = 'es-test-cluster', ssl, } = options; - const randomHash = Math.random() - .toString(36) - .substring(2); - const clusterName = `test-${randomHash}`; + const clusterName = `${CI_PARALLEL_PROCESS_PREFIX}${customClusterName}`; + + const esArgs = [ + `cluster.name=${clusterName}`, + `http.port=${port}`, + 'discovery.type=single-node', + `transport.port=${esTestConfig.getTransportPort()}`, + ...customEsArgs, + ]; + const config = { version: esTestConfig.getVersion(), installPath: resolve(basePath, clusterName), @@ -55,7 +64,6 @@ export function createLegacyEsTestCluster(options = {}) { basePath, esArgs, }; - const transportPort = esTestConfig.getTransportPort(); const cluster = new Cluster({ log, ssl }); @@ -67,7 +75,7 @@ export function createLegacyEsTestCluster(options = {}) { return esFrom === 'snapshot' ? 3 * minute : 6 * minute; } - async start(esArgs = [], esEnvVars) { + async start() { let installPath; if (esFrom === 'source') { @@ -86,13 +94,7 @@ export function createLegacyEsTestCluster(options = {}) { await cluster.start(installPath, { password: config.password, - esArgs: [ - `cluster.name=${clusterName}`, - `http.port=${port}`, - 'discovery.type=single-node', - `transport.port=${transportPort}`, - ...esArgs, - ], + esArgs, esEnvVars, }); } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 3521d7ef9c66e..9b672d40961d8 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -120,6 +120,7 @@ export class DocLinksService { }, management: { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, + dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, }, }, }); diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index efd9fdd053674..f223956075e97 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -21,6 +21,7 @@ import fetchMock from 'fetch-mock/es5/client'; import { readFileSync } from 'fs'; import { join } from 'path'; +import { first } from 'rxjs/operators'; import { Fetch } from './fetch'; import { BasePath } from './base_path'; @@ -30,9 +31,11 @@ function delay(duration: number) { return new Promise(r => setTimeout(r, duration)); } +const BASE_PATH = 'http://localhost/myBase'; + describe('Fetch', () => { const fetchInstance = new Fetch({ - basePath: new BasePath('http://localhost/myBase'), + basePath: new BasePath(BASE_PATH), kibanaVersion: 'VERSION', }); afterEach(() => { @@ -40,6 +43,79 @@ describe('Fetch', () => { fetchInstance.removeAllInterceptors(); }); + describe('getRequestCount$', () => { + const getCurrentRequestCount = () => + fetchInstance + .getRequestCount$() + .pipe(first()) + .toPromise(); + + it('should increase and decrease when request receives success response', async () => { + fetchMock.get('*', 200); + + const fetchResponse = fetchInstance.fetch('/path'); + expect(await getCurrentRequestCount()).toEqual(1); + + await expect(fetchResponse).resolves.not.toThrow(); + expect(await getCurrentRequestCount()).toEqual(0); + }); + + it('should increase and decrease when request receives error response', async () => { + fetchMock.get('*', 500); + + const fetchResponse = fetchInstance.fetch('/path'); + expect(await getCurrentRequestCount()).toEqual(1); + + await expect(fetchResponse).rejects.toThrow(); + expect(await getCurrentRequestCount()).toEqual(0); + }); + + it('should increase and decrease when request fails', async () => { + fetchMock.get('*', Promise.reject('Network!')); + + const fetchResponse = fetchInstance.fetch('/path'); + expect(await getCurrentRequestCount()).toEqual(1); + + await expect(fetchResponse).rejects.toThrow(); + expect(await getCurrentRequestCount()).toEqual(0); + }); + + it('should change for multiple requests', async () => { + fetchMock.get(`${BASE_PATH}/success`, 200); + fetchMock.get(`${BASE_PATH}/fail`, 400); + fetchMock.get(`${BASE_PATH}/network-fail`, Promise.reject('Network!')); + + const requestCounts: number[] = []; + const subscription = fetchInstance + .getRequestCount$() + .subscribe(count => requestCounts.push(count)); + + const success1 = fetchInstance.fetch('/success'); + const success2 = fetchInstance.fetch('/success'); + const failure1 = fetchInstance.fetch('/fail'); + const failure2 = fetchInstance.fetch('/fail'); + const networkFailure1 = fetchInstance.fetch('/network-fail'); + const success3 = fetchInstance.fetch('/success'); + const failure3 = fetchInstance.fetch('/fail'); + const networkFailure2 = fetchInstance.fetch('/network-fail'); + + const swallowError = (p: Promise) => p.catch(() => {}); + await Promise.all([ + success1, + success2, + success3, + swallowError(failure1), + swallowError(failure2), + swallowError(failure3), + swallowError(networkFailure1), + swallowError(networkFailure2), + ]); + + expect(requestCounts).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 0]); + subscription.unsubscribe(); + }); + }); + describe('http requests', () => { it('should fail with invalid arguments', async () => { fetchMock.get('*', {}); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index b433acdb6dbb9..d88dc2e3a9037 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -19,6 +19,7 @@ import { merge } from 'lodash'; import { format } from 'url'; +import { BehaviorSubject } from 'rxjs'; import { IBasePath, @@ -43,6 +44,7 @@ const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; export class Fetch { private readonly interceptors = new Set(); + private readonly requestCount$ = new BehaviorSubject(0); constructor(private readonly params: Params) {} @@ -57,6 +59,10 @@ export class Fetch { this.interceptors.clear(); } + public getRequestCount$() { + return this.requestCount$.asObservable(); + } + public readonly delete = this.shorthand('DELETE'); public readonly get = this.shorthand('GET'); public readonly head = this.shorthand('HEAD'); @@ -76,6 +82,7 @@ export class Fetch { // a halt is called we do not resolve or reject, halting handling of the promise. return new Promise>(async (resolve, reject) => { try { + this.requestCount$.next(this.requestCount$.value + 1); const interceptedOptions = await interceptRequest( optionsWithPath, this.interceptors, @@ -98,6 +105,8 @@ export class Fetch { if (!(error instanceof HttpInterceptHaltError)) { reject(error); } + } finally { + this.requestCount$.next(this.requestCount$.value - 1); } }); }; diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index a40fcb06273dd..78220af9cc83b 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -24,6 +24,7 @@ import { loadingServiceMock } from './http_service.test.mocks'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { HttpService } from './http_service'; +import { Observable } from 'rxjs'; describe('interceptors', () => { afterEach(() => fetchMock.restore()); @@ -52,6 +53,18 @@ describe('interceptors', () => { }); }); +describe('#setup()', () => { + it('registers Fetch#getLoadingCount$() with LoadingCountSetup#addLoadingCountSource()', () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const httpService = new HttpService(); + httpService.setup({ fatalErrors, injectedMetadata }); + const loadingServiceSetup = loadingServiceMock.setup.mock.results[0].value; + // We don't verify that this Observable comes from Fetch#getLoadingCount$() to avoid complex mocking + expect(loadingServiceSetup.addLoadingCountSource).toHaveBeenCalledWith(expect.any(Observable)); + }); +}); + describe('#stop()', () => { it('calls loadingCount.stop()', () => { const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 44fc9d65565d4..98de1d919c481 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -45,6 +45,7 @@ export class HttpService implements CoreService { ); const fetchService = new Fetch({ basePath, kibanaVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); + loadingCount.addLoadingCountSource(fetchService.getRequestCount$()); this.service = { basePath, diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index b40dbdc1b6651..a91e128f62d2d 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -81,6 +81,19 @@ describe('core deprecations', () => { }); }); + describe('xsrfDeprecation', () => { + it('logs a warning if server.xsrf.whitelist is set', () => { + const { messages } = applyCoreDeprecations({ + server: { xsrf: { whitelist: ['/path'] } }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. It will be removed in 8.0 release. Instead, supply the \\"kbn-xsrf\\" header.", + ] + `); + }); + }); + describe('rewriteBasePath', () => { it('logs a warning is server.basePath is set and server.rewriteBasePath is not', () => { const { messages } = applyCoreDeprecations({ diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 4fa51dcd5a082..d91e55115d0b1 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -38,6 +38,19 @@ const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; +const xsrfDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if ( + has(settings, 'server.xsrf.whitelist') && + get(settings, 'server.xsrf.whitelist').length > 0 + ) { + log( + 'It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. ' + + 'It will be removed in 8.0 release. Instead, supply the "kbn-xsrf" header.' + ); + } + return settings; +}; + const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { log( @@ -177,4 +190,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rewriteBasePathDeprecation, cspRulesDeprecation, mapManifestServiceUrlDeprecation, + xsrfDeprecation, ]; diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 0a9541393284e..741c723ca9365 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -29,6 +29,7 @@ import { RouteMethod, KibanaResponseFactory, RouteValidationSpec, + KibanaRouteState, } from './router'; import { OnPreResponseToolkit } from './lifecycle/on_pre_response'; import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; @@ -43,6 +44,7 @@ interface RequestFixtureOptions

{ method?: RouteMethod; socket?: Socket; routeTags?: string[]; + kibanaRouteState?: KibanaRouteState; routeAuthRequired?: false; validation?: { params?: RouteValidationSpec

; @@ -62,6 +64,7 @@ function createKibanaRequestMock

({ routeTags, routeAuthRequired, validation = {}, + kibanaRouteState = { xsrfRequired: true }, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); @@ -80,7 +83,7 @@ function createKibanaRequestMock

({ search: queryString ? `?${queryString}` : queryString, }, route: { - settings: { tags: routeTags, auth: routeAuthRequired }, + settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteState }, }, raw: { req: { socket }, @@ -109,6 +112,7 @@ function createRawRequestMock(customization: DeepPartial = {}) { return merge( {}, { + app: { xsrfRequired: true } as any, headers: {}, path: '/', route: { settings: {} }, diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index a9fc80c86d878..27db79bb94d25 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -811,6 +811,7 @@ test('exposes route details of incoming request to a route handler', async () => path: '/', options: { authRequired: true, + xsrfRequired: false, tags: [], }, }); @@ -923,6 +924,7 @@ test('exposes route details of incoming request to a route handler (POST + paylo path: '/', options: { authRequired: true, + xsrfRequired: true, tags: [], body: { parse: true, // hapi populates the default diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 025ab2bf56ac2..cffdffab0d0cf 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -27,7 +27,7 @@ import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_p import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response'; -import { IRouter } from './router'; +import { IRouter, KibanaRouteState, isSafeMethod } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -147,9 +147,14 @@ export class HttpServer { for (const route of router.getRoutes()) { this.log.debug(`registering route handler for [${route.path}]`); // Hapi does not allow payload validation to be specified for 'head' or 'get' requests - const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true }; + const validate = isSafeMethod(route.method) ? undefined : { payload: true }; const { authRequired = true, tags, body = {} } = route.options; const { accepts: allow, maxBytes, output, parse } = body; + + const kibanaRouteState: KibanaRouteState = { + xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method), + }; + this.server.route({ handler: route.handler, method: route.method, @@ -157,6 +162,7 @@ export class HttpServer { options: { // Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }` auth: authRequired === true ? undefined : false, + app: kibanaRouteState, tags: tags ? Array.from(tags) : undefined, // TODO: This 'validate' section can be removed once the legacy platform is completely removed. // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index d31afe1670e41..8f4c02680f8a3 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -58,6 +58,8 @@ export { RouteValidationError, RouteValidatorFullConfig, RouteValidationResultFactory, + DestructiveRouteMethod, + SafeRouteMethod, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index f4c5f16870c7e..b5364c616f17c 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -36,6 +36,7 @@ const versionHeader = 'kbn-version'; const xsrfHeader = 'kbn-xsrf'; const nameHeader = 'kbn-name'; const whitelistedTestPath = '/xsrf/test/route/whitelisted'; +const xsrfDisabledTestPath = '/xsrf/test/route/disabled'; const kibanaName = 'my-kibana-name'; const setupDeps = { context: contextServiceMock.createSetupContract(), @@ -188,6 +189,12 @@ describe('core lifecycle handlers', () => { return res.ok({ body: 'ok' }); } ); + ((router as any)[method.toLowerCase()] as RouteRegistrar)( + { path: xsrfDisabledTestPath, validate: false, options: { xsrfRequired: false } }, + (context, req, res) => { + return res.ok({ body: 'ok' }); + } + ); }); await server.start(); @@ -235,6 +242,10 @@ describe('core lifecycle handlers', () => { it('accepts whitelisted requests without either an xsrf or version header', async () => { await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok'); }); + + it('accepts requests on a route with disabled xsrf protection', async () => { + await getSupertest(method.toLowerCase(), xsrfDisabledTestPath).expect(200, 'ok'); + }); }); }); }); diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts index 48a6973b741ba..a80e432e0d4cb 100644 --- a/src/core/server/http/lifecycle_handlers.test.ts +++ b/src/core/server/http/lifecycle_handlers.test.ts @@ -24,7 +24,7 @@ import { } from './lifecycle_handlers'; import { httpServerMock } from './http_server.mocks'; import { HttpConfig } from './http_config'; -import { KibanaRequest, RouteMethod } from './router'; +import { KibanaRequest, RouteMethod, KibanaRouteState } from './router'; const createConfig = (partial: Partial): HttpConfig => partial as HttpConfig; @@ -32,12 +32,14 @@ const forgeRequest = ({ headers = {}, path = '/', method = 'get', + kibanaRouteState, }: Partial<{ headers: Record; path: string; method: RouteMethod; + kibanaRouteState: KibanaRouteState; }>): KibanaRequest => { - return httpServerMock.createKibanaRequest({ headers, path, method }); + return httpServerMock.createKibanaRequest({ headers, path, method, kibanaRouteState }); }; describe('xsrf post-auth handler', () => { @@ -142,6 +144,29 @@ describe('xsrf post-auth handler', () => { expect(toolkit.next).toHaveBeenCalledTimes(1); expect(result).toEqual('next'); }); + + it('accepts requests if xsrf protection on a route is disabled', () => { + const config = createConfig({ + xsrf: { whitelist: [], disableProtection: false }, + }); + const handler = createXsrfPostAuthHandler(config); + const request = forgeRequest({ + method: 'post', + headers: {}, + path: '/some-path', + kibanaRouteState: { + xsrfRequired: false, + }, + }); + + toolkit.next.mockReturnValue('next' as any); + + const result = handler(request, responseFactory, toolkit); + + expect(responseFactory.badRequest).not.toHaveBeenCalled(); + expect(toolkit.next).toHaveBeenCalledTimes(1); + expect(result).toEqual('next'); + }); }); }); diff --git a/src/core/server/http/lifecycle_handlers.ts b/src/core/server/http/lifecycle_handlers.ts index ee877ee031a2b..7ef7e86326039 100644 --- a/src/core/server/http/lifecycle_handlers.ts +++ b/src/core/server/http/lifecycle_handlers.ts @@ -20,6 +20,7 @@ import { OnPostAuthHandler } from './lifecycle/on_post_auth'; import { OnPreResponseHandler } from './lifecycle/on_pre_response'; import { HttpConfig } from './http_config'; +import { isSafeMethod } from './router'; import { Env } from '../config'; import { LifecycleRegistrar } from './http_server'; @@ -31,15 +32,18 @@ export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler const { whitelist, disableProtection } = config.xsrf; return (request, response, toolkit) => { - if (disableProtection || whitelist.includes(request.route.path)) { + if ( + disableProtection || + whitelist.includes(request.route.path) || + request.route.options.xsrfRequired === false + ) { return toolkit.next(); } - const isSafeMethod = request.route.method === 'get' || request.route.method === 'head'; const hasVersionHeader = VERSION_HEADER in request.headers; const hasXsrfHeader = XSRF_HEADER in request.headers; - if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) { + if (!isSafeMethod(request.route.method) && !hasVersionHeader && !hasXsrfHeader) { return response.badRequest({ body: `Request must contain a ${XSRF_HEADER} header.` }); } diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 32663d1513f36..d254f391ca5e4 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -24,16 +24,20 @@ export { KibanaRequestEvents, KibanaRequestRoute, KibanaRequestRouteOptions, + KibanaRouteState, isRealRequest, LegacyRequest, ensureRawRequest, } from './request'; export { + DestructiveRouteMethod, + isSafeMethod, RouteMethod, RouteConfig, RouteConfigOptions, RouteContentType, RouteConfigOptionsBody, + SafeRouteMethod, validBodyOutput, } from './route'; export { HapiResponseAdapter } from './response_adapter'; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 703571ba53c0a..bb2db6367f701 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -18,18 +18,24 @@ */ import { Url } from 'url'; -import { Request } from 'hapi'; +import { Request, ApplicationState } from 'hapi'; import { Observable, fromEvent, merge } from 'rxjs'; import { shareReplay, first, takeUntil } from 'rxjs/operators'; import { deepFreeze, RecursiveReadonly } from '../../../utils'; import { Headers } from './headers'; -import { RouteMethod, RouteConfigOptions, validBodyOutput } from './route'; +import { RouteMethod, RouteConfigOptions, validBodyOutput, isSafeMethod } from './route'; import { KibanaSocket, IKibanaSocket } from './socket'; import { RouteValidator, RouteValidatorFullConfig } from './validator'; const requestSymbol = Symbol('request'); +/** + * @internal + */ +export interface KibanaRouteState extends ApplicationState { + xsrfRequired: boolean; +} /** * Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. * @public @@ -184,8 +190,10 @@ export class KibanaRequest< const options = ({ authRequired: request.route.settings.auth !== false, + // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8 + xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true, tags: request.route.settings.tags || [], - body: ['get', 'options'].includes(method) + body: isSafeMethod(method) ? undefined : { parse, diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index 4439a80b1eac7..d1458ef4ad063 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -19,11 +19,27 @@ import { RouteValidatorFullConfig } from './validator'; +export function isSafeMethod(method: RouteMethod): method is SafeRouteMethod { + return method === 'get' || method === 'options'; +} + +/** + * Set of HTTP methods changing the state of the server. + * @public + */ +export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; + +/** + * Set of HTTP methods not changing the state of the server. + * @public + */ +export type SafeRouteMethod = 'get' | 'options'; + /** * The set of common HTTP methods supported by Kibana routing. * @public */ -export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; +export type RouteMethod = SafeRouteMethod | DestructiveRouteMethod; /** * The set of valid body.output @@ -108,6 +124,15 @@ export interface RouteConfigOptions { */ authRequired?: boolean; + /** + * Defines xsrf protection requirements for a route: + * - true. Requires an incoming POST/PUT/DELETE request to contain `kbn-xsrf` header. + * - false. Disables xsrf protection. + * + * Set to true by default + */ + xsrfRequired?: Method extends 'get' ? never : boolean; + /** * Additional metadata tag strings to attach to the route. */ diff --git a/src/core/server/index.ts b/src/core/server/index.ts index de6cdb2d7acd7..8e481171116fa 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -54,6 +54,7 @@ import { SavedObjectsClientContract } from './saved_objects/types'; import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { UuidServiceSetup } from './uuid'; +import { MetricsServiceSetup } from './metrics'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; @@ -159,6 +160,8 @@ export { SessionStorageCookieOptions, SessionCookieValidationResult, SessionStorageFactory, + DestructiveRouteMethod, + SafeRouteMethod, } from './http'; export { RenderingServiceSetup, IRenderOptions } from './rendering'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; @@ -229,6 +232,9 @@ export { SavedObjectsType, SavedObjectMigrationMap, SavedObjectMigrationFn, + exportSavedObjectsToStream, + importSavedObjectsFromStream, + resolveSavedObjectsImportErrors, } from './saved_objects'; export { @@ -330,6 +336,8 @@ export interface CoreSetup { uiSettings: UiSettingsServiceSetup; /** {@link UuidServiceSetup} */ uuid: UuidServiceSetup; + /** {@link MetricsServiceSetup} */ + metrics: MetricsServiceSetup; /** * Allows plugins to get access to APIs available in start inside async handlers. * Promise will not resolve until Core and plugin dependencies have completed `start`. diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 44f77b5ad215e..f67148d720446 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -296,10 +296,14 @@ export class LegacyService implements CoreService { isTlsEnabled: setupDeps.core.http.isTlsEnabled, getServerInfo: setupDeps.core.http.getServerInfo, }, + metrics: { + getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, + }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, registerType: setupDeps.core.savedObjects.registerType, + getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, }, uiSettings: { register: setupDeps.core.uiSettings.register, diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts index a387de80212d9..6baf95894b9b4 100644 --- a/src/core/server/metrics/integration_tests/server_collector.test.ts +++ b/src/core/server/metrics/integration_tests/server_collector.test.ts @@ -17,8 +17,8 @@ * under the License. */ -import { Subject } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { take, filter } from 'rxjs/operators'; import supertest from 'supertest'; import { Server as HapiServer } from 'hapi'; import { createHttpServer } from '../../http/test_utils'; @@ -26,6 +26,8 @@ import { HttpService, IRouter } from '../../http'; import { contextServiceMock } from '../../context/context_service.mock'; import { ServerMetricsCollector } from '../collectors/server'; +const requestWaitDelay = 25; + describe('ServerMetricsCollector', () => { let server: HttpService; let collector: ServerMetricsCollector; @@ -80,11 +82,13 @@ describe('ServerMetricsCollector', () => { it('collect disconnects requests infos', async () => { const never = new Promise(resolve => undefined); + const hitSubject = new BehaviorSubject(0); router.get({ path: '/', validate: false }, async (ctx, req, res) => { return res.ok({ body: '' }); }); router.get({ path: '/disconnect', validate: false }, async (ctx, req, res) => { + hitSubject.next(hitSubject.value + 1); await never; return res.ok({ body: '' }); }); @@ -93,7 +97,13 @@ describe('ServerMetricsCollector', () => { await sendGet('/'); const discoReq1 = sendGet('/disconnect').end(); const discoReq2 = sendGet('/disconnect').end(); - await delay(20); + + await hitSubject + .pipe( + filter(count => count >= 2), + take(1) + ) + .toPromise(); let metrics = await collector.collect(); expect(metrics.requests).toEqual( @@ -104,7 +114,7 @@ describe('ServerMetricsCollector', () => { ); discoReq1.abort(); - await delay(20); + await delay(requestWaitDelay); metrics = await collector.collect(); expect(metrics.requests).toEqual( @@ -115,7 +125,7 @@ describe('ServerMetricsCollector', () => { ); discoReq2.abort(); - await delay(20); + await delay(requestWaitDelay); metrics = await collector.collect(); expect(metrics.requests).toEqual( @@ -155,28 +165,38 @@ describe('ServerMetricsCollector', () => { it('collect connection count', async () => { const waitSubject = new Subject(); + const hitSubject = new BehaviorSubject(0); router.get({ path: '/', validate: false }, async (ctx, req, res) => { + hitSubject.next(hitSubject.value + 1); await waitSubject.pipe(take(1)).toPromise(); return res.ok({ body: '' }); }); await server.start(); + const waitForHits = (hits: number) => + hitSubject + .pipe( + filter(count => count >= hits), + take(1) + ) + .toPromise(); + let metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(0); sendGet('/').end(() => null); - await delay(20); + await waitForHits(1); metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(1); sendGet('/').end(() => null); - await delay(20); + await waitForHits(2); metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(2); waitSubject.next('go'); - await delay(20); + await delay(requestWaitDelay); metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(0); }); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 037f3bbed67e0..93d8e2c632e38 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -128,6 +128,7 @@ function createCoreSetupMock() { savedObjects: savedObjectsServiceMock.createInternalSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), + metrics: metricsServiceMock.createSetupContract(), getStartServices: jest .fn, object]>, []>() .mockResolvedValue([createCoreStartMock(), {}]), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index a8a16713f69a4..b430fd28fb896 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -166,10 +166,14 @@ export function createPluginSetupContext( isTlsEnabled: deps.http.isTlsEnabled, getServerInfo: deps.http.getServerInfo, }, + metrics: { + getOpsMetrics$: deps.metrics.getOpsMetrics$, + }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, registerType: deps.savedObjects.registerType, + getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit, }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index 1088478add137..32485f461f59b 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { getSortedObjectsForExport } from './get_sorted_objects_for_export'; +import { exportSavedObjectsToStream } from './get_sorted_objects_for_export'; import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '../../../../legacy/utils/streams'; @@ -65,7 +65,7 @@ describe('getSortedObjectsForExport()', () => { per_page: 1, page: 0, }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 500, types: ['index-pattern', 'search'], @@ -151,7 +151,7 @@ describe('getSortedObjectsForExport()', () => { per_page: 1, page: 0, }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 500, types: ['index-pattern', 'search'], @@ -210,7 +210,7 @@ describe('getSortedObjectsForExport()', () => { per_page: 1, page: 0, }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 500, types: ['index-pattern', 'search'], @@ -297,7 +297,7 @@ describe('getSortedObjectsForExport()', () => { per_page: 1, page: 0, }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 500, types: ['index-pattern', 'search'], @@ -385,7 +385,7 @@ describe('getSortedObjectsForExport()', () => { page: 0, }); await expect( - getSortedObjectsForExport({ + exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit: 1, types: ['index-pattern', 'search'], @@ -425,7 +425,7 @@ describe('getSortedObjectsForExport()', () => { }, ], }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ exportSizeLimit: 10000, savedObjectsClient, types: ['index-pattern'], @@ -489,7 +489,7 @@ describe('getSortedObjectsForExport()', () => { }, ], }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ exportSizeLimit: 10000, savedObjectsClient, objects: [ @@ -587,7 +587,7 @@ describe('getSortedObjectsForExport()', () => { }, ], }); - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ exportSizeLimit: 10000, savedObjectsClient, objects: [ @@ -681,7 +681,7 @@ describe('getSortedObjectsForExport()', () => { }, ], }; - await expect(getSortedObjectsForExport(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( `"Can't export more than 1 objects"` ); }); @@ -694,7 +694,7 @@ describe('getSortedObjectsForExport()', () => { objects: undefined, }; - expect(getSortedObjectsForExport(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( `"Either \`type\` or \`objects\` are required."` ); }); @@ -707,7 +707,7 @@ describe('getSortedObjectsForExport()', () => { search: 'foo', }; - expect(getSortedObjectsForExport(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( `"Can't specify both \\"search\\" and \\"objects\\" properties when exporting"` ); }); diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index 4b4cf1146aca0..a703c9f9fbd96 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -124,7 +124,13 @@ async function fetchObjectsToExport({ } } -export async function getSortedObjectsForExport({ +/** + * Generates sorted saved object stream to be used for export. + * See the {@link SavedObjectsExportOptions | options} for more detailed information. + * + * @public + */ +export async function exportSavedObjectsToStream({ types, objects, search, diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index 7533b8e500039..37824cceb18cb 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -18,7 +18,7 @@ */ export { - getSortedObjectsForExport, + exportSavedObjectsToStream, SavedObjectsExportOptions, SavedObjectsExportResultDetails, } from './get_sorted_objects_for_export'; diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index f0719cbf4c829..b43e5063c13e1 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -19,7 +19,7 @@ import { Readable } from 'stream'; import { SavedObject } from '../types'; -import { importSavedObjects } from './import_saved_objects'; +import { importSavedObjectsFromStream } from './import_saved_objects'; import { savedObjectsClientMock } from '../../mocks'; const emptyResponse = { @@ -76,7 +76,7 @@ describe('importSavedObjects()', () => { this.push(null); }, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 1, overwrite: false, @@ -103,7 +103,7 @@ describe('importSavedObjects()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: false, @@ -186,7 +186,7 @@ describe('importSavedObjects()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: false, @@ -270,7 +270,7 @@ describe('importSavedObjects()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: true, @@ -362,7 +362,7 @@ describe('importSavedObjects()', () => { references: [], })), }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: false, @@ -460,7 +460,7 @@ describe('importSavedObjects()', () => { }, ], }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 4, overwrite: false, @@ -536,7 +536,7 @@ describe('importSavedObjects()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects, }); - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ readStream, objectLimit: 5, overwrite: false, diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index ef3b4a214c2c2..cb1d70e5c8dc4 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -26,7 +26,13 @@ import { } from './types'; import { validateReferences } from './validate_references'; -export async function importSavedObjects({ +/** + * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more + * detailed information. + * + * @public + */ +export async function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, diff --git a/src/core/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts index 95fa8aa192f3e..e268e970b94ac 100644 --- a/src/core/server/saved_objects/import/index.ts +++ b/src/core/server/saved_objects/import/index.ts @@ -17,8 +17,8 @@ * under the License. */ -export { importSavedObjects } from './import_saved_objects'; -export { resolveImportErrors } from './resolve_import_errors'; +export { importSavedObjectsFromStream } from './import_saved_objects'; +export { resolveSavedObjectsImportErrors } from './resolve_import_errors'; export { SavedObjectsImportResponse, SavedObjectsImportError, diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index c522d76f1ff04..2c6d89e0a0a47 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -19,7 +19,7 @@ import { Readable } from 'stream'; import { SavedObject } from '../types'; -import { resolveImportErrors } from './resolve_import_errors'; +import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; import { savedObjectsClientMock } from '../../mocks'; describe('resolveImportErrors()', () => { @@ -80,7 +80,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [], }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [], @@ -107,7 +107,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: savedObjects.filter(obj => obj.type === 'visualization' && obj.id === '3'), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [ @@ -168,7 +168,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects.filter(obj => obj.type === 'index-pattern' && obj.id === '1'), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [ @@ -230,7 +230,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects.filter(obj => obj.type === 'dashboard' && obj.id === '4'), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [ @@ -312,7 +312,7 @@ describe('resolveImportErrors()', () => { references: [], })), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: savedObjects.map(obj => ({ @@ -415,7 +415,7 @@ describe('resolveImportErrors()', () => { }, ], }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 2, retries: [ @@ -503,7 +503,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [], }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 5, retries: [ @@ -547,7 +547,7 @@ describe('resolveImportErrors()', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: savedObjects.filter(obj => obj.type === 'index-pattern' && obj.id === '1'), }); - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ readStream, objectLimit: 4, retries: [ diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index 6f56f283b4aec..d9ac567882573 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -27,7 +27,13 @@ import { } from './types'; import { validateReferences } from './validate_references'; -export async function resolveImportErrors({ +/** + * Resolve and return saved object import errors. + * See the {@link SavedObjectsResolveImportErrorsOptions | options} for more detailed informations. + * + * @public + */ +export async function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 44046378a7b97..067579f54edac 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -107,11 +107,17 @@ export interface SavedObjectsImportResponse { * @public */ export interface SavedObjectsImportOptions { + /** The stream of {@link SavedObject | saved objects} to import */ readStream: Readable; + /** The maximum number of object to import */ objectLimit: number; + /** if true, will override existing object if present */ overwrite: boolean; + /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ savedObjectsClient: SavedObjectsClientContract; + /** the list of allowed types to import */ supportedTypes: string[]; + /** if specified, will import in given namespace, else will import as global object */ namespace?: string; } @@ -120,10 +126,16 @@ export interface SavedObjectsImportOptions { * @public */ export interface SavedObjectsResolveImportErrorsOptions { + /** The stream of {@link SavedObject | saved objects} to resolve errors from */ readStream: Readable; + /** The maximum number of object to import */ objectLimit: number; + /** client to use to perform the import operation */ savedObjectsClient: SavedObjectsClientContract; + /** saved object import references to retry */ retries: SavedObjectsImportRetry[]; + /** the list of allowed types to import */ supportedTypes: string[]; + /** if specified, will import in given namespace */ namespace?: string; } diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 9bfe658028258..661c6cbb79e58 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -26,7 +26,7 @@ export { SavedObjectsManagement } from './management'; export * from './import'; export { - getSortedObjectsForExport, + exportSavedObjectsToStream, SavedObjectsExportOptions, SavedObjectsExportResultDetails, } from './export'; diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index ab287332d8a65..04d310681aec5 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -26,7 +26,7 @@ import { } from '../../../../legacy/utils/streams'; import { IRouter } from '../../http'; import { SavedObjectConfig } from '../saved_objects_config'; -import { getSortedObjectsForExport } from '../export'; +import { exportSavedObjectsToStream } from '../export'; export const registerExportRoute = ( router: IRouter, @@ -67,7 +67,7 @@ export const registerExportRoute = ( router.handleLegacyErrors(async (context, req, res) => { const savedObjectsClient = context.core.savedObjects.client; const { type, objects, search, excludeExportDetails, includeReferencesDeep } = req.body; - const exportStream = await getSortedObjectsForExport({ + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, types: typeof type === 'string' ? [type] : type, search, diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index e3f249dca05f7..313e84c0b301d 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -21,7 +21,7 @@ import { Readable } from 'stream'; import { extname } from 'path'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { importSavedObjects } from '../import'; +import { importSavedObjectsFromStream } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; import { createSavedObjectsStreamFromNdJson } from './utils'; @@ -65,7 +65,7 @@ export const registerImportRoute = ( return res.badRequest({ body: `Invalid file extension ${fileExtension}` }); } - const result = await importSavedObjects({ + const result = await importSavedObjectsFromStream({ supportedTypes, savedObjectsClient: context.core.savedObjects.client, readStream: createSavedObjectsStreamFromNdJson(file), diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index b52a8957176cc..a81079b6825d6 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -18,7 +18,7 @@ */ jest.mock('../../export', () => ({ - getSortedObjectsForExport: jest.fn(), + exportSavedObjectsToStream: jest.fn(), })); import * as exportMock from '../../export'; @@ -30,7 +30,7 @@ import { registerExportRoute } from '../export'; import { setupServer } from './test_utils'; type setupServerReturn = UnwrapPromise>; -const getSortedObjectsForExport = exportMock.getSortedObjectsForExport as jest.Mock; +const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock; const allowedTypes = ['index-pattern', 'search']; const config = { maxImportPayloadBytes: 10485760, @@ -76,7 +76,7 @@ describe('POST /api/saved_objects/_export', () => { ], }, ]; - getSortedObjectsForExport.mockResolvedValueOnce(createListStream(sortedObjects)); + exportSavedObjectsToStream.mockResolvedValueOnce(createListStream(sortedObjects)); const result = await supertest(httpSetup.server.listener) .post('/api/saved_objects/_export') @@ -96,7 +96,7 @@ describe('POST /api/saved_objects/_export', () => { const objects = (result.text as string).split('\n').map(row => JSON.parse(row)); expect(objects).toEqual(sortedObjects); - expect(getSortedObjectsForExport.mock.calls[0][0]).toEqual( + expect(exportSavedObjectsToStream.mock.calls[0][0]).toEqual( expect.objectContaining({ excludeExportDetails: false, exportSizeLimit: 10000, diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index efa7add7951b0..a10a19ba1d8ff 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -21,7 +21,7 @@ import { extname } from 'path'; import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { resolveImportErrors } from '../import'; +import { resolveSavedObjectsImportErrors } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; import { createSavedObjectsStreamFromNdJson } from './utils'; @@ -75,7 +75,7 @@ export const registerResolveImportErrorsRoute = ( if (fileExtension !== '.ndjson') { return res.badRequest({ body: `Invalid file extension ${fileExtension}` }); } - const result = await resolveImportErrors({ + const result = await resolveSavedObjectsImportErrors({ supportedTypes, savedObjectsClient: context.core.savedObjects.client, readStream: createSavedObjectsStreamFromNdJson(file), diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index cbdff16324536..9fe32b14e6450 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -64,8 +64,11 @@ const createSetupContractMock = () => { setClientFactoryProvider: jest.fn(), addClientWrapper: jest.fn(), registerType: jest.fn(), + getImportExportObjectLimit: jest.fn(), }; + setupContract.getImportExportObjectLimit.mockReturnValue(100); + return setupContract; }; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 62e25ad5fb458..89f7990c771c8 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -154,6 +154,11 @@ export interface SavedObjectsServiceSetup { * This API is the single entry point to register saved object types in the new platform. */ registerType: (type: SavedObjectsType) => void; + + /** + * Returns the maximum number of objects allowed for import or export operations. + */ + getImportExportObjectLimit: () => number; } /** @@ -344,6 +349,7 @@ export class SavedObjectsService } this.typeRegistry.registerType(type); }, + getImportExportObjectLimit: () => this.config!.maxImportExportSize, }; } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 495d896ad12cd..c9c672d0f8b1c 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -62,7 +62,6 @@ export interface SavedObjectsMigrationVersion { } /** - * * @public */ export interface SavedObject { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 445ed16ec7829..30695df33345a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -606,6 +606,8 @@ export interface CoreSetup { // (undocumented) http: HttpServiceSetup; // (undocumented) + metrics: MetricsServiceSetup; + // (undocumented) savedObjects: SavedObjectsServiceSetup; // (undocumented) uiSettings: UiSettingsServiceSetup; @@ -685,6 +687,9 @@ export interface DeprecationSettings { message: string; } +// @public +export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; + // @public export interface DiscoveredPlugin { readonly configPath: ConfigPath; @@ -763,6 +768,9 @@ export interface ErrorHttpResponseOptions { headers?: ResponseHeaders; } +// @public +export function exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; + // @public export interface FakeRequest { headers: Headers; @@ -891,6 +899,9 @@ export interface ImageValidation { }; } +// @public +export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; + // @public (undocumented) export interface IndexSettingsDeprecationInfo { // (undocumented) @@ -1431,6 +1442,9 @@ export type RequestHandlerContextContainer = IContextContainer = IContextProvider, TContextName>; +// @public +export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; + // @public export type ResponseError = string | Error | { message: string | Error; @@ -1459,6 +1473,7 @@ export interface RouteConfigOptions { authRequired?: boolean; body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; tags?: readonly string[]; + xsrfRequired?: Method extends 'get' ? never : boolean; } // @public @@ -1473,7 +1488,7 @@ export interface RouteConfigOptionsBody { export type RouteContentType = 'application/json' | 'application/*+json' | 'application/octet-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*'; // @public -export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; +export type RouteMethod = SafeRouteMethod | DestructiveRouteMethod; // @public export type RouteRegistrar = (route: RouteConfig, handler: RequestHandler) => void; @@ -1526,6 +1541,9 @@ export interface RouteValidatorOptions { }; } +// @public +export type SafeRouteMethod = 'get' | 'options'; + // @public (undocumented) export interface SavedObject { attributes: T; @@ -1889,17 +1907,11 @@ export interface SavedObjectsImportMissingReferencesError { // @public export interface SavedObjectsImportOptions { - // (undocumented) namespace?: string; - // (undocumented) objectLimit: number; - // (undocumented) overwrite: boolean; - // (undocumented) readStream: Readable; - // (undocumented) savedObjectsClient: SavedObjectsClientContract; - // (undocumented) supportedTypes: string[]; } @@ -2053,17 +2065,11 @@ export interface SavedObjectsRepositoryFactory { // @public export interface SavedObjectsResolveImportErrorsOptions { - // (undocumented) namespace?: string; - // (undocumented) objectLimit: number; - // (undocumented) readStream: Readable; - // (undocumented) retries: SavedObjectsImportRetry[]; - // (undocumented) savedObjectsClient: SavedObjectsClientContract; - // (undocumented) supportedTypes: string[]; } @@ -2094,6 +2100,7 @@ export class SavedObjectsSerializer { // @public export interface SavedObjectsServiceSetup { addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; + getImportExportObjectLimit: () => number; registerType: (type: SavedObjectsType) => void; setClientFactoryProvider: (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void; } diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index fb91b865097fa..35ac4e27f9c8b 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -20,6 +20,7 @@ export const storybookAliases = { apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js', canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', + codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', diff --git a/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.test.ts b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.test.ts new file mode 100644 index 0000000000000..bfba4d7f4c8da --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { + fieldFormats, + FieldFormatsGetConfigFn, + esFilters, + IndexPatternsContract, +} from '../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setIndexPatterns } from '../../../../../../plugins/data/public/services'; +import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; +import { createFiltersFromEvent, EventData } from './create_filters_from_event'; +import { mockDataServices } from '../../search/aggs/test_helpers'; + +jest.mock('ui/new_platform'); + +const mockField = { + name: 'bytes', + indexPattern: { + id: 'logstash-*', + }, + filterable: true, + format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), +}; + +describe('createFiltersFromEvent', () => { + let dataPoints: EventData[]; + + beforeEach(() => { + dataPoints = [ + { + table: { + columns: [ + { + name: 'test', + id: '1-1', + meta: { + type: 'histogram', + indexPatternId: 'logstash-*', + aggConfigParams: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, + }, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }, + column: 0, + row: 0, + value: 'test', + }, + ]; + + mockDataServices(); + setIndexPatterns(({ + ...dataPluginMock.createStartContract().indexPatterns, + get: async () => ({ + id: 'logstash-*', + fields: { + getByName: () => mockField, + filter: () => [mockField], + }, + }), + } as unknown) as IndexPatternsContract); + }); + + test('ignores event when value for rows is not provided', async () => { + dataPoints[0].table.rows[0]['1-1'] = null; + const filters = await createFiltersFromEvent(dataPoints); + + expect(filters.length).toEqual(0); + }); + + test('handles an event when aggregations type is a terms', async () => { + if (dataPoints[0].table.columns[0].meta) { + dataPoints[0].table.columns[0].meta.type = 'terms'; + } + const filters = await createFiltersFromEvent(dataPoints); + + expect(filters.length).toEqual(1); + expect(filters[0].query.match_phrase.bytes).toEqual('2048'); + }); + + test('handles an event when aggregations type is not terms', async () => { + const filters = await createFiltersFromEvent(dataPoints); + + expect(filters.length).toEqual(1); + + const [rangeFilter] = filters; + + if (esFilters.isRangeFilter(rangeFilter)) { + expect(rangeFilter.range.bytes.gte).toEqual(2048); + expect(rangeFilter.range.bytes.lt).toEqual(2078); + } + }); +}); diff --git a/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.ts similarity index 70% rename from src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js rename to src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.ts index 1037c718d0003..3713c781b0958 100644 --- a/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.ts @@ -17,21 +17,33 @@ * under the License. */ -import { esFilters } from '../../../../../../plugins/data/public'; +import { KibanaDatatable } from '../../../../../../plugins/expressions/public'; +import { esFilters, Filter } from '../../../../../../plugins/data/public'; import { deserializeAggConfig } from '../../search/expressions/utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getIndexPatterns } from '../../../../../../plugins/data/public/services'; +export interface EventData { + table: Pick; + column: number; + row: number; + value: any; +} + /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter * terms based on a specific cell in the tabified data. * - * @param {object} table - tabified table data + * @param {EventData['table']} table - tabified table data * @param {number} columnIndex - current column index * @param {number} rowIndex - current row index * @return {array} - array of terms to filter against */ -const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { +const getOtherBucketFilterTerms = ( + table: EventData['table'], + columnIndex: number, + rowIndex: number +) => { if (rowIndex === -1) { return []; } @@ -42,7 +54,7 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; }); }); - const terms = rows.map(row => row[table.columns[columnIndex].id]); + const terms: any[] = rows.map(row => row[table.columns[columnIndex].id]); return [ ...new Set( @@ -59,22 +71,27 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { * Assembles the filters needed to apply filtering against a specific cell value, while accounting * for cases like if the value is a terms agg in an `__other__` or `__missing__` bucket. * - * @param {object} table - tabified table data + * @param {EventData['table']} table - tabified table data * @param {number} columnIndex - current column index * @param {number} rowIndex - current row index * @param {string} cellValue - value of the current cell - * @return {array|string} - filter or list of filters to provide to queryFilter.addFilters() + * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() */ -const createFilter = async (table, columnIndex, rowIndex) => { - if (!table || !table.columns || !table.columns[columnIndex]) return; +const createFilter = async (table: EventData['table'], columnIndex: number, rowIndex: number) => { + if (!table || !table.columns || !table.columns[columnIndex]) { + return; + } const column = table.columns[columnIndex]; + if (!column.meta || !column.meta.indexPatternId) { + return; + } const aggConfig = deserializeAggConfig({ type: column.meta.type, - aggConfigParams: column.meta.aggConfigParams, + aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), }); - let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; + let filter: Filter[] = []; + const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -85,6 +102,10 @@ const createFilter = async (table, columnIndex, rowIndex) => { filter = aggConfig.createFilter(value); } + if (!filter) { + return; + } + if (!Array.isArray(filter)) { filter = [filter]; } @@ -92,19 +113,18 @@ const createFilter = async (table, columnIndex, rowIndex) => { return filter; }; -const createFiltersFromEvent = async event => { - const filters = []; - const dataPoints = event.data || [event]; +const createFiltersFromEvent = async (dataPoints: EventData[], negate?: boolean) => { + const filters: Filter[] = []; await Promise.all( dataPoints .filter(point => point) .map(async val => { const { table, column, row } = val; - const filter = await createFilter(table, column, row); + const filter: Filter[] = (await createFilter(table, column, row)) || []; if (filter) { filter.forEach(f => { - if (event.negate) { + if (negate) { f = esFilters.toggleFilterNegated(f); } filters.push(f); diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts index 7f1c5d78ab800..21046f8bb834f 100644 --- a/src/legacy/core_plugins/data/public/actions/select_range_action.ts +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -19,21 +19,21 @@ import { i18n } from '@kbn/i18n'; import { - Action, createAction, IncompatibleActionError, + ActionByType, } from '../../../../../plugins/ui_actions/public'; import { onBrushEvent } from './filters/brush_event'; import { FilterManager, TimefilterContract, esFilters } from '../../../../../plugins/data/public'; -export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; +export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE'; -interface ActionContext { +export interface SelectRangeActionContext { data: any; timeFieldName: string; } -async function isCompatible(context: ActionContext) { +async function isCompatible(context: SelectRangeActionContext) { try { return Boolean(await onBrushEvent(context.data)); } catch { @@ -44,17 +44,17 @@ async function isCompatible(context: ActionContext) { export function selectRangeAction( filterManager: FilterManager, timeFilter: TimefilterContract -): Action { - return createAction({ - type: SELECT_RANGE_ACTION, - id: SELECT_RANGE_ACTION, +): ActionByType { + return createAction({ + type: ACTION_SELECT_RANGE, + id: ACTION_SELECT_RANGE, getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', }); }, isCompatible, - execute: async ({ timeFieldName, data }: ActionContext) => { + execute: async ({ timeFieldName, data }: SelectRangeActionContext) => { if (!(await isCompatible({ timeFieldName, data }))) { throw new IncompatibleActionError(); } diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts index 260b401e6d658..4c69bc8262922 100644 --- a/src/legacy/core_plugins/data/public/actions/value_click_action.ts +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../plugins/kibana_react/public'; import { - Action, + ActionByType, createAction, IncompatibleActionError, } from '../../../../../plugins/ui_actions/public'; @@ -37,16 +37,18 @@ import { esFilters, } from '../../../../../plugins/data/public'; -export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; +export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; -interface ActionContext { +export interface ValueClickActionContext { data: any; timeFieldName: string; } -async function isCompatible(context: ActionContext) { +async function isCompatible(context: ValueClickActionContext) { try { - const filters: Filter[] = (await createFiltersFromEvent(context.data)) || []; + const filters: Filter[] = + (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || + []; return filters.length > 0; } catch { return false; @@ -56,22 +58,23 @@ async function isCompatible(context: ActionContext) { export function valueClickAction( filterManager: FilterManager, timeFilter: TimefilterContract -): Action { - return createAction({ - type: VALUE_CLICK_ACTION, - id: VALUE_CLICK_ACTION, +): ActionByType { + return createAction({ + type: ACTION_VALUE_CLICK, + id: ACTION_VALUE_CLICK, getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', }); }, isCompatible, - execute: async ({ timeFieldName, data }: ActionContext) => { + execute: async ({ timeFieldName, data }: ValueClickActionContext) => { if (!(await isCompatible({ timeFieldName, data }))) { throw new IncompatibleActionError(); } - const filters: Filter[] = (await createFiltersFromEvent(data)) || []; + const filters: Filter[] = + (await createFiltersFromEvent(data.data || [data], data.negate)) || []; let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 8d730d18a1755..424e5ab0bf4d5 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -44,7 +44,6 @@ export { IFieldParamType, IMetricAggType, IpRangeKey, // only used in field formatter deserialization, which will live in data - ISchemas, OptionedParamEditorProps, // only type is used externally OptionedValueProp, // only type is used externally } from './search/types'; @@ -67,7 +66,6 @@ export { convertIPRangeToString, intervalOptions, // only used in Discover isDateHistogramBucketAggConfig, - setBounds, isStringType, isType, isValidInterval, @@ -76,10 +74,9 @@ export { OptionedParamType, parentPipelineType, propFilter, - Schema, - Schemas, siblingPipelineType, termsAggFilter, + toAbsoluteDates, // search_source getRequestInspectorStats, getResponseInspectorStats, diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index e2b8ca5dda78c..18230646ab412 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -37,8 +37,16 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; import { setSearchServiceShim } from './services'; -import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; -import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; +import { + selectRangeAction, + SelectRangeActionContext, + ACTION_SELECT_RANGE, +} from './actions/select_range_action'; +import { + valueClickAction, + ACTION_VALUE_CLICK, + ValueClickActionContext, +} from './actions/value_click_action'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, @@ -76,6 +84,12 @@ export interface DataSetup { export interface DataStart { search: SearchStart; } +declare module '../../../../plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_SELECT_RANGE]: SelectRangeActionContext; + [ACTION_VALUE_CLICK]: ValueClickActionContext; + } +} /** * Data Plugin - public @@ -100,10 +114,13 @@ export class DataPlugin // This is to be deprecated once we switch to the new search service fully addSearchStrategy(defaultSearchStrategy); - uiActions.registerAction( + uiActions.attachAction( + SELECT_RANGE_TRIGGER, selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) ); - uiActions.registerAction( + + uiActions.attachAction( + VALUE_CLICK_TRIGGER, valueClickAction(data.query.filterManager, data.query.timefilter.timefilter) ); @@ -123,9 +140,6 @@ export class DataPlugin setSearchService(data.search); setOverlays(core.overlays); - uiActions.attachAction(SELECT_RANGE_TRIGGER, SELECT_RANGE_ACTION); - uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); - return { search, }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts index 7769aa29184d3..36d5451a4cd00 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_config.test.ts @@ -21,7 +21,7 @@ import { identity } from 'lodash'; import { AggConfig, IAggConfig } from './agg_config'; import { AggConfigs, CreateAggConfigParams } from './agg_configs'; -import { AggType } from './agg_types'; +import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public'; @@ -343,8 +343,7 @@ describe('AggConfig', () => { expect(typeof aggConfig.params).toBe('object'); expect(aggConfig.type).toBeInstanceOf(AggType); expect(aggConfig.type).toHaveProperty('name', 'date_histogram'); - expect(typeof aggConfig.schema).toBe('object'); - expect(aggConfig.schema).toHaveProperty('name', 'segment'); + expect(typeof aggConfig.schema).toBe('string'); const state = aggConfig.toJSON(); expect(state).toHaveProperty('id', '1'); diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts index 659bec3f702e3..bf2d2f734c989 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts @@ -20,10 +20,8 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { IAggType } from './agg_type'; -import { AggGroupNames } from './agg_groups'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; -import { Schema } from './schemas'; import { ISearchSource, FetchOptions, @@ -38,37 +36,9 @@ export interface AggConfigOptions { enabled?: boolean; id?: string; params?: Record; - schema?: string | Schema; + schema?: string; } -const unknownSchema: Schema = { - name: 'unknown', - title: 'Unknown', // only here for illustrative purposes - hideCustomLabel: true, - aggFilter: [], - min: 1, - max: 1, - params: [], - defaults: {}, - editor: false, - group: AggGroupNames.Metrics, - aggSettings: { - top_hits: { - allowStrings: true, - }, - }, -}; - -const getSchemaFromRegistry = (schemas: any, schema: string): Schema => { - let registeredSchema = schemas ? schemas.byName[schema] : null; - if (!registeredSchema) { - registeredSchema = Object.assign({}, unknownSchema); - registeredSchema.name = schema; - } - - return registeredSchema; -}; - /** * @name AggConfig * @@ -122,8 +92,8 @@ export class AggConfig { public params: any; public parent?: IAggConfigs; public brandNew?: boolean; + public schema?: string; - private __schema: Schema; private __type: IAggType; private __typeDecorations: any; private subAggs: AggConfig[] = []; @@ -141,14 +111,12 @@ export class AggConfig { this.setType(opts.type); if (opts.schema) { - this.setSchema(opts.schema); + this.schema = opts.schema; } // set the params to the values from opts, or just to the defaults this.setParams(opts.params || {}); - // @ts-ignore - this.__schema = this.__schema; // @ts-ignore this.__type = this.__type; } @@ -305,16 +273,13 @@ export class AggConfig { id: this.id, enabled: this.enabled, type: this.type && this.type.name, - schema: _.get(this, 'schema.name', this.schema), + schema: this.schema, params: outParams, }; } getAggParams() { - return [ - ...(_.has(this, 'type.params') ? this.type.params : []), - ...(_.has(this, 'schema.params') ? (this.schema as Schema).params : []), - ]; + return [...(_.has(this, 'type.params') ? this.type.params : [])]; } getRequestAggs() { @@ -397,7 +362,6 @@ export class AggConfig { fieldIsTimeField() { const indexPattern = this.getIndexPattern(); if (!indexPattern) return false; - // @ts-ignore const timeFieldName = indexPattern.timeFieldName; return timeFieldName && this.fieldName() === timeFieldName; } @@ -435,9 +399,6 @@ export class AggConfig { // clear out the previous params except for a few special ones this.setParams({ - // split row/columns is "outside" of the agg, so don't reset it - row: this.params.row, - // almost every agg has fields, so we try to persist that when type changes field: availableFields.find((field: any) => field.name === this.getField()), }); @@ -446,17 +407,4 @@ export class AggConfig { public setType(type: IAggType) { this.type = type; } - - public get schema() { - return this.__schema; - } - - public set schema(schema) { - this.__schema = schema; - } - - public setSchema(schema: string | Schema) { - this.schema = - typeof schema === 'string' ? getSchemaFromRegistry(this.aggConfigs.schemas, schema) : schema; - } } diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts index 29f16b1e4f0bf..d69376b4026d9 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.test.ts @@ -21,8 +21,6 @@ import { indexBy } from 'lodash'; import { AggConfig } from './agg_config'; import { AggConfigs } from './agg_configs'; import { AggTypesRegistryStart } from './agg_types_registry'; -import { Schemas } from './schemas'; -import { AggGroupNames } from './agg_groups'; import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public'; import { @@ -36,6 +34,7 @@ describe('AggConfigs', () => { let typesRegistry: AggTypesRegistryStart; beforeEach(() => { + mockDataServices(); indexPattern = stubIndexPatternWithFields as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); @@ -80,67 +79,6 @@ describe('AggConfigs', () => { expect(spy.mock.calls[0]).toEqual([configStates]); spy.mockRestore(); }); - - describe('defaults', () => { - const schemas = new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: 'Simple', - min: 1, - max: 2, - defaults: [ - { schema: 'metric', type: 'count' }, - { schema: 'metric', type: 'avg' }, - { schema: 'metric', type: 'sum' }, - ], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: 'Example', - min: 0, - max: 1, - defaults: [ - { schema: 'segment', type: 'terms' }, - { schema: 'segment', type: 'filters' }, - ], - }, - ]); - - it('should only set the number of defaults defined by the max', () => { - const ac = new AggConfigs(indexPattern, [], { - schemas: schemas.all, - typesRegistry, - }); - expect(ac.bySchemaName('metric')).toHaveLength(2); - }); - - it('should set the defaults defined in the schema when none exist', () => { - const ac = new AggConfigs(indexPattern, [], { - schemas: schemas.all, - typesRegistry, - }); - expect(ac.aggs).toHaveLength(3); - }); - - it('should NOT set the defaults defined in the schema when some exist', () => { - const configStates = [ - { - enabled: true, - type: 'date_histogram', - params: {}, - schema: 'segment', - }, - ]; - const ac = new AggConfigs(indexPattern, configStates, { - schemas: schemas.all, - typesRegistry, - }); - expect(ac.aggs).toHaveLength(3); - expect(ac.bySchemaName('segment')[0].type.name).toEqual('date_histogram'); - }); - }); }); describe('#createAggConfig', () => { @@ -284,19 +222,7 @@ describe('AggConfigs', () => { }); describe('#toDsl', () => { - const schemas = new Schemas([ - { - group: AggGroupNames.Buckets, - name: 'segment', - }, - { - group: AggGroupNames.Buckets, - name: 'split', - }, - ]); - beforeEach(() => { - mockDataServices(); indexPattern = stubIndexPattern as IndexPattern; indexPattern.fields.getByName = name => (name as unknown) as IndexPatternField; }); @@ -319,7 +245,6 @@ describe('AggConfigs', () => { const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, - schemas: schemas.all, }); const aggInfos = ac.aggs.map(aggConfig => { @@ -390,11 +315,10 @@ describe('AggConfigs', () => { const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, - schemas: schemas.all, }); const dsl = ac.toDsl(); const histo = ac.byName('date_histogram')[0]; - const metrics = ac.bySchemaGroup('metrics'); + const metrics = ac.bySchemaName('metrics'); expect(dsl).toHaveProperty(histo.id); expect(typeof dsl[histo.id]).toBe('object'); @@ -418,8 +342,8 @@ describe('AggConfigs', () => { const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); const topLevelDsl = ac.toDsl(true); - const buckets = ac.bySchemaGroup('buckets'); - const metrics = ac.bySchemaGroup('metrics'); + const buckets = ac.bySchemaName('buckets'); + const metrics = ac.bySchemaName('metrics'); (function checkLevel(dsl) { const bucket = buckets.shift(); diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts index ab70e66b1e138..4a48f356d3f79 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts @@ -23,7 +23,6 @@ import { Assign } from '@kbn/utility-types'; import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; -import { Schema } from './schemas'; import { AggGroupNames } from './agg_groups'; import { IndexPattern, @@ -32,8 +31,6 @@ import { TimeRange, } from '../../../../../../plugins/data/public'; -type Schemas = Record; - function removeParentAggs(obj: any) { for (const prop in obj) { if (prop === 'parentAggs') delete obj[prop]; @@ -51,7 +48,6 @@ function parseParentAggs(dslLvlCursor: any, dsl: any) { } export interface AggConfigsOptions { - schemas?: Schemas; typesRegistry: AggTypesRegistryStart; } @@ -73,7 +69,6 @@ export type IAggConfigs = AggConfigs; export class AggConfigs { public indexPattern: IndexPattern; - public schemas: any; public timeRange?: TimeRange; private readonly typesRegistry: AggTypesRegistryStart; @@ -90,37 +85,8 @@ export class AggConfigs { this.aggs = []; this.indexPattern = indexPattern; - this.schemas = opts.schemas; configStates.forEach((params: any) => this.createAggConfig(params)); - - if (this.schemas) { - this.initializeDefaultsFromSchemas(this.schemas); - } - } - - // do this wherever the schemas were passed in, & pass in state defaults instead - initializeDefaultsFromSchemas(schemas: Schemas) { - // Set the defaults for any schema which has them. If the defaults - // for some reason has more then the max only set the max number - // of defaults (not sure why a someone define more... - // but whatever). Also if a schema.name is already set then don't - // set anything. - _(schemas) - .filter((schema: Schema) => { - return Array.isArray(schema.defaults) && schema.defaults.length > 0; - }) - .each((schema: any) => { - if (!this.aggs.find((agg: AggConfig) => agg.schema && agg.schema.name === schema.name)) { - // the result here should be passable as a configState - const defaults = schema.defaults.slice(0, schema.max); - _.each(defaults, defaultState => { - const state = _.defaults({ id: AggConfig.nextId(this.aggs) }, defaultState); - this.createAggConfig(state as AggConfigOptions); - }); - } - }) - .commit(); } setTimeRange(timeRange: TimeRange) { @@ -148,7 +114,6 @@ export class AggConfigs { }; const aggConfigs = new AggConfigs(this.indexPattern, this.aggs.filter(filterAggs), { - schemas: this.schemas, typesRegistry: this.typesRegistry, }); @@ -271,23 +236,19 @@ export class AggConfigs { } byName(name: string) { - return this.aggs.filter(agg => agg.type.name === name); + return this.aggs.filter(agg => agg.type?.name === name); } byType(type: string) { - return this.aggs.filter(agg => agg.type.type === type); + return this.aggs.filter(agg => agg.type?.type === type); } byTypeName(type: string) { - return this.aggs.filter(agg => agg.type.name === type); + return this.byName(type); } bySchemaName(schema: string) { - return this.aggs.filter(agg => agg.schema && agg.schema.name === schema); - } - - bySchemaGroup(group: string) { - return this.aggs.filter(agg => agg.schema && agg.schema.group === group); + return this.aggs.filter(agg => agg.schema === schema); } getRequestAggs(): AggConfig[] { diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_types.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_types.ts index c16eb06eeb116..691598fe27e31 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_types.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_types.ts @@ -88,27 +88,3 @@ export const aggTypes = { geoTileBucketAgg, ], }; - -export { AggType } from './agg_type'; -export { AggConfig } from './agg_config'; -export { AggConfigs } from './agg_configs'; -export { FieldParamType } from './param_types'; -export { aggTypeFieldFilters } from './param_types/filter'; -export { parentPipelineAggHelper } from './metrics/lib/parent_pipeline_agg_helper'; - -// static code -export { AggParamType } from './param_types/agg'; -export { AggGroupNames, aggGroupNamesMap } from './agg_groups'; -export { intervalOptions } from './buckets/_interval_options'; // only used in Discover -export { isDateHistogramBucketAggConfig, setBounds } from './buckets/date_histogram'; -export { termsAggFilter } from './buckets/terms'; -export { isType, isStringType } from './buckets/migrate_include_exclude_format'; -export { CidrMask } from './buckets/lib/cidr_mask'; -export { convertDateRangeToString } from './buckets/date_range'; -export { convertIPRangeToString } from './buckets/ip_range'; -export { aggTypeFilters, propFilter } from './filter'; -export { OptionedParamType } from './param_types/optioned'; -export { isValidJson, isValidInterval } from './utils'; -export { BUCKET_TYPES } from './buckets/bucket_agg_types'; -export { METRIC_TYPES } from './metrics/metric_agg_types'; -export { ISchemas, Schema, Schemas } from './schemas'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts index 2b47dc384bca2..f21ca6c975809 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -26,9 +26,6 @@ import { dateHistogramBucketAgg, IBucketDateHistogramAggConfig } from '../date_h import { BUCKET_TYPES } from '../bucket_agg_types'; import { RangeFilter } from '../../../../../../../../plugins/data/public'; -// TODO: remove this once time buckets is migrated -jest.mock('ui/new_platform'); - describe('AggConfig Filters', () => { describe('date_histogram', () => { beforeEach(() => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts index a5368135728d4..8c8911bda99a5 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -21,8 +21,7 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -// TODO need to move TimeBuckets -import { TimeBuckets } from 'ui/time_buckets'; +import { TimeBuckets } from './lib/time_buckets'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterDateHistogram } from './create_filter/date_histogram'; @@ -31,34 +30,42 @@ import { dateHistogramInterval } from '../../../../common'; import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; -import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getQueryService, getUiSettings } from '../../../../../../../plugins/data/public/services'; +import { + fieldFormats, + KBN_FIELD_TYPES, + TimefilterContract, +} from '../../../../../../../plugins/data/public'; +import { + getFieldFormats, + getQueryService, + getUiSettings, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../plugins/data/public/services'; const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); -const getInterval = (agg: IBucketAggConfig): string => _.get(agg, ['params', 'interval']); - -export const setBounds = (agg: IBucketDateHistogramAggConfig, force?: boolean) => { - const { timefilter } = getQueryService().timefilter; - if (agg.buckets._alreadySet && !force) return; - agg.buckets._alreadySet = true; +const updateTimeBuckets = ( + agg: IBucketDateHistogramAggConfig, + timefilter: TimefilterContract, + customBuckets?: IBucketDateHistogramAggConfig['buckets'] +) => { const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; - agg.buckets.setBounds(agg.fieldIsTimeField() && bounds); + const buckets = customBuckets || agg.buckets; + buckets.setBounds(agg.fieldIsTimeField() && bounds); + buckets.setInterval(agg.params.interval); }; -// will be replaced by src/legacy/ui/public/time_buckets/time_buckets.js -interface TimeBuckets { - _alreadySet?: boolean; +// TODO: Need to incorporate these properly into TimeBuckets +interface ITimeBuckets { setBounds: Function; - getScaledDateFormatter: Function; + getScaledDateFormat: TimeBuckets['getScaledDateFormat']; setInterval: Function; getInterval: Function; } export interface IBucketDateHistogramAggConfig extends IBucketAggConfig { - buckets: TimeBuckets; + buckets: ITimeBuckets; } export function isDateHistogramBucketAggConfig(agg: any): agg is IBucketDateHistogramAggConfig { @@ -91,16 +98,18 @@ export const dateHistogramBucketAgg = new BucketAggType getUiSettings().get(key) + ); }, params: [ { @@ -122,8 +142,6 @@ export const dateHistogramBucketAgg = new BucketAggType string -) => { +export function convertDateRangeToString({ from, to }: DateRangeKey, format: (val: any) => string) { if (!from) { return 'Before ' + format(to); } else if (!to) { @@ -33,4 +30,4 @@ export const convertDateRangeToString = ( } else { return format(from) + ' to ' + format(to); } -}; +} diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/date_utils.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/date_utils.ts new file mode 100644 index 0000000000000..c333a1dbe8524 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/date_utils.ts @@ -0,0 +1,56 @@ +/* + * 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 dateMath from '@elastic/datemath'; +import { TimeBuckets } from './time_buckets'; +import { TimeRange } from '../../../../../../../../plugins/data/public'; +import { IUiSettingsClient } from '../../../../../../../../core/public'; + +export function toAbsoluteDates(range: TimeRange) { + const fromDate = dateMath.parse(range.from); + const toDate = dateMath.parse(range.to, { roundUp: true }); + + if (!fromDate || !toDate) { + return; + } + + return { + from: fromDate.toDate(), + to: toDate.toDate(), + }; +} + +export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { + return function calculateAutoTimeExpression(range: TimeRange) { + const dates = toAbsoluteDates(range); + if (!dates) { + return; + } + + const buckets = new TimeBuckets({ uiSettings }); + + buckets.setInterval('auto'); + buckets.setBounds({ + min: dates.from, + max: dates.to, + }); + + return buckets.getInterval().expression; + }; +} diff --git a/src/legacy/ui/public/time_buckets/calc_auto_interval.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_auto_interval.test.ts similarity index 100% rename from src/legacy/ui/public/time_buckets/calc_auto_interval.test.ts rename to src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_auto_interval.test.ts diff --git a/src/legacy/ui/public/time_buckets/calc_auto_interval.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts similarity index 100% rename from src/legacy/ui/public/time_buckets/calc_auto_interval.ts rename to src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts diff --git a/src/legacy/ui/public/time_buckets/calc_es_interval.js b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts similarity index 81% rename from src/legacy/ui/public/time_buckets/calc_es_interval.js rename to src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts index abfaa50c1505f..3e7d315a0a42a 100644 --- a/src/legacy/ui/public/time_buckets/calc_es_interval.js +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts @@ -17,13 +17,20 @@ * under the License. */ -import dateMath from '@elastic/datemath'; +import moment from 'moment'; +import dateMath, { Unit } from '@elastic/datemath'; -import { parseEsInterval } from '../../../core_plugins/data/public'; +import { parseEsInterval } from '../../../../../../common'; const unitsDesc = dateMath.unitsDesc; const largeMax = unitsDesc.indexOf('M'); +export interface EsInterval { + expression: string; + unit: Unit; + value: number; +} + /** * Convert a moment.duration into an es * compatible expression, and provide @@ -32,7 +39,7 @@ const largeMax = unitsDesc.indexOf('M'); * @param {moment.duration} duration * @return {object} */ -export function convertDurationToNormalizedEsInterval(duration) { +export function convertDurationToNormalizedEsInterval(duration: moment.Duration): EsInterval { for (let i = 0; i < unitsDesc.length; i++) { const unit = unitsDesc[i]; const val = duration.as(unit); @@ -47,7 +54,7 @@ export function convertDurationToNormalizedEsInterval(duration) { return { value: val, - unit: unit, + unit, expression: val + unit, }; } @@ -61,7 +68,7 @@ export function convertDurationToNormalizedEsInterval(duration) { }; } -export function convertIntervalToEsInterval(interval) { +export function convertIntervalToEsInterval(interval: string): EsInterval { const { value, unit } = parseEsInterval(interval); return { value, diff --git a/src/legacy/ui/public/time_buckets/index.js b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/index.ts similarity index 100% rename from src/legacy/ui/public/time_buckets/index.js rename to src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/index.ts diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts new file mode 100644 index 0000000000000..9f43181932d7e --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -0,0 +1,438 @@ +/* + * 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 _ from 'lodash'; +import moment from 'moment'; + +import { IUiSettingsClient } from '../../../../../../../../../core/public'; +import { parseInterval } from '../../../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; +import { + convertDurationToNormalizedEsInterval, + convertIntervalToEsInterval, + EsInterval, +} from './calc_es_interval'; + +interface Bounds { + min: Date | number | null; + max: Date | number | null; +} + +interface TimeBucketsInterval extends moment.Duration { + // TODO double-check whether all of these are needed + description: string; + esValue: EsInterval['value']; + esUnit: EsInterval['unit']; + expression: EsInterval['expression']; + overflow: moment.Duration | boolean; + preScaled?: moment.Duration; + scale?: number; + scaled?: boolean; +} + +function isObject(o: any): o is Record { + return _.isObject(o); +} + +function isString(s: any): s is string { + return _.isString(s); +} + +function isValidMoment(m: any): boolean { + return m && 'isValid' in m && m.isValid(); +} + +interface TimeBucketsConfig { + uiSettings: IUiSettingsClient; +} + +/** + * Helper class for wrapping the concept of an "Interval", + * which describes a timespan that will separate moments. + * + * @param {state} object - one of "" + * @param {[type]} display [description] + */ +export class TimeBuckets { + private getConfig: (key: string) => any; + + private _lb: Bounds['min'] = null; + private _ub: Bounds['max'] = null; + private _originalInterval: string | null = null; + private _i?: moment.Duration | 'auto'; + + // because other parts of Kibana arbitrarily add properties + [key: string]: any; + + static __cached__(self: TimeBuckets) { + let cache: any = {}; + const sameMoment = same(moment.isMoment); + const sameDuration = same(moment.isDuration); + + const desc: Record = { + __cached__: { + value: self, + }, + }; + + const breakers: Record = { + setBounds: 'bounds', + clearBounds: 'bounds', + setInterval: 'interval', + }; + + const resources: Record = { + bounds: { + setup() { + return [self._lb, self._ub]; + }, + changes(prev: any) { + return !sameMoment(prev[0], self._lb) || !sameMoment(prev[1], self._ub); + }, + }, + interval: { + setup() { + return self._i; + }, + changes(prev: any) { + return !sameDuration(prev, self._i); + }, + }, + }; + + function cachedGetter(prop: string) { + return { + value: (...rest: any) => { + if (cache.hasOwnProperty(prop)) { + return cache[prop]; + } + + return (cache[prop] = self[prop](...rest)); + }, + }; + } + + function cacheBreaker(prop: string) { + const resource = resources[breakers[prop]]; + const setup = resource.setup; + const changes = resource.changes; + const fn = self[prop]; + + return { + value: (...args: any) => { + const prev = setup.call(self); + const ret = fn.apply(self, ...args); + + if (changes.call(self, prev)) { + cache = {}; + } + + return ret; + }, + }; + } + + function same(checkType: any) { + return function(a: any, b: any) { + if (a === b) return true; + if (checkType(a) === checkType(b)) return +a === +b; + return false; + }; + } + + _.forOwn(TimeBuckets.prototype, (fn, prop) => { + if (!prop || prop[0] === '_') return; + + if (breakers.hasOwnProperty(prop)) { + desc[prop] = cacheBreaker(prop); + } else { + desc[prop] = cachedGetter(prop); + } + }); + + return Object.create(self, desc); + } + + constructor({ uiSettings }: TimeBucketsConfig) { + this.getConfig = (key: string) => uiSettings.get(key); + return TimeBuckets.__cached__(this); + } + + /** + * Get a moment duration object representing + * the distance between the bounds, if the bounds + * are set. + * + * @return {moment.duration|undefined} + */ + private getDuration(): moment.Duration | undefined { + if (this._ub === null || this._lb === null || !this.hasBounds()) { + return; + } + const difference = (this._ub as number) - (this._lb as number); + return moment.duration(difference, 'ms'); + } + + /** + * Set the bounds that these buckets are expected to cover. + * This is required to support interval "auto" as well + * as interval scaling. + * + * @param {object} input - an object with properties min and max, + * representing the edges for the time span + * we should cover + * + * @returns {undefined} + */ + setBounds(input?: Bounds | Bounds[]) { + if (!input) return this.clearBounds(); + + let bounds; + if (_.isPlainObject(input) && !Array.isArray(input)) { + // accept the response from timefilter.getActiveBounds() + bounds = [input.min, input.max]; + } else { + bounds = Array.isArray(input) ? input : []; + } + + const moments = _(bounds) + .map(_.ary(moment, 1)) + .sortBy(Number); + + const valid = moments.size() === 2 && moments.every(isValidMoment); + if (!valid) { + this.clearBounds(); + throw new Error('invalid bounds set: ' + input); + } + + this._lb = moments.shift() as any; + this._ub = moments.pop() as any; + + const duration = this.getDuration(); + if (!duration || duration.asSeconds() < 0) { + throw new TypeError('Intervals must be positive'); + } + } + + /** + * Clear the stored bounds + * + * @return {undefined} + */ + clearBounds() { + this._lb = this._ub = null; + } + + /** + * Check to see if we have received bounds yet + * + * @return {Boolean} + */ + hasBounds(): boolean { + return isValidMoment(this._ub) && isValidMoment(this._lb); + } + + /** + * Return the current bounds, if we have any. + * + * THIS DOES NOT CLONE THE BOUNDS, so editing them + * may have unexpected side-effects. Always + * call bounds.min.clone() before editing + * + * @return {object|undefined} - If bounds are not defined, this + * returns undefined, else it returns the bounds + * for these buckets. This object has two props, + * min and max. Each property will be a moment() + * object + * + */ + getBounds(): Bounds | undefined { + if (!this.hasBounds()) return; + return { + min: this._lb, + max: this._ub, + }; + } + + /** + * Update the interval at which buckets should be + * generated. + * + * Input can be one of the following: + * - Any object from src/legacy/ui/agg_types.js + * - "auto" + * - Pass a valid moment unit + * - a moment.duration object. + * + * @param {object|string|moment.duration} input - see desc + */ + setInterval(input: null | string | Record | moment.Duration) { + let interval = input; + + // selection object -> val + if (isObject(input) && !moment.isDuration(input)) { + interval = input.val; + } + + if (!interval || interval === 'auto') { + this._i = 'auto'; + return; + } + + if (isString(interval)) { + input = interval; + + // Preserve the original units because they're lost when the interval is converted to a + // moment duration object. + this._originalInterval = input; + + interval = parseInterval(interval); + if (interval === null || +interval === 0) { + interval = null; + } + } + + // if the value wasn't converted to a duration, and isn't + // already a duration, we have a problem + if (!moment.isDuration(interval)) { + throw new TypeError('"' + input + '" is not a valid interval.'); + } + + this._i = interval; + } + + /** + * Get the interval for the buckets. If the + * number of buckets created by the interval set + * is larger than config:histogram:maxBars then the + * interval will be scaled up. If the number of buckets + * created is less than one, the interval is scaled back. + * + * The interval object returned is a moment.duration + * object that has been decorated with the following + * properties. + * + * interval.description: a text description of the interval. + * designed to be used list "field per {{ desc }}". + * - "minute" + * - "10 days" + * - "3 years" + * + * interval.expression: the elasticsearch expression that creates this + * interval. If the interval does not properly form an elasticsearch + * expression it will be forced into one. + * + * interval.scaled: the interval was adjusted to + * accommodate the maxBars setting. + * + * interval.scale: the number that y-values should be + * multiplied by + */ + getInterval(useNormalizedEsInterval = true): TimeBucketsInterval { + const duration = this.getDuration(); + + // either pull the interval from state or calculate the auto-interval + const readInterval = () => { + const interval = this._i; + if (moment.isDuration(interval)) return interval; + return calcAutoIntervalNear(this.getConfig('histogram:barTarget'), Number(duration)); + }; + + const parsedInterval = readInterval(); + + // check to see if the interval should be scaled, and scale it if so + const maybeScaleInterval = (interval: moment.Duration) => { + if (!this.hasBounds() || !duration) { + return interval; + } + + const maxLength: number = this.getConfig('histogram:maxBars'); + const approxLen = Number(duration) / Number(interval); + + let scaled; + + if (approxLen > maxLength) { + scaled = calcAutoIntervalLessThan(maxLength, Number(duration)); + } else { + return interval; + } + + if (+scaled === +interval) return interval; + + interval = decorateInterval(interval); + return Object.assign(scaled, { + preScaled: interval, + scale: Number(interval) / Number(scaled), + scaled: true, + }); + }; + + // append some TimeBuckets specific props to the interval + const decorateInterval = (interval: moment.Duration): TimeBucketsInterval => { + const esInterval = useNormalizedEsInterval + ? convertDurationToNormalizedEsInterval(interval) + : convertIntervalToEsInterval(String(this._originalInterval)); + const prettyUnits = moment.normalizeUnits(esInterval.unit); + + return Object.assign(interval, { + description: + esInterval.value === 1 ? prettyUnits : esInterval.value + ' ' + prettyUnits + 's', + esValue: esInterval.value, + esUnit: esInterval.unit, + expression: esInterval.expression, + overflow: + Number(duration) > Number(interval) + ? moment.duration(Number(interval) - Number(duration)) + : false, + }); + }; + + if (useNormalizedEsInterval) { + return decorateInterval(maybeScaleInterval(parsedInterval)); + } else { + return decorateInterval(parsedInterval); + } + } + + /** + * Get a date format string that will represent dates that + * progress at our interval. + * + * Since our interval can be as small as 1ms, the default + * date format is usually way too much. with `dateFormat:scaled` + * users can modify how dates are formatted within series + * produced by TimeBuckets + * + * @return {string} + */ + getScaledDateFormat() { + const interval = this.getInterval(); + const rules = this.getConfig('dateFormat:scaled'); + + for (let i = rules.length - 1; i >= 0; i--) { + const rule = rules[i]; + if (!rule[0] || (interval && interval >= moment.duration(rule[0]))) { + return rule[1]; + } + } + + return this.getConfig('dateFormat'); + } +} diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts index 8fd95c86d8476..b387e9b7d306a 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts @@ -40,8 +40,6 @@ import { mergeOtherBucketAggResponse, updateMissingBucket, } from './_terms_other_bucket_helper'; -import { Schemas } from '../schemas'; -import { AggGroupNames } from '../agg_groups'; export const termsAggFilter = [ '!top_hits', @@ -58,17 +56,6 @@ export const termsAggFilter = [ '!sum_bucket', ]; -const [orderAggSchema] = new Schemas([ - { - group: AggGroupNames.None, - name: 'orderAgg', - // This string is never visible to the user so it doesn't need to be translated - title: 'Order Agg', - hideCustomLabel: true, - aggFilter: termsAggFilter, - }, -]).all; - const termsTitle = i18n.translate('data.search.aggs.buckets.termsTitle', { defaultMessage: 'Terms', }); @@ -158,10 +145,11 @@ export const termsBucketAgg = new BucketAggType({ { name: 'orderAgg', type: 'agg', + allowedAggs: termsAggFilter, default: null, makeAgg(termsAgg, state) { state = state || {}; - state.schema = orderAggSchema; + state.schema = 'orderAgg'; const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false, }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts index 0de1c31d02f96..90c29675c0db2 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.test.ts @@ -32,7 +32,7 @@ describe('AggTypeFilters', () => { it('should filter nothing without registered filters', async () => { const aggTypes = [{ name: 'count' }, { name: 'sum' }] as IAggType[]; - const filtered = registry.filter(aggTypes, indexPattern, aggConfig); + const filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); expect(filtered).toEqual(aggTypes); }); @@ -40,23 +40,23 @@ describe('AggTypeFilters', () => { const aggTypes = [{ name: 'count' }, { name: 'sum' }] as IAggType[]; const filter = jest.fn(); registry.addFilter(filter); - registry.filter(aggTypes, indexPattern, aggConfig); - expect(filter).toHaveBeenCalledWith(aggTypes[0], indexPattern, aggConfig); - expect(filter).toHaveBeenCalledWith(aggTypes[1], indexPattern, aggConfig); + registry.filter(aggTypes, indexPattern, aggConfig, []); + expect(filter).toHaveBeenCalledWith(aggTypes[0], indexPattern, aggConfig, []); + expect(filter).toHaveBeenCalledWith(aggTypes[1], indexPattern, aggConfig, []); }); it('should allow registered filters to filter out aggTypes', async () => { const aggTypes = [{ name: 'count' }, { name: 'sum' }, { name: 'avg' }] as IAggType[]; - let filtered = registry.filter(aggTypes, indexPattern, aggConfig); + let filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); expect(filtered).toEqual(aggTypes); registry.addFilter(() => true); registry.addFilter(aggType => aggType.name !== 'count'); - filtered = registry.filter(aggTypes, indexPattern, aggConfig); + filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); expect(filtered).toEqual([aggTypes[1], aggTypes[2]]); registry.addFilter(aggType => aggType.name !== 'avg'); - filtered = registry.filter(aggTypes, indexPattern, aggConfig); + filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); expect(filtered).toEqual([aggTypes[1]]); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts index 13a4cc0856b09..8da547e592af9 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/filter/agg_type_filters.ts @@ -23,7 +23,8 @@ import { IAggConfig, IAggType } from '../types'; type AggTypeFilter = ( aggType: IAggType, indexPattern: IndexPattern, - aggConfig: IAggConfig + aggConfig: IAggConfig, + aggFilter: string[] ) => boolean; /** @@ -48,12 +49,20 @@ class AggTypeFilters { * @param aggTypes A list of aggTypes that will be filtered down by this registry. * @param indexPattern The indexPattern for which this list should be filtered down. * @param aggConfig The aggConfig for which the returning list will be used. + * @param schema * @return A filtered list of the passed aggTypes. */ - public filter(aggTypes: IAggType[], indexPattern: IndexPattern, aggConfig: IAggConfig) { + public filter( + aggTypes: IAggType[], + indexPattern: IndexPattern, + aggConfig: IAggConfig, + aggFilter: string[] + ) { const allFilters = Array.from(this.filters); const allowedAggTypes = aggTypes.filter(aggType => { - const isAggTypeAllowed = allFilters.every(filter => filter(aggType, indexPattern, aggConfig)); + const isAggTypeAllowed = allFilters.every(filter => + filter(aggType, indexPattern, aggConfig, aggFilter) + ); return isAggTypeAllowed; }); return allowedAggTypes; diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.ts b/src/legacy/core_plugins/data/public/search/aggs/index.ts index f6914c36f6c05..8d6fbeacd606a 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.ts @@ -27,6 +27,7 @@ export { aggTypes } from './agg_types'; export { AggConfig } from './agg_config'; export { AggConfigs } from './agg_configs'; export { FieldParamType } from './param_types'; +export { getCalculateAutoTimeExpression } from './buckets/lib/date_utils'; export { MetricAggType } from './metrics/metric_agg_type'; export { AggTypeFilters } from './filter'; export { aggTypeFieldFilters, AggTypeFieldFilters } from './param_types/filter'; @@ -43,18 +44,18 @@ export { export { AggParamType } from './param_types/agg'; export { AggGroupNames, aggGroupNamesMap } from './agg_groups'; export { intervalOptions } from './buckets/_interval_options'; // only used in Discover -export { isDateHistogramBucketAggConfig, setBounds } from './buckets/date_histogram'; +export { isDateHistogramBucketAggConfig } from './buckets/date_histogram'; export { termsAggFilter } from './buckets/terms'; export { isType, isStringType } from './buckets/migrate_include_exclude_format'; export { CidrMask } from './buckets/lib/cidr_mask'; export { convertDateRangeToString } from './buckets/date_range'; +export { toAbsoluteDates } from './buckets/lib/date_utils'; export { convertIPRangeToString } from './buckets/ip_range'; export { aggTypeFilters, propFilter } from './filter'; export { OptionedParamType } from './param_types/optioned'; export { isValidJson, isValidInterval } from './utils'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; -export { ISchemas, Schema, Schemas } from './schemas'; // types export { CreateAggConfigParams, IAggConfig, IAggConfigs } from './types'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 88549ee3019ee..df4cbaf49c8b3 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -23,7 +23,7 @@ import { noop, identity } from 'lodash'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; -import { Schemas } from '../../schemas'; + import { fieldFormats } from '../../../../../../../../plugins/data/public'; const metricAggFilter = [ @@ -36,20 +36,6 @@ const metricAggFilter = [ '!geo_centroid', ]; -const metricAggTitle = i18n.translate('data.search.aggs.metrics.metricAggTitle', { - defaultMessage: 'Metric agg', -}); - -const [metricAggSchema] = new Schemas([ - { - group: 'none', - name: 'metricAgg', - title: metricAggTitle, - hideCustomLabel: true, - aggFilter: metricAggFilter, - }, -]).all; - const parentPipelineType = i18n.translate( 'data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle', { @@ -69,9 +55,9 @@ const parentPipelineAggHelper = { { name: 'customMetric', type: 'agg', + allowedAggs: metricAggFilter, makeAgg(termsAgg, state: any) { state = state || { type: 'count' }; - state.schema = metricAggSchema; const metricAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index 05e009cc9da30..33d6d72540868 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n'; import { siblingPipelineAggWriter } from './sibling_pipeline_agg_writer'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; -import { Schemas } from '../../schemas'; import { fieldFormats } from '../../../../../../../../plugins/data/public'; const metricAggFilter: string[] = [ @@ -44,28 +43,6 @@ const metricAggFilter: string[] = [ ]; const bucketAggFilter: string[] = []; -const [metricAggSchema] = new Schemas([ - { - group: 'none', - name: 'metricAgg', - title: i18n.translate('data.search.aggs.metrics.metricAggTitle', { - defaultMessage: 'Metric agg', - }), - aggFilter: metricAggFilter, - }, -]).all; - -const [bucketAggSchema] = new Schemas([ - { - group: 'none', - title: i18n.translate('data.search.aggs.metrics.bucketAggTitle', { - defaultMessage: 'Bucket agg', - }), - name: 'bucketAgg', - aggFilter: bucketAggFilter, - }, -]).all; - const siblingPipelineType = i18n.translate( 'data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle', { @@ -80,10 +57,10 @@ const siblingPipelineAggHelper = { { name: 'customBucket', type: 'agg', + allowedAggs: bucketAggFilter, default: null, makeAgg(agg: IMetricAggConfig, state: any) { state = state || { type: 'date_histogram' }; - state.schema = bucketAggSchema; const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-bucket'; @@ -97,10 +74,10 @@ const siblingPipelineAggHelper = { { name: 'customMetric', type: 'agg', + allowedAggs: metricAggFilter, default: null, makeAgg(agg: IMetricAggConfig, state: any) { state = state || { type: 'count' }; - state.schema = metricAggSchema; const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-metric'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts index 952dcc96de833..82b042a1e3378 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts @@ -21,11 +21,11 @@ import { i18n } from '@kbn/i18n'; import { AggType, AggTypeConfig } from '../agg_type'; import { AggParamType } from '../param_types/agg'; import { AggConfig } from '../agg_config'; -import { FilterFieldTypes } from '../param_types/field'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getFieldFormats } from '../../../../../../../plugins/data/public/services'; +import { FieldTypes } from '../param_types'; export interface IMetricAggConfig extends AggConfig { type: InstanceType; @@ -33,7 +33,7 @@ export interface IMetricAggConfig extends AggConfig { export interface MetricAggParam extends AggParamType { - filterFieldTypes?: FilterFieldTypes; + filterFieldTypes?: FieldTypes; onlyAggregatable?: boolean; } diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts index 58b4ee530a8c2..02e63f653f94f 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts @@ -25,15 +25,6 @@ import { AggConfigs } from '../agg_configs'; import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; -jest.mock('../schemas', () => { - class MockedSchemas { - all = [{}]; - } - return { - Schemas: jest.fn().mockImplementation(() => new MockedSchemas()), - }; -}); - describe('parent pipeline aggs', function() { beforeEach(() => { mockDataServices(); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts index d3456bacceb6a..8389ed8262ce5 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts @@ -26,15 +26,6 @@ import { AggConfigs } from '../agg_configs'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; -jest.mock('../schemas', () => { - class MockedSchemas { - all = [{}]; - } - return { - Schemas: jest.fn().mockImplementation(() => new MockedSchemas()), - }; -}); - describe('sibling pipeline aggs', () => { beforeEach(() => { mockDataServices(); diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.ts index 3112d882bb87e..c850eb4ff2220 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.ts @@ -63,10 +63,7 @@ export const topHitMetricAgg = new MetricAggType({ name: 'field', type: 'field', onlyAggregatable: false, - filterFieldTypes: (aggConfig: IMetricAggConfig) => - _.get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false) - ? '*' - : KBN_FIELD_TYPES.NUMBER, + filterFieldTypes: '*', write(agg, output) { const field = agg.getParam('field'); output.params = {}; @@ -133,7 +130,7 @@ export const topHitMetricAgg = new MetricAggType({ defaultMessage: 'Concatenate', }), isCompatible(aggConfig: IMetricAggConfig) { - return _.get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false); + return _.get(aggConfig.params, 'field.filterFieldTypes', '*') === '*'; }, disabled: true, value: 'concat', diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts index d31abe64491d0..e5b53020c3159 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/agg.ts @@ -24,10 +24,15 @@ export class AggParamType extends Ba TAggConfig > { makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + allowedAggs: string[] = []; constructor(config: Record) { super(config); + if (config.allowedAggs) { + this.allowedAggs = config.allowedAggs; + } + if (!config.write) { this.write = (aggConfig: TAggConfig, output: Record) => { if (aggConfig.params[this.name] && aggConfig.params[this.name].length) { diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts index 7338c41f920d7..18b666f454664 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts @@ -17,13 +17,10 @@ * under the License. */ -import { get } from 'lodash'; import { BaseParamType } from './base'; import { FieldParamType } from './field'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { IAggConfig } from '../agg_config'; -import { IMetricAggConfig } from '../metrics/metric_agg_type'; -import { Schema } from '../schemas'; describe('Field', () => { const indexPattern = { @@ -105,43 +102,5 @@ describe('Field', () => { expect(fields.length).toBe(2); }); - - it('should return only numeric fields if filterFieldTypes was specified as a function', () => { - const aggParam = new FieldParamType({ - name: 'field', - type: 'field', - filterFieldTypes: (aggConfig: IMetricAggConfig) => - get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false) - ? '*' - : KBN_FIELD_TYPES.NUMBER, - }); - const fields = aggParam.getAvailableFields(agg); - - expect(fields.length).toBe(1); - expect(fields[0].type).toBe(KBN_FIELD_TYPES.NUMBER); - }); - - it('should return all fields if filterFieldTypes was specified as a function and aggSettings allow string type fields', () => { - const aggParam = new FieldParamType({ - name: 'field', - type: 'field', - filterFieldTypes: (aggConfig: IMetricAggConfig) => - get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false) - ? '*' - : KBN_FIELD_TYPES.NUMBER, - }); - - agg.schema = { - aggSettings: { - top_hits: { - allowStrings: true, - }, - }, - } as Schema; - - const fields = aggParam.getAvailableFields(agg); - - expect(fields.length).toBe(2); - }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts index bb5707cbb482e..6882b8aa39e7e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts @@ -18,12 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -import { isFunction } from 'lodash'; import { IAggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; import { propFilter } from '../filter'; -import { IMetricAggConfig } from '../metrics/metric_agg_type'; import { IndexPatternField, indexPatterns, @@ -34,15 +32,14 @@ import { getNotifications } from '../../../../../../../plugins/data/public/servi const filterByType = propFilter('type'); -type FieldTypes = KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; -export type FilterFieldTypes = ((aggConfig: IMetricAggConfig) => FieldTypes) | FieldTypes; +export type FieldTypes = KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; // TODO need to make a more explicit interface for this export type IFieldParamType = FieldParamType; export class FieldParamType extends BaseParamType { required = true; scriptable = true; - filterFieldTypes: FilterFieldTypes; + filterFieldTypes: FieldTypes; onlyAggregatable: boolean; constructor(config: Record) { @@ -127,12 +124,6 @@ export class FieldParamType extends BaseParamType { return false; } - if (isFunction(filterFieldTypes)) { - const filter = filterFieldTypes(aggConfig as IMetricAggConfig); - - return filterByType([field], filter).length !== 0; - } - return filterByType([field], filterFieldTypes).length !== 0; }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/types.ts b/src/legacy/core_plugins/data/public/search/aggs/types.ts index 5d02f426b5896..069a933fd994a 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/types.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/types.ts @@ -26,4 +26,3 @@ export { IMetricAggType } from './metrics/metric_agg_type'; export { DateRangeKey } from './buckets/date_range'; export { IpRangeKey } from './buckets/ip_range'; export { OptionedValueProp, OptionedParamEditorProps } from './param_types/optioned'; -export { ISchemas } from './schemas'; diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index e85e9deff6ddf..bd05fa21bfd5d 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -20,7 +20,7 @@ import { set } from 'lodash'; // @ts-ignore import { FormattedData } from '../../../../../../plugins/inspector/public'; -// @ts-ignore + import { createFilter } from './create_filter'; import { TabbedTable } from '../tabify'; @@ -66,7 +66,10 @@ export async function buildTabularInspectorData( row => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw ); const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw); - queryFilter.addFilters(filter); + + if (filter) { + queryFilter.addFilters(filter); + } }), filterOut: isCellContentFilterable && @@ -75,14 +78,17 @@ export async function buildTabularInspectorData( row => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw ); const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw); - const notOther = value.raw !== '__other__'; - const notMissing = value.raw !== '__missing__'; - if (Array.isArray(filter)) { - filter.forEach(f => set(f, 'meta.negate', notOther && notMissing)); - } else { - set(filter, 'meta.negate', notOther && notMissing); + + if (filter) { + const notOther = value.raw !== '__other__'; + const notMissing = value.raw !== '__missing__'; + if (Array.isArray(filter)) { + filter.forEach(f => set(f, 'meta.negate', notOther && notMissing)); + } else { + set(filter, 'meta.negate', notOther && notMissing); + } + queryFilter.addFilters(filter); } - queryFilter.addFilters(filter); }), }; }); diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.test.ts b/src/legacy/core_plugins/data/public/search/expressions/create_filter.test.ts new file mode 100644 index 0000000000000..890ec81778d4b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { + fieldFormats, + FieldFormatsGetConfigFn, + esFilters, +} from '../../../../../../plugins/data/public'; +import { createFilter } from './create_filter'; +import { TabbedTable } from '../tabify'; +import { AggConfigs } from '../aggs/agg_configs'; +import { IAggConfig } from '../aggs/agg_config'; +import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; + +describe('createFilter', () => { + let table: TabbedTable; + let aggConfig: IAggConfig; + + const typesRegistry = mockAggTypesRegistry(); + + const getAggConfigs = (type: string, params: any) => { + const field = { + name: 'bytes', + filterable: true, + indexPattern: { + id: '1234', + }, + format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new AggConfigs( + indexPattern, + [ + { + id: type, + type, + schema: 'buckets', + params, + }, + ], + { typesRegistry } + ); + }; + + const aggConfigParams: Record = { + field: 'bytes', + interval: 30, + otherBucket: true, + }; + + beforeEach(() => { + table = { + columns: [ + { + id: '1-1', + name: 'test', + aggConfig, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }; + mockDataServices(); + }); + + test('ignores event when cell value is not provided', async () => { + aggConfig = getAggConfigs('histogram', aggConfigParams).aggs[0]; + const filters = await createFilter([aggConfig], table, 0, -1, null); + + expect(filters).not.toBeDefined(); + }); + + test('handles an event when aggregations type is a terms', async () => { + aggConfig = getAggConfigs('terms', aggConfigParams).aggs[0]; + const filters = await createFilter([aggConfig], table, 0, 0, 'test'); + + expect(filters).toBeDefined(); + + if (filters) { + expect(filters.length).toEqual(1); + expect(filters[0].query.match_phrase.bytes).toEqual('2048'); + } + }); + + test('handles an event when aggregations type is not terms', async () => { + aggConfig = getAggConfigs('histogram', aggConfigParams).aggs[0]; + const filters = await createFilter([aggConfig], table, 0, 0, 'test'); + + expect(filters).toBeDefined(); + + if (filters) { + expect(filters.length).toEqual(1); + + const [rangeFilter] = filters; + + if (esFilters.isRangeFilter(rangeFilter)) { + expect(rangeFilter.range.bytes.gte).toEqual(2048); + expect(rangeFilter.range.bytes.lt).toEqual(2078); + } + } + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js b/src/legacy/core_plugins/data/public/search/expressions/create_filter.ts similarity index 78% rename from src/legacy/core_plugins/data/public/search/expressions/create_filter.js rename to src/legacy/core_plugins/data/public/search/expressions/create_filter.ts index 3f4028a9b5525..77e011932195c 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.ts @@ -17,7 +17,11 @@ * under the License. */ -const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { +import { IAggConfig } from 'ui/agg_types'; +import { Filter } from '../../../../../../plugins/data/public'; +import { TabbedTable } from '../tabify'; + +const getOtherBucketFilterTerms = (table: TabbedTable, columnIndex: number, rowIndex: number) => { if (rowIndex === -1) { return []; } @@ -41,11 +45,17 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { ]; }; -const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { +const createFilter = ( + aggConfigs: IAggConfig[], + table: TabbedTable, + columnIndex: number, + rowIndex: number, + cellValue: any +) => { const column = table.columns[columnIndex]; const aggConfig = aggConfigs[columnIndex]; - let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + let filter: Filter[] = []; + const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -56,6 +66,10 @@ const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { filter = aggConfig.createFilter(value); } + if (!filter) { + return; + } + if (!Array.isArray(filter)) { filter = [filter]; } diff --git a/src/legacy/core_plugins/data/public/search/mocks.ts b/src/legacy/core_plugins/data/public/search/mocks.ts index 86b6a928dc5b4..46c26dc8f1bd0 100644 --- a/src/legacy/core_plugins/data/public/search/mocks.ts +++ b/src/legacy/core_plugins/data/public/search/mocks.ts @@ -17,8 +17,11 @@ * under the License. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { coreMock } from '../../../../../../src/core/public/mocks'; import { SearchSetup, SearchStart } from './search_service'; import { AggTypesRegistrySetup, AggTypesRegistryStart } from './aggs/agg_types_registry'; +import { getCalculateAutoTimeExpression } from './aggs'; import { AggConfigs } from './aggs/agg_configs'; import { mockAggTypesRegistry } from './aggs/test_helpers'; @@ -41,12 +44,12 @@ const aggTypeConfigMock = () => ({ params: [aggTypeBaseParamMock()], }); -export const aggTypesRegistrySetupMock = (): MockedKeys => ({ +export const aggTypesRegistrySetupMock = (): AggTypesRegistrySetup => ({ registerBucket: jest.fn(), registerMetric: jest.fn(), }); -export const aggTypesRegistryStartMock = (): MockedKeys => ({ +export const aggTypesRegistryStartMock = (): AggTypesRegistryStart => ({ get: jest.fn().mockImplementation(aggTypeConfigMock), getBuckets: jest.fn().mockImplementation(() => [aggTypeConfigMock()]), getMetrics: jest.fn().mockImplementation(() => [aggTypeConfigMock()]), @@ -56,17 +59,18 @@ export const aggTypesRegistryStartMock = (): MockedKeys = })), }); -export const searchSetupMock = (): MockedKeys => ({ +export const searchSetupMock = (): SearchSetup => ({ aggs: { + calculateAutoTimeExpression: getCalculateAutoTimeExpression(coreMock.createSetup().uiSettings), types: aggTypesRegistrySetupMock(), }, }); -export const searchStartMock = (): MockedKeys => ({ +export const searchStartMock = (): SearchStart => ({ aggs: { + calculateAutoTimeExpression: getCalculateAutoTimeExpression(coreMock.createStart().uiSettings), createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { - schemas, typesRegistry: mockAggTypesRegistry(), }); }), @@ -78,7 +82,6 @@ export const searchStartMock = (): MockedKeys => ({ FieldParamType: jest.fn(), MetricAggType: jest.fn(), parentPipelineAggHelper: jest.fn() as any, - setBounds: jest.fn(), siblingPipelineAggHelper: jest.fn() as any, }, }, diff --git a/src/legacy/core_plugins/data/public/search/search_service.ts b/src/legacy/core_plugins/data/public/search/search_service.ts index 6754c0e3551af..2d01ac446d951 100644 --- a/src/legacy/core_plugins/data/public/search/search_service.ts +++ b/src/legacy/core_plugins/data/public/search/search_service.ts @@ -29,14 +29,15 @@ import { AggConfigs, CreateAggConfigParams, FieldParamType, + getCalculateAutoTimeExpression, MetricAggType, aggTypeFieldFilters, - setBounds, parentPipelineAggHelper, siblingPipelineAggHelper, } from './aggs'; interface AggsSetup { + calculateAutoTimeExpression: ReturnType; types: AggTypesRegistrySetup; } @@ -47,11 +48,11 @@ interface AggsStartLegacy { FieldParamType: typeof FieldParamType; MetricAggType: typeof MetricAggType; parentPipelineAggHelper: typeof parentPipelineAggHelper; - setBounds: typeof setBounds; siblingPipelineAggHelper: typeof siblingPipelineAggHelper; } interface AggsStart { + calculateAutoTimeExpression: ReturnType; createAggConfigs: ( indexPattern: IndexPattern, configStates?: CreateAggConfigParams[], @@ -85,6 +86,7 @@ export class SearchService { return { aggs: { + calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), types: aggTypesSetup, }, }; @@ -94,9 +96,9 @@ export class SearchService { const aggTypesStart = this.aggTypesRegistry.start(); return { aggs: { + calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { - schemas, typesRegistry: aggTypesStart, }); }, @@ -108,7 +110,6 @@ export class SearchService { FieldParamType, MetricAggType, parentPipelineAggHelper, // TODO make static - setBounds, // TODO make static siblingPipelineAggHelper, // TODO make static }, }, diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts index 6c5dc790ef976..b7dadc3f65d82 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts @@ -19,7 +19,7 @@ import { tabifyGetColumns } from './get_columns'; import { TabbedAggColumn } from './types'; -import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; +import { AggConfigs } from '../aggs'; import { mockAggTypesRegistry, mockDataServices } from '../aggs/test_helpers'; describe('get columns', () => { @@ -45,26 +45,10 @@ describe('get columns', () => { return new AggConfigs(indexPattern, aggs, { typesRegistry, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - min: 1, - defaults: [{ schema: 'metric', type: 'count' }], - }, - ]).all, }); }; - test('should inject a count metric if no aggs exist', () => { - const columns = tabifyGetColumns(createAggConfigs().aggs, true); - - expect(columns).toHaveLength(1); - expect(columns[0]).toHaveProperty('aggConfig'); - expect(columns[0].aggConfig.type).toHaveProperty('name', 'count'); - }); - - test('should inject a count metric if only buckets exist', () => { + test('should inject the metric after each bucket if the vis is hierarchical', () => { const columns = tabifyGetColumns( createAggConfigs([ { @@ -72,18 +56,6 @@ describe('get columns', () => { schema: 'segment', params: { field: '@timestamp', interval: '10s' }, }, - ]).aggs, - true - ); - - expect(columns).toHaveLength(2); - expect(columns[1]).toHaveProperty('aggConfig'); - expect(columns[1].aggConfig.type).toHaveProperty('name', 'count'); - }); - - test('should inject the metric after each bucket if the vis is hierarchical', () => { - const columns = tabifyGetColumns( - createAggConfigs([ { type: 'date_histogram', schema: 'segment', @@ -100,9 +72,7 @@ describe('get columns', () => { params: { field: '@timestamp', interval: '10s' }, }, { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, + type: 'count', }, ]).aggs, false diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts index 94301eedac74a..91835bc948abb 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts @@ -18,7 +18,7 @@ */ import { TabbedAggResponseWriter } from './response_writer'; -import { AggConfigs, AggGroupNames, Schemas, BUCKET_TYPES } from '../aggs'; +import { AggConfigs, BUCKET_TYPES } from '../aggs'; import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; import { TabbedResponseWriterOptions } from './types'; @@ -39,6 +39,7 @@ describe('TabbedAggResponseWriter class', () => { field: 'geo.src', }, }, + { type: 'count' }, ]; const twoSplitsAggConfig = [ @@ -54,6 +55,7 @@ describe('TabbedAggResponseWriter class', () => { field: 'machine.os.raw', }, }, + { type: 'count' }, ]; const createResponseWritter = (aggs: any[] = [], opts?: Partial) => { @@ -73,14 +75,6 @@ describe('TabbedAggResponseWriter class', () => { return new TabbedAggResponseWriter( new AggConfigs(indexPattern, aggs, { typesRegistry, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - min: 1, - defaults: [{ schema: 'metric', type: 'count' }], - }, - ]).all, }), { metricsAtAllLevels: false, diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts index db4ad3bdea96b..7e7748c00ab43 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts @@ -19,7 +19,7 @@ import { IndexPattern } from '../../../../../../plugins/data/public'; import { tabifyAggResponse } from './tabify'; -import { IAggConfig, IAggConfigs, AggGroupNames, Schemas, AggConfigs } from '../aggs'; +import { IAggConfig, IAggConfigs, AggConfigs } from '../aggs'; import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; @@ -42,21 +42,13 @@ describe('tabifyAggResponse Integration', () => { return new AggConfigs(indexPattern, aggs, { typesRegistry, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - min: 1, - defaults: [{ schema: 'metric', type: 'count' }], - }, - ]).all, }); }; const mockAggConfig = (agg: any): IAggConfig => (agg as unknown) as IAggConfig; test('transforms a simple response properly', () => { - const aggConfigs = createAggConfigs(); + const aggConfigs = createAggConfigs([{ type: 'count' } as any]); const resp = tabifyAggResponse(aggConfigs, metricOnly, { metricsAtAllLevels: true, diff --git a/src/legacy/core_plugins/kibana/public/.eslintrc.js b/src/legacy/core_plugins/kibana/public/.eslintrc.js index b3ee0a8fa7b04..e7171a5291d26 100644 --- a/src/legacy/core_plugins/kibana/public/.eslintrc.js +++ b/src/legacy/core_plugins/kibana/public/.eslintrc.js @@ -77,7 +77,7 @@ module.exports = { { basePath: path.resolve(__dirname, '../../../../../'), zones: topLevelRestricedZones.concat( - buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools', 'home']) + buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools']) ), }, ], diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index 6b0d2368cc1a2..c58307adaf38c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -33,11 +33,10 @@ import { import { DiscoverStartPlugins } from './plugin'; import { SharePluginStart } from '../../../../../plugins/share/public'; -import { SavedSearch } from './np_ready/types'; import { DocViewsRegistry } from './np_ready/doc_views/doc_views_registry'; import { ChartsPluginStart } from '../../../../../plugins/charts/public'; import { VisualizationsStart } from '../../../visualizations/public'; -import { createSavedSearchesLoader } from '../../../../../plugins/discover/public'; +import { createSavedSearchesLoader, SavedSearch } from '../../../../../plugins/discover/public'; export interface DiscoverServices { addBasePath: (path: string) => string; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index 3fd3c5b5b7633..18254aeca5094 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -23,7 +23,7 @@

{{screenTitle}}

-